From 8f352e880aaa045b44d09cf2d694ca225c5dc4dd Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Thu, 14 Nov 2024 19:45:02 +0530 Subject: [PATCH 01/19] added test cases for asset bed (#2532) added test cases for asset bed (#2532) --- care/facility/api/serializers/asset.py | 9 +- care/facility/api/serializers/bed.py | 12 +- care/facility/tests/test_asset_bed_api.py | 219 -------- .../facility/tests/test_asset_location_api.py | 6 +- care/facility/tests/test_assetbed_api.py | 531 ++++++++++++++++++ care/facility/tests/test_bed_api.py | 17 +- care/utils/tests/test_utils.py | 12 +- 7 files changed, 571 insertions(+), 235 deletions(-) delete mode 100644 care/facility/tests/test_asset_bed_api.py create mode 100644 care/facility/tests/test_assetbed_api.py diff --git a/care/facility/api/serializers/asset.py b/care/facility/api/serializers/asset.py index f403361e1a..ddb6a8e5d4 100644 --- a/care/facility/api/serializers/asset.py +++ b/care/facility/api/serializers/asset.py @@ -7,7 +7,7 @@ from django.utils.timezone import now from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from rest_framework.exceptions import ValidationError +from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.serializers import ( CharField, JSONField, @@ -174,11 +174,14 @@ def validate(self, attrs): facilities = get_facility_queryset(user) if not facilities.filter(id=location.facility.id).exists(): - raise PermissionError + error_message = ( + "You do not have permission to access this facility's asset." + ) + raise PermissionDenied(error_message) del attrs["location"] attrs["current_location"] = location - # validate that warraty date is not in the past + # validate that warranty date is not in the past if warranty_amc_end_of_validity := attrs.get("warranty_amc_end_of_validity"): # pop out warranty date if it is not changed if ( diff --git a/care/facility/api/serializers/bed.py b/care/facility/api/serializers/bed.py index 031d2a68c1..508bc25bc4 100644 --- a/care/facility/api/serializers/bed.py +++ b/care/facility/api/serializers/bed.py @@ -2,7 +2,7 @@ from django.db.models import Exists, OuterRef, Q from django.shortcuts import get_object_or_404 from django.utils import timezone -from rest_framework.exceptions import ValidationError +from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.serializers import ( BooleanField, CharField, @@ -74,7 +74,10 @@ def validate(self, attrs): if (not facilities.filter(id=location.facility.id).exists()) or ( not facilities.filter(id=facility.id).exists() ): - raise PermissionError + error_message = ( + "You do not have permission to access this facility's bed." + ) + raise PermissionDenied(error_message) del attrs["location"] attrs["location"] = location attrs["facility"] = facility @@ -110,7 +113,10 @@ def validate(self, attrs): if ( not facilities.filter(id=asset.current_location.facility.id).exists() ) or (not facilities.filter(id=bed.facility.id).exists()): - raise PermissionError + error_message = ( + "You do not have permission to access this facility's assetbed." + ) + raise PermissionDenied(error_message) if AssetBed.objects.filter(asset=asset, bed=bed).exists(): raise ValidationError( {"non_field_errors": "Asset is already linked to bed"} diff --git a/care/facility/tests/test_asset_bed_api.py b/care/facility/tests/test_asset_bed_api.py deleted file mode 100644 index 4ed81a36b8..0000000000 --- a/care/facility/tests/test_asset_bed_api.py +++ /dev/null @@ -1,219 +0,0 @@ -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.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): - 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) - - 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 - 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/facility/tests/test_asset_location_api.py b/care/facility/tests/test_asset_location_api.py index 9e8280d617..6bcfed7850 100644 --- a/care/facility/tests/test_asset_location_api.py +++ b/care/facility/tests/test_asset_location_api.py @@ -21,7 +21,7 @@ def setUpTestData(cls) -> None: 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.asset_bed = cls.create_assetbed(cls.bed, cls.asset) 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) @@ -36,8 +36,8 @@ def setUpTestData(cls) -> None: 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 + cls.assetbed_second = cls.create_assetbed( + cls.asset_bed_second, cls.asset_second ) def test_list_asset_locations(self): diff --git a/care/facility/tests/test_assetbed_api.py b/care/facility/tests/test_assetbed_api.py new file mode 100644 index 0000000000..2129ef1a00 --- /dev/null +++ b/care/facility/tests/test_assetbed_api.py @@ -0,0 +1,531 @@ +from rest_framework import status +from rest_framework.test import APITestCase + +from care.facility.models import AssetBed, Bed +from care.users.models import User +from care.utils.assetintegration.asset_classes import AssetClasses +from care.utils.tests.test_utils import TestUtils + + +class AssetBedViewSetTests(TestUtils, APITestCase): + """ + Test class for AssetBed + """ + + @classmethod + def setUpTestData(cls) -> None: + 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.facility2 = cls.create_facility( + cls.super_user, cls.district, cls.local_body + ) + cls.user = cls.create_user( + "user", + district=cls.district, + local_body=cls.local_body, + home_facility=cls.facility, + ) # user from facility + cls.foreign_user = cls.create_user( + "foreign_user", + district=cls.district, + local_body=cls.local_body, + home_facility=cls.facility2, + ) # user outside the facility + cls.patient = cls.create_patient( + cls.district, cls.facility, local_body=cls.local_body + ) + cls.asset_location1 = cls.create_asset_location(cls.facility) + cls.asset1 = cls.create_asset( + cls.asset_location1, asset_class=AssetClasses.HL7MONITOR.name + ) + cls.bed1 = Bed.objects.create( + name="bed1", location=cls.asset_location1, facility=cls.facility + ) + cls.asset_location2 = cls.create_asset_location(cls.facility) + # camera asset + cls.asset2 = cls.create_asset( + cls.asset_location2, asset_class=AssetClasses.ONVIF.name + ) + cls.bed2 = Bed.objects.create( + name="bed2", location=cls.asset_location2, facility=cls.facility + ) + cls.asset_location3 = cls.create_asset_location(cls.facility) + cls.asset3 = cls.create_asset( + cls.asset_location3, asset_class=AssetClasses.VENTILATOR.name + ) + cls.bed3 = Bed.objects.create( + name="bed3", location=cls.asset_location3, facility=cls.facility + ) + # for testing create, put and patch requests + cls.bed4 = Bed.objects.create( + name="bed4", location=cls.asset_location3, facility=cls.facility + ) + cls.foreign_asset_location = cls.create_asset_location(cls.facility2) + cls.foreign_asset = cls.create_asset(cls.foreign_asset_location) + cls.foreign_bed = Bed.objects.create( + name="foreign_bed", + location=cls.foreign_asset_location, + facility=cls.facility2, + ) + + cls.create_assetbed(bed=cls.bed2, asset=cls.asset2) + cls.create_assetbed(bed=cls.bed3, asset=cls.asset3) + + # assetbed for different facility + cls.create_assetbed(bed=cls.foreign_bed, asset=cls.foreign_asset) + + def setUp(self) -> None: + super().setUp() + self.assetbed = self.create_assetbed(bed=self.bed1, asset=self.asset1) + + def get_base_url(self) -> str: + return "/api/v1/assetbed" + + def get_url(self, external_id=None): + """ + Constructs the url for ambulance api + """ + base_url = f"{self.get_base_url()}/" + if external_id is not None: + base_url += f"{external_id}/" + return base_url + + def test_list_assetbed(self): + # assetbed accessible to facility 1 user (current user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 3) + + # logging in as foreign user + self.client.force_login(self.foreign_user) + + # assetbed accessible to facility 2 user (foreign user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 1) + + # logging in as superuser + self.client.force_login(self.super_user) + + # access to all assetbed + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], AssetBed.objects.count()) + + # testing for filters + response = self.client.get(self.get_url(), {"asset": self.asset1.external_id}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["count"], AssetBed.objects.filter(asset=self.asset1).count() + ) + + response = self.client.get(self.get_url(), {"bed": self.bed1.external_id}) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["count"], AssetBed.objects.filter(bed=self.bed1).count() + ) + self.assertEqual( + response.data["results"][0]["bed_object"]["name"], self.bed1.name + ) + + response = self.client.get( + self.get_url(), {"facility": self.facility.external_id} + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["count"], + AssetBed.objects.filter(bed__facility=self.facility).count(), + ) + + def test_create_assetbed(self): + # Missing asset and bed + missing_fields_data = {} + response = self.client.post(self.get_url(), missing_fields_data, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("asset", response.data) + self.assertIn("bed", response.data) + + # Invalid asset UUID + invalid_asset_uuid_data = { + "asset": "invalid-uuid", + "bed": str(self.bed1.external_id), + } + response = self.client.post( + self.get_url(), invalid_asset_uuid_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("asset", response.data) + + # Invalid bed UUID + invalid_bed_uuid_data = { + "asset": str(self.asset1.external_id), + "bed": "invalid-uuid", + } + response = self.client.post( + self.get_url(), invalid_bed_uuid_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("bed", response.data) + + # Non-existent asset UUID + non_existent_asset_uuid_data = { + "asset": "11111111-1111-1111-1111-111111111111", + "bed": str(self.bed1.external_id), + } + response = self.client.post( + self.get_url(), non_existent_asset_uuid_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # Non-existent bed UUID + non_existent_bed_uuid_data = { + "asset": str(self.asset1.external_id), + "bed": "11111111-1111-1111-1111-111111111111", + } + response = self.client.post( + self.get_url(), non_existent_bed_uuid_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # User does not have access to foreign facility + foreign_user_data = { + "asset": str(self.foreign_asset.external_id), + "bed": str(self.foreign_bed.external_id), + } + self.client.force_login(self.user) # Ensure current user is logged in + response = self.client.post(self.get_url(), foreign_user_data, format="json") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Invalid asset class (e.g., VENTILATOR) + invalid_asset_class_data = { + "asset": str(self.asset3.external_id), # VENTILATOR asset class + "bed": str(self.bed1.external_id), + } + response = self.client.post( + self.get_url(), invalid_asset_class_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("asset", response.data) + + # Asset and bed in different facilities + asset_bed_different_facilities = { + "asset": str(self.foreign_asset.external_id), + "bed": str(self.bed1.external_id), + } + response = self.client.post( + self.get_url(), asset_bed_different_facilities, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + # Trying to create a duplicate assetbed with bed2 and asset2 (assetbed already exist with same bed and asset) + duplicate_asset_class_data = { + "asset": str(self.asset2.external_id), # asset2 is already assigned to bed2 + "bed": str(self.bed2.external_id), + } + response = self.client.post( + self.get_url(), duplicate_asset_class_data, format="json" + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + # Successfully create AssetBed with valid data + valid_data = { + "asset": str(self.asset1.external_id), + "bed": str(self.bed4.external_id), + } + response = self.client.post(self.get_url(), valid_data, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_retrieve_assetbed(self): + response = self.client.get(self.get_url(external_id=self.assetbed.external_id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["asset_object"]["id"], str(self.assetbed.asset.external_id) + ) + self.assertEqual( + response.data["bed_object"]["id"], str(self.assetbed.bed.external_id) + ) + + def test_update_assetbed(self): + # checking old values + response = self.client.get(self.get_url(external_id=self.assetbed.external_id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["meta"], {}) + self.assertEqual( + response.data["asset_object"]["id"], str(self.assetbed.asset.external_id) + ) + self.assertEqual( + response.data["bed_object"]["id"], str(self.assetbed.bed.external_id) + ) + + invalid_updated_data = { + "asset": self.asset2.external_id, + "meta": {"sample_data": "sample value"}, + } + response = self.client.put( + self.get_url(external_id=self.assetbed.external_id), + invalid_updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + invalid_updated_data = { + "bed": self.bed2.external_id, + "meta": {"sample_data": "sample value"}, + } + response = self.client.put( + self.get_url(external_id=self.assetbed.external_id), + invalid_updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + updated_data = { + "bed": self.bed4.external_id, + "asset": self.asset2.external_id, + "meta": {"sample_data": "sample value"}, + } + response = self.client.put( + self.get_url(external_id=self.assetbed.external_id), + updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assetbed.refresh_from_db() + + self.assertEqual(self.assetbed.bed.external_id, self.bed4.external_id) + self.assertEqual(self.assetbed.asset.external_id, self.asset2.external_id) + self.assertEqual(self.assetbed.meta, {"sample_data": "sample value"}) + + def test_patch_assetbed(self): + # checking old values + response = self.client.get(self.get_url(external_id=self.assetbed.external_id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["meta"], {}) + self.assertEqual( + response.data["asset_object"]["id"], str(self.assetbed.asset.external_id) + ) + self.assertEqual( + response.data["bed_object"]["id"], str(self.assetbed.bed.external_id) + ) + + invalid_updated_data = { + "asset": self.asset2.external_id, + "meta": {"sample_data": "sample value"}, + } + response = self.client.patch( + self.get_url(external_id=self.assetbed.external_id), + invalid_updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + invalid_updated_data = { + "bed": self.bed4.external_id, + "meta": {"sample_data": "sample value"}, + } + response = self.client.patch( + self.get_url(external_id=self.assetbed.external_id), + invalid_updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + updated_data = { + "bed": self.bed4.external_id, + "asset": self.asset2.external_id, + } + + response = self.client.patch( + self.get_url(external_id=self.assetbed.external_id), + updated_data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assetbed.refresh_from_db() + + self.assertEqual(self.assetbed.bed.external_id, self.bed4.external_id) + self.assertEqual(self.assetbed.asset.external_id, self.asset2.external_id) + + def test_delete_assetbed(self): + # confirming that the object exist + response = self.client.get(self.get_url(external_id=self.assetbed.external_id)) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.delete( + self.get_url(external_id=self.assetbed.external_id) + ) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + # confirming if it's deleted + response = self.client.get(self.get_url(external_id=self.assetbed.external_id)) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + # confirming using db + self.assetbed.refresh_from_db() + self.assertFalse( + AssetBed.objects.filter(external_id=self.assetbed.external_id).exists() + ) + + def test_linking_multiple_cameras_to_a_bed(self): + # We already have camera linked(asset2) to bed2 + # Attempt linking another camera to same bed. + new_camera_asset = self.create_asset( + self.asset_location2, asset_class=AssetClasses.ONVIF.name + ) + data = { + "bed": self.bed2.external_id, + "asset": new_camera_asset.external_id, + } + res = self.client.post(self.get_url(), data, format="json") + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + + def test_linking_multiple_hl7_monitors_to_a_bed(self): + # We already have hl7 monitor linked(asset1) to bed1) + # Attempt linking another hl7 monitor to same bed. + new_hl7_monitor_asset = self.create_asset( + self.asset_location2, asset_class=AssetClasses.HL7MONITOR.name + ) + data = { + "bed": self.bed1.external_id, + "asset": new_hl7_monitor_asset.external_id, + } + res = self.client.post("/api/v1/assetbed/", data, format="json") + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + +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_assetbed(cls.bed, cls.asset1) + cls.asset_bed2 = cls.create_assetbed(cls.bed, cls.asset2) + + 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/facility/tests/test_bed_api.py b/care/facility/tests/test_bed_api.py index ce334dd6e4..9bceece342 100644 --- a/care/facility/tests/test_bed_api.py +++ b/care/facility/tests/test_bed_api.py @@ -75,7 +75,7 @@ def test_list_beds(self): self.client.logout() def test_create_beds(self): - sample_data = { + base_data = { "name": "Sample Beds", "bed_type": 2, "location": self.asset_location.external_id, @@ -83,23 +83,27 @@ def test_create_beds(self): "number_of_beds": 10, "description": "This is a sample bed description", } + sample_data = base_data.copy() # Create a fresh copy of the base data response = self.client.post("/api/v1/bed/", sample_data, format="json") self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(Bed.objects.filter(bed_type=2).count(), 10) # without location + sample_data = base_data.copy() sample_data.update({"location": None}) response = self.client.post("/api/v1/bed/", sample_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.data["location"][0], "This field may not be null.") # without facility + sample_data = base_data.copy() sample_data.update({"facility": None}) response = self.client.post("/api/v1/bed/", sample_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.data["facility"][0], "This field may not be null.") # Test - if beds > 100 + sample_data = base_data.copy() sample_data.update({"number_of_beds": 200}) response = self.client.post("/api/v1/bed/", sample_data, format="json") self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) @@ -108,6 +112,17 @@ def test_create_beds(self): "Cannot create more than 100 beds at once.", ) + # creating bed in different facility + sample_data = base_data.copy() + sample_data.update( + { + "location": self.asset_location2.external_id, + "facility": self.facility2.external_id, + } + ) + response = self.client.post("/api/v1/bed/", sample_data, format="json") + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_retrieve_bed(self): response = self.client.get(f"/api/v1/bed/{self.bed1.external_id}/") self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index 91d4ac8d67..fbc286a337 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -446,12 +446,6 @@ 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, @@ -728,6 +722,12 @@ def create_prescription( data.update(**kwargs) return Prescription.objects.create(**data) + @classmethod + def create_assetbed(cls, bed: Bed, asset: Asset, **kwargs) -> AssetBed: + data = {"bed": bed, "asset": asset} + data.update(kwargs) + return AssetBed.objects.create(**data) + def get_list_representation(self, obj) -> dict: """ Returns the dict representation of the obj in list API From 05cf1cf8a8ff894828170b47fdb8a6ed077d0e5e Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Thu, 14 Nov 2024 19:45:16 +0530 Subject: [PATCH 02/19] added tests for patient transfer (#2564) added tests for patient transfer (#2564) --- care/facility/api/viewsets/patient.py | 2 +- care/facility/tests/test_patient_api.py | 164 +++++++++++++++++++++++- 2 files changed, 164 insertions(+), 2 deletions(-) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 72731cd6e2..963b6d4731 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -589,7 +589,7 @@ def list(self, request, *args, **kwargs): @action(detail=True, methods=["POST"]) def transfer(self, request, *args, **kwargs): patient = PatientRegistration.objects.get(external_id=kwargs["external_id"]) - facility = Facility.objects.get(external_id=request.data["facility"]) + facility = get_object_or_404(Facility, external_id=request.data["facility"]) if patient.is_expired: return Response( diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index 6facfdd3ad..37accc7c2c 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -4,7 +4,7 @@ from rest_framework import status from rest_framework.test import APITestCase -from care.facility.models import PatientNoteThreadChoices +from care.facility.models import PatientNoteThreadChoices, ShiftingRequest from care.facility.models.file_upload import FileUpload from care.facility.models.icd11_diagnosis import ( ConditionVerificationStatus, @@ -913,6 +913,8 @@ def setUpTestData(cls): cls.patient.save() def test_patient_transfer(self): + # test transfer of patient to a outside facility with allow_transfer set to "True" + # test transfer patient with dob self.client.force_authenticate(user=self.super_user) response = self.client.post( f"/api/v1/patient/{self.patient.external_id}/transfer/", @@ -1000,6 +1002,166 @@ def test_transfer_disallowed_by_facility(self): "Patient transfer cannot be completed because the source facility does not permit it", ) + def test_transfer_within_facility(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "date_of_birth": "1992-04-01", + "facility": self.facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_406_NOT_ACCEPTABLE) + self.assertEqual( + response.data["Patient"], + "Patient transfer cannot be completed because the patient has an active consultation in the same facility", + ) + + def test_transfer_without_dob(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "age": "32", + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.patient.refresh_from_db() + self.consultation.refresh_from_db() + + self.assertEqual(self.patient.facility, self.destination_facility) + + self.assertEqual( + self.consultation.new_discharge_reason, NewDischargeReasonEnum.REFERRED + ) + self.assertIsNotNone(self.consultation.discharge_date) + + def test_transfer_with_no_active_consultation(self): + # Mocking discharge of the patient + self.consultation.discharge_date = now() + self.consultation.save() + + # Ensure transfer succeeds when there's no active consultation + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": 1992, + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Refresh patient data + self.patient.refresh_from_db() + + # Assert the patient's facility has been updated + self.assertEqual(self.patient.facility, self.destination_facility) + + def test_transfer_with_incorrect_year_of_birth(self): + # Set the patient's facility to allow transfers + self.patient.allow_transfer = True + self.patient.save() + + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": 1990, # Incorrect year of birth + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + response.data["year_of_birth"][0], "Year of birth does not match" + ) + + def test_auto_reject_active_shifting_requests_upon_transfer(self): + # Create a mock shifting request that is still active (PENDING status) + shifting_request = ShiftingRequest.objects.create( + patient=self.patient, + origin_facility=self.facility, + status=10, # PENDING status + comments="Initial request", + created_by=self.super_user, + ) + + # Perform the patient transfer + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": 1992, + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Refresh the shifting request and verify it was auto-rejected + shifting_request.refresh_from_db() + self.assertEqual(shifting_request.status, 30) # REJECTED status + self.assertIn( + f"The shifting request was auto rejected by the system as the patient was moved to {self.destination_facility.name}", + shifting_request.comments, + ) + + def test_transfer_with_matching_year_of_birth(self): + # Set the patient's facility to allow transfers + self.patient.allow_transfer = True + self.patient.save() + + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": self.patient.year_of_birth, # Correct year of birth + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Refresh patient data + self.patient.refresh_from_db() + + # Assert the patient's facility has been updated + self.assertEqual(self.patient.facility, self.destination_facility) + + def test_transfer_to_non_existent_facility(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": 1992, + "facility": "dff237c5-9410-4714-9101-399941b60ede", # Non-existent facility + }, + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_transfer_with_invalid_data(self): + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": "invalid-year", # Invalid year of birth + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("year_of_birth", response.data) + + def test_unauthorized_transfer_request(self): + # Not authenticating the user to test unauthorized access + response = self.client.post( + f"/api/v1/patient/{self.patient.external_id}/transfer/", + { + "year_of_birth": 1992, + "facility": self.destination_facility.external_id, + }, + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + class PatientSearchTestCase(TestUtils, APITestCase): @classmethod From 5b2b30404dfb75429a47bc8fddc50da9c7b30963 Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Thu, 14 Nov 2024 19:46:30 +0530 Subject: [PATCH 03/19] Clean up asset classes (#2494) Clean up asset classes (#2494) --------- Co-authored-by: Aakash Singh --- care/facility/api/viewsets/asset.py | 4 +- care/facility/tests/test_asset_api.py | 141 ++++++++++++++++++++++ care/utils/assetintegration/base.py | 38 ++++-- care/utils/assetintegration/hl7monitor.py | 10 +- care/utils/assetintegration/onvif.py | 20 +-- care/utils/assetintegration/schema.py | 34 ++++++ care/utils/assetintegration/ventilator.py | 10 +- 7 files changed, 227 insertions(+), 30 deletions(-) create mode 100644 care/utils/assetintegration/schema.py diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index 8b24bebb51..caf784cb9f 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -61,6 +61,7 @@ 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.assetintegration.base import BaseAssetIntegration 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 @@ -389,7 +390,6 @@ def operate_assets(self, request, *args, **kwargs): This API is used to operate assets. API accepts the asset_id and action as parameters. """ try: - action = request.data["action"] asset: Asset = self.get_object() middleware_hostname = ( asset.meta.get( @@ -405,7 +405,7 @@ def operate_assets(self, request, *args, **kwargs): "middleware_hostname": middleware_hostname, } ) - result = asset_class.handle_action(action) + result = asset_class.handle_action(**request.data["action"]) return Response({"result": result}, status=status.HTTP_200_OK) except ValidationError as e: diff --git a/care/facility/tests/test_asset_api.py b/care/facility/tests/test_asset_api.py index a0771b089b..3989a19eef 100644 --- a/care/facility/tests/test_asset_api.py +++ b/care/facility/tests/test_asset_api.py @@ -1,10 +1,14 @@ from django.utils.timezone import now, timedelta from rest_framework import status +from rest_framework.exceptions import ValidationError from rest_framework.test import APITestCase from care.facility.models import Asset, Bed from care.users.models import User from care.utils.assetintegration.asset_classes import AssetClasses +from care.utils.assetintegration.hl7monitor import HL7MonitorAsset +from care.utils.assetintegration.onvif import OnvifAsset +from care.utils.assetintegration.ventilator import VentilatorAsset from care.utils.tests.test_utils import TestUtils @@ -31,6 +35,143 @@ def setUp(self) -> None: super().setUp() self.asset = self.create_asset(self.asset_location) + def validate_invalid_meta(self, asset_class, meta): + with self.assertRaises(ValidationError): + asset_class(meta) + + def test_meta_validations_for_onvif_asset(self): + valid_meta = { + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + } + onvif_asset = OnvifAsset(valid_meta) + self.assertEqual(onvif_asset.middleware_hostname, "middleware.local") + self.assertEqual(onvif_asset.host, "192.168.0.1") + self.assertEqual(onvif_asset.username, "username") + self.assertEqual(onvif_asset.password, "password") + self.assertEqual(onvif_asset.access_key, "access_key") + self.assertTrue(onvif_asset.insecure_connection) + + invalid_meta_cases = [ + # Invalid format for camera_access_key + { + "id": "123", + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "camera_access_key": "invalid_format", + }, + # Missing username/password in camera_access_key + { + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "camera_access_key": "invalid_format", + }, + # Missing middleware_hostname + { + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + }, + # Missing local_ip_address + { + "middleware_hostname": "middleware.local", + "camera_access_key": "username:password:access_key", + }, + # Invalid value for insecure_connection + { + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + "middleware_hostname": "middleware.local", + "insecure_connection": "invalid_value", + }, + ] + for meta in invalid_meta_cases: + self.validate_invalid_meta(OnvifAsset, meta) + + def test_meta_validations_for_ventilator_asset(self): + valid_meta = { + "id": "123", + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + } + ventilator_asset = VentilatorAsset(valid_meta) + self.assertEqual(ventilator_asset.middleware_hostname, "middleware.local") + self.assertEqual(ventilator_asset.host, "192.168.0.1") + self.assertTrue(ventilator_asset.insecure_connection) + + invalid_meta_cases = [ + # Missing id + { + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + }, + # Missing middleware_hostname + {"id": "123", "local_ip_address": "192.168.0.1"}, + # Missing local_ip_address + {"id": "123", "middleware_hostname": "middleware.local"}, + # Invalid insecure_connection + { + "id": "123", + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "insecure_connection": "invalid_value", + }, + # Camera access key not required for ventilator, invalid meta + { + "id": "21", + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + }, + ] + for meta in invalid_meta_cases: + self.validate_invalid_meta(VentilatorAsset, meta) + + def test_meta_validations_for_hl7monitor_asset(self): + valid_meta = { + "id": "123", + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + } + hl7monitor_asset = HL7MonitorAsset(valid_meta) + self.assertEqual(hl7monitor_asset.middleware_hostname, "middleware.local") + self.assertEqual(hl7monitor_asset.host, "192.168.0.1") + self.assertEqual(hl7monitor_asset.id, "123") + self.assertTrue(hl7monitor_asset.insecure_connection) + + invalid_meta_cases = [ + # Missing id + { + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + }, + # Missing middleware_hostname + {"id": "123", "local_ip_address": "192.168.0.1"}, + # Missing local_ip_address + {"id": "123", "middleware_hostname": "middleware.local"}, + # Invalid insecure_connection + { + "id": "123", + "local_ip_address": "192.168.0.1", + "middleware_hostname": "middleware.local", + "insecure_connection": "invalid_value", + }, + # Camera access key not required for HL7Monitor, invalid meta + { + "id": "123", + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + }, + ] + for meta in invalid_meta_cases: + self.validate_invalid_meta(HL7MonitorAsset, meta) + def test_list_assets(self): response = self.client.get("/api/v1/asset/") self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/care/utils/assetintegration/base.py b/care/utils/assetintegration/base.py index 334bcecfa5..cc6c59e1c4 100644 --- a/care/utils/assetintegration/base.py +++ b/care/utils/assetintegration/base.py @@ -1,17 +1,35 @@ import json +from typing import TypedDict +import jsonschema import requests from django.conf import settings +from jsonschema import ValidationError as JSONValidationError from rest_framework import status -from rest_framework.exceptions import APIException +from rest_framework.exceptions import APIException, ValidationError from care.utils.jwks.token_generator import generate_jwt +from .schema import meta_object_schema + + +class ActionParams(TypedDict, total=False): + type: str + data: dict | None + timeout: int | None + class BaseAssetIntegration: auth_header_type = "Care_Bearer " def __init__(self, meta): + try: + meta["_name"] = self._name + jsonschema.validate(instance=meta, schema=meta_object_schema) + except JSONValidationError as e: + error_message = f"Invalid metadata: {e.message}" + raise ValidationError(error_message) from e + self.meta = meta self.id = self.meta.get("id", "") self.host = self.meta["local_ip_address"] @@ -19,8 +37,8 @@ def __init__(self, meta): self.insecure_connection = self.meta.get("insecure_connection", False) self.timeout = settings.MIDDLEWARE_REQUEST_TIMEOUT - def handle_action(self, action): - pass + def handle_action(self, **kwargs): + """Handle actions using kwargs instead of dict.""" def get_url(self, endpoint): protocol = "http" @@ -48,16 +66,14 @@ def _validate_response(self, response: requests.Response): {"error": "Invalid Response"}, response.status_code ) from e - def api_post(self, url, data=None): + def api_post(self, url, data=None, timeout=None): + timeout = timeout or self.timeout return self._validate_response( - requests.post( - url, json=data, headers=self.get_headers(), timeout=self.timeout - ) + requests.post(url, json=data, headers=self.get_headers(), timeout=timeout) ) - def api_get(self, url, data=None): + def api_get(self, url, data=None, timeout=None): + timeout = timeout or self.timeout return self._validate_response( - requests.get( - url, params=data, headers=self.get_headers(), timeout=self.timeout - ) + requests.get(url, params=data, headers=self.get_headers(), timeout=timeout) ) diff --git a/care/utils/assetintegration/hl7monitor.py b/care/utils/assetintegration/hl7monitor.py index abd14247d3..bf331f71ca 100644 --- a/care/utils/assetintegration/hl7monitor.py +++ b/care/utils/assetintegration/hl7monitor.py @@ -2,7 +2,7 @@ from rest_framework.exceptions import ValidationError -from care.utils.assetintegration.base import BaseAssetIntegration +from care.utils.assetintegration.base import ActionParams, BaseAssetIntegration class HL7MonitorAsset(BaseAssetIntegration): @@ -20,12 +20,13 @@ def __init__(self, meta): {key: f"{key} not found in asset metadata" for key in e.args} ) from e - def handle_action(self, action): - action_type = action["type"] + def handle_action(self, **kwargs: ActionParams): + action_type = kwargs["type"] + timeout = kwargs.get("timeout") if action_type == self.HL7MonitorActions.GET_VITALS.value: request_params = {"device_id": self.host} - return self.api_get(self.get_url("vitals"), request_params) + return self.api_get(self.get_url("vitals"), request_params, timeout) if action_type == self.HL7MonitorActions.GET_STREAM_TOKEN.value: return self.api_post( @@ -34,6 +35,7 @@ def handle_action(self, action): "asset_id": self.id, "ip": self.host, }, + timeout, ) raise ValidationError({"action": "invalid action type"}) diff --git a/care/utils/assetintegration/onvif.py b/care/utils/assetintegration/onvif.py index 815994855e..2dd814b4e6 100644 --- a/care/utils/assetintegration/onvif.py +++ b/care/utils/assetintegration/onvif.py @@ -2,7 +2,7 @@ from rest_framework.exceptions import ValidationError -from care.utils.assetintegration.base import BaseAssetIntegration +from care.utils.assetintegration.base import ActionParams, BaseAssetIntegration class OnvifAsset(BaseAssetIntegration): @@ -27,9 +27,10 @@ def __init__(self, meta): {key: f"{key} not found in asset metadata" for key in e.args} ) from e - def handle_action(self, action): - action_type = action["type"] - action_data = action.get("data", {}) + def handle_action(self, **kwargs: ActionParams): + action_type = kwargs["type"] + action_data = kwargs.get("data", {}) + timeout = kwargs.get("timeout") request_body = { "hostname": self.host, @@ -41,19 +42,19 @@ def handle_action(self, action): } if action_type == self.OnvifActions.GET_CAMERA_STATUS.value: - return self.api_get(self.get_url("status"), request_body) + return self.api_get(self.get_url("status"), request_body, timeout) if action_type == self.OnvifActions.GET_PRESETS.value: - return self.api_get(self.get_url("presets"), request_body) + return self.api_get(self.get_url("presets"), request_body, timeout) if action_type == self.OnvifActions.GOTO_PRESET.value: - return self.api_post(self.get_url("gotoPreset"), request_body) + return self.api_post(self.get_url("gotoPreset"), request_body, timeout) if action_type == self.OnvifActions.ABSOLUTE_MOVE.value: - return self.api_post(self.get_url("absoluteMove"), request_body) + return self.api_post(self.get_url("absoluteMove"), request_body, timeout) if action_type == self.OnvifActions.RELATIVE_MOVE.value: - return self.api_post(self.get_url("relativeMove"), request_body) + return self.api_post(self.get_url("relativeMove"), request_body, timeout) if action_type == self.OnvifActions.GET_STREAM_TOKEN.value: return self.api_post( @@ -61,6 +62,7 @@ def handle_action(self, action): { "stream_id": self.access_key, }, + timeout, ) raise ValidationError({"action": "invalid action type"}) diff --git a/care/utils/assetintegration/schema.py b/care/utils/assetintegration/schema.py new file mode 100644 index 0000000000..3396747162 --- /dev/null +++ b/care/utils/assetintegration/schema.py @@ -0,0 +1,34 @@ +meta_object_schema = { + "type": "object", + "properties": { + "id": {"type": "string"}, + "local_ip_address": {"type": "string", "format": "ipv4"}, + "middleware_hostname": {"type": "string"}, + "insecure_connection": {"type": "boolean", "default": False}, + "camera_access_key": { + "type": "string", + "pattern": "^[^:]+:[^:]+:[^:]+$", # valid pattern for "abc:def:ghi" , "123:456:789" + }, + }, + "required": ["local_ip_address", "middleware_hostname"], + "allOf": [ + { + "if": {"properties": {"_name": {"const": "onvif"}}}, + "then": { + "properties": {"camera_access_key": {"type": "string"}}, + "required": [ + "camera_access_key" + ], # Require camera_access_key for Onvif + }, + "else": { + "properties": {"id": {"type": "string"}}, + "required": ["id"], # Require id for non-Onvif assets + "not": { + "required": [ + "camera_access_key" + ] # Make camera_access_key not required for non-Onvif + }, + }, + } + ], +} diff --git a/care/utils/assetintegration/ventilator.py b/care/utils/assetintegration/ventilator.py index 23a5280960..afb896bfdb 100644 --- a/care/utils/assetintegration/ventilator.py +++ b/care/utils/assetintegration/ventilator.py @@ -2,7 +2,7 @@ from rest_framework.exceptions import ValidationError -from care.utils.assetintegration.base import BaseAssetIntegration +from care.utils.assetintegration.base import ActionParams, BaseAssetIntegration class VentilatorAsset(BaseAssetIntegration): @@ -20,12 +20,13 @@ def __init__(self, meta): {key: f"{key} not found in asset metadata" for key in e.args} ) from e - def handle_action(self, action): - action_type = action["type"] + def handle_action(self, **kwargs: ActionParams): + action_type = kwargs["type"] + timeout = kwargs.get("timeout") if action_type == self.VentilatorActions.GET_VITALS.value: request_params = {"device_id": self.host} - return self.api_get(self.get_url("vitals"), request_params) + return self.api_get(self.get_url("vitals"), request_params, timeout) if action_type == self.VentilatorActions.GET_STREAM_TOKEN.value: return self.api_post( @@ -34,6 +35,7 @@ def handle_action(self, action): "asset_id": self.id, "ip": self.host, }, + timeout, ) raise ValidationError({"action": "invalid action type"}) From 7986fdd3696e57378566120b92f1eb3b267a81b5 Mon Sep 17 00:00:00 2001 From: "qodana-cloud[bot]" <163413896+qodana-cloud[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:08:30 +0530 Subject: [PATCH 04/19] Add qodana CI checks (#2607) * Add qodana.yaml file * Add github workflow file * Update qodana_code_quality.yml --------- Co-authored-by: Qodana Application Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- .github/workflows/qodana_code_quality.yml | 27 +++++++++++++++++++++++ qodana.yaml | 6 +++++ 2 files changed, 33 insertions(+) create mode 100644 .github/workflows/qodana_code_quality.yml create mode 100644 qodana.yaml diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml new file mode 100644 index 0000000000..66640a50c9 --- /dev/null +++ b/.github/workflows/qodana_code_quality.yml @@ -0,0 +1,27 @@ +name: Qodana +on: + workflow_dispatch: + pull_request: + push: + branches: # Specify your branches here + - develop + +jobs: + qodana: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + checks: write + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit + fetch-depth: 0 # a full history is required for pull request analysis + - name: 'Qodana Scan' + uses: JetBrains/qodana-action@v2024.2 + with: + pr-mode: false + env: + QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_1210838162 }} + QODANA_ENDPOINT: 'https://qodana.cloud' diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000000..c2af26bad6 --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,6 @@ +version: "1.0" +linter: jetbrains/qodana-python:2024.2 +profile: + name: qodana.recommended +include: + - name: CheckDependencyLicenses \ No newline at end of file From 7114ba88a506cc7cce20b14b632b8e7e5e7ae209 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Wed, 20 Nov 2024 14:17:08 +0530 Subject: [PATCH 05/19] Fix linting issue --- qodana.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qodana.yaml b/qodana.yaml index c2af26bad6..368475bc3e 100644 --- a/qodana.yaml +++ b/qodana.yaml @@ -3,4 +3,4 @@ linter: jetbrains/qodana-python:2024.2 profile: name: qodana.recommended include: - - name: CheckDependencyLicenses \ No newline at end of file + - name: CheckDependencyLicenses From 734c3e068602ca50bfdca0581ced7db3ff461299 Mon Sep 17 00:00:00 2001 From: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:48:10 +0530 Subject: [PATCH 06/19] Modified the Patient and users names in dummy data (#2608) modified the users and patient names --- data/dummy/facility.json | 34 +++++++++++++++++----------------- data/dummy/users.json | 6 +++--- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/data/dummy/facility.json b/data/dummy/facility.json index 17d98574ff..c6875988db 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -4164,7 +4164,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient", + "name": "Dummy Patient One", "gender": 1, "phone_number": "+919987455444", "emergency_phone_number": "+919898797775", @@ -4247,7 +4247,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Test E2E User", + "name": "Dummy Patient Two", "gender": 1, "phone_number": "+919765259927", "emergency_phone_number": "+919228973557", @@ -4330,7 +4330,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 1", + "name": "Dummy Patient Three", "gender": 1, "phone_number": "+919192495353", "emergency_phone_number": "+919460491040", @@ -4413,7 +4413,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 2", + "name": "Dummy Patient Four", "gender": 1, "phone_number": "+919112608904", "emergency_phone_number": "+919110616234", @@ -4496,7 +4496,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 3", + "name": "Dummy Patient Five", "gender": 1, "phone_number": "+919640229897", "emergency_phone_number": "+919135436547", @@ -4579,7 +4579,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 4", + "name": "Dummy Patient Six", "gender": 1, "phone_number": "+919762277015", "emergency_phone_number": "+919342634016", @@ -4662,7 +4662,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 5", + "name": "Dummy Patient Seven", "gender": 1, "phone_number": "+919303212282", "emergency_phone_number": "+919229738916", @@ -4745,7 +4745,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 6", + "name": "Dummy Patient Eight", "gender": 1, "phone_number": "+919740701377", "emergency_phone_number": "+919321666516", @@ -4828,7 +4828,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 7", + "name": "Dummy Patient Nine", "gender": 1, "phone_number": "+919148299129", "emergency_phone_number": "+919267280161", @@ -4911,7 +4911,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 8", + "name": "Dummy Patient Ten", "gender": 1, "phone_number": "+919490490290", "emergency_phone_number": "+919828674710", @@ -4994,7 +4994,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 9", + "name": "Dummy Patient Eleven", "gender": 1, "phone_number": "+919983927490", "emergency_phone_number": "+919781111140", @@ -5160,7 +5160,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 11", + "name": "Dummy Patient Twelve", "gender": 1, "phone_number": "+919343556704", "emergency_phone_number": "+919967920474", @@ -5243,7 +5243,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 12", + "name": "Dummy Patient Thirteen", "gender": 1, "phone_number": "+919320374643", "emergency_phone_number": "+919493558024", @@ -5326,7 +5326,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 13", + "name": "Discharge Patient One", "gender": 1, "phone_number": "+919292990239", "emergency_phone_number": "+919992258784", @@ -5409,7 +5409,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 14", + "name": "Discharge Patient Two", "gender": 1, "phone_number": "+919650206292", "emergency_phone_number": "+919596454242", @@ -5492,7 +5492,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 15", + "name": "Discharge Patient Three", "gender": 1, "phone_number": "+919266236581", "emergency_phone_number": "+919835286558", @@ -5575,7 +5575,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 16", + "name": "Discharge Patient Four", "gender": 1, "phone_number": "+919243083817", "emergency_phone_number": "+919924971004", diff --git a/data/dummy/users.json b/data/dummy/users.json index e7b0115614..b86ae90ac4 100644 --- a/data/dummy/users.json +++ b/data/dummy/users.json @@ -894,14 +894,14 @@ "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", + "first_name": "Tester", + "last_name": "Doctor", "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", + "username": "dev-doctor2", "user_type": 15, "created_by": 2, "ward": null, From e90133344e13e21f8dec2c9f99cb052414c4bdc5 Mon Sep 17 00:00:00 2001 From: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Date: Wed, 20 Nov 2024 17:43:48 +0530 Subject: [PATCH 07/19] Modified Missed Patient Name in dummy data (#2609) * modified the users and patient names * missed out one patient name * missed out one patient name --- data/dummy/facility.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/dummy/facility.json b/data/dummy/facility.json index c6875988db..da4b726469 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -5077,7 +5077,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient 10", + "name": "Dummy Patient Twelve", "gender": 1, "phone_number": "+919849511866", "emergency_phone_number": "+919622326248", @@ -5160,7 +5160,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient Twelve", + "name": "Dummy Patient Thirteen", "gender": 1, "phone_number": "+919343556704", "emergency_phone_number": "+919967920474", @@ -5243,7 +5243,7 @@ "facility": 1, "nearest_facility": null, "meta_info": null, - "name": "Dummy Patient Thirteen", + "name": "Dummy Patient Fourteen", "gender": 1, "phone_number": "+919320374643", "emergency_phone_number": "+919493558024", From 2970fe1f4a95a300fedd1df427a48a080fe2d56f Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 21 Nov 2024 19:40:49 +0530 Subject: [PATCH 08/19] fix asset class initialization (#2611) --- care/facility/api/viewsets/asset.py | 2 +- care/facility/tasks/asset_monitor.py | 2 +- care/facility/tests/test_asset_api.py | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index caf784cb9f..2563dfaf45 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -401,7 +401,7 @@ def operate_assets(self, request, *args, **kwargs): asset_class: BaseAssetIntegration = AssetClasses[asset.asset_class].value( { **asset.meta, - "id": asset.external_id, + "id": str(asset.external_id), "middleware_hostname": middleware_hostname, } ) diff --git a/care/facility/tasks/asset_monitor.py b/care/facility/tasks/asset_monitor.py index 9c8701618c..1adfa725ae 100644 --- a/care/facility/tasks/asset_monitor.py +++ b/care/facility/tasks/asset_monitor.py @@ -65,7 +65,7 @@ def check_asset_status(): # noqa: PLR0912 ].value( { **asset.meta, - "id": asset.external_id, + "id": str(asset.external_id), "middleware_hostname": resolved_middleware, } ) diff --git a/care/facility/tests/test_asset_api.py b/care/facility/tests/test_asset_api.py index 3989a19eef..0e96498d4b 100644 --- a/care/facility/tests/test_asset_api.py +++ b/care/facility/tests/test_asset_api.py @@ -6,6 +6,7 @@ from care.facility.models import Asset, Bed from care.users.models import User from care.utils.assetintegration.asset_classes import AssetClasses +from care.utils.assetintegration.base import BaseAssetIntegration from care.utils.assetintegration.hl7monitor import HL7MonitorAsset from care.utils.assetintegration.onvif import OnvifAsset from care.utils.assetintegration.ventilator import VentilatorAsset @@ -39,6 +40,26 @@ def validate_invalid_meta(self, asset_class, meta): with self.assertRaises(ValidationError): asset_class(meta) + def test_asset_class_initialization(self): + asset = self.create_asset( + self.asset_location, + asset_class=AssetClasses.ONVIF.name, + meta={ + "local_ip_address": "192.168.0.1", + "camera_access_key": "username:password:access_key", + "middleware_hostname": "middleware.local", + "insecure_connection": True, + }, + ) + asset_class = AssetClasses[asset.asset_class].value( + { + **asset.meta, + "id": str(asset.external_id), + "middleware_hostname": "middleware.local", + } + ) + self.assertIsInstance(asset_class, BaseAssetIntegration) + def test_meta_validations_for_onvif_asset(self): valid_meta = { "local_ip_address": "192.168.0.1", From d23cbcb8461c9e5c545eca045597a201fa252cf6 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 21 Nov 2024 20:02:33 +0530 Subject: [PATCH 09/19] Revert "Add qodana CI checks (#2607)" (#2612) This reverts commit 7986fdd3696e57378566120b92f1eb3b267a81b5. --- .github/workflows/qodana_code_quality.yml | 27 ----------------------- qodana.yaml | 6 ----- 2 files changed, 33 deletions(-) delete mode 100644 .github/workflows/qodana_code_quality.yml delete mode 100644 qodana.yaml diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml deleted file mode 100644 index 66640a50c9..0000000000 --- a/.github/workflows/qodana_code_quality.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Qodana -on: - workflow_dispatch: - pull_request: - push: - branches: # Specify your branches here - - develop - -jobs: - qodana: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - checks: write - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit - fetch-depth: 0 # a full history is required for pull request analysis - - name: 'Qodana Scan' - uses: JetBrains/qodana-action@v2024.2 - with: - pr-mode: false - env: - QODANA_TOKEN: ${{ secrets.QODANA_TOKEN_1210838162 }} - QODANA_ENDPOINT: 'https://qodana.cloud' diff --git a/qodana.yaml b/qodana.yaml deleted file mode 100644 index 368475bc3e..0000000000 --- a/qodana.yaml +++ /dev/null @@ -1,6 +0,0 @@ -version: "1.0" -linter: jetbrains/qodana-python:2024.2 -profile: - name: qodana.recommended -include: - - name: CheckDependencyLicenses From 7454ab93e23a0da9366a018c1048b0eb670883b6 Mon Sep 17 00:00:00 2001 From: Anvesh Nalimela <151531961+AnveshNalimela@users.noreply.github.com> Date: Mon, 25 Nov 2024 20:22:05 +0530 Subject: [PATCH 10/19] Added CSV support To API listFacilityDischargedPatients (#2601) Added CSV support To API listFacilityDischargedPatients (#2601) Co-authored-by: Aakash Singh --- care/facility/api/viewsets/patient.py | 73 +-------------- care/utils/exports/__init__.py | 0 care/utils/exports/mixins.py | 129 ++++++++++++++++++++++++++ pyproject.toml | 1 + 4 files changed, 135 insertions(+), 68 deletions(-) create mode 100644 care/utils/exports/__init__.py create mode 100644 care/utils/exports/mixins.py diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 963b6d4731..585d13cdee 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -19,7 +19,6 @@ from django.db.models.query import QuerySet from django.utils import timezone from django_filters import rest_framework as filters -from djqscsv import render_to_csv_response from drf_spectacular.utils import extend_schema, extend_schema_view from dry_rest_permissions.generics import DRYPermissionFiltersBase, DRYPermissions from rest_framework import filters as rest_framework_filters @@ -80,6 +79,7 @@ from care.facility.models.patient_consultation import PatientConsultation from care.users.models import User from care.utils.cache.cache_allowed_facilities import get_accessible_facilities +from care.utils.exports.mixins import CSVExportViewSetMixin from care.utils.filters.choicefilter import CareChoiceFilter from care.utils.filters.multiselect import MultiSelectFilter from care.utils.notification_handler import NotificationGenerator @@ -376,6 +376,7 @@ def filter_queryset(self, request, queryset, view): @extend_schema_view(history=extend_schema(tags=["patient"])) class PatientViewSet( + CSVExportViewSetMixin, HistoryMixin, mixins.CreateModelMixin, mixins.ListModelMixin, @@ -475,7 +476,6 @@ class PatientViewSet( "last_consultation_encounter_date", "last_consultation_discharge_date", ] - CSV_EXPORT_LIMIT = 7 def get_queryset(self): queryset = super().get_queryset().order_by("modified_date") @@ -520,71 +520,6 @@ def filter_queryset(self, queryset: QuerySet) -> QuerySet: return super().filter_queryset(queryset) - def list(self, request, *args, **kwargs): - """ - Patient List - - `without_facility` accepts boolean - default is false - - if true: shows only patients without a facility mapped - if false (default behaviour): shows only patients with a facility mapped - - `disease_status` accepts - string and int - - SUSPECTED = 1 - POSITIVE = 2 - NEGATIVE = 3 - RECOVERY = 4 - RECOVERED = 5 - EXPIRED = 6 - - """ - if settings.CSV_REQUEST_PARAMETER in request.GET: - # Start Date Validation - temp = filters.DjangoFilterBackend().get_filterset( - self.request, self.queryset, self - ) - temp.is_valid() - within_limits = False - for field in self.date_range_fields: - slice_obj = temp.form.cleaned_data.get(field) - if slice_obj: - if not slice_obj.start or not slice_obj.stop: - raise ValidationError( - { - field: "both starting and ending date must be provided for export" - } - ) - days_difference = ( - temp.form.cleaned_data.get(field).stop - - temp.form.cleaned_data.get(field).start - ).days - if days_difference <= self.CSV_EXPORT_LIMIT: - within_limits = True - else: - raise ValidationError( - { - field: f"Cannot export more than {self.CSV_EXPORT_LIMIT} days at a time" - } - ) - if not within_limits: - raise ValidationError( - { - "date": f"Atleast one date field must be filtered to be within {self.CSV_EXPORT_LIMIT} days" - } - ) - # End Date Limiting Validation - queryset = ( - self.filter_queryset(self.get_queryset()) - .annotate(**PatientRegistration.CSV_ANNOTATE_FIELDS) - .values(*PatientRegistration.CSV_MAPPING.keys()) - ) - return render_to_csv_response( - queryset, - field_header_map=PatientRegistration.CSV_MAPPING, - field_serializer_map=PatientRegistration.CSV_MAKE_PRETTY, - ) - - return super().list(request, *args, **kwargs) - @extend_schema(tags=["patient"]) @action(detail=True, methods=["POST"]) def transfer(self, request, *args, **kwargs): @@ -678,7 +613,9 @@ def filter_by_bed_type(self, queryset, name, value): @extend_schema_view(tags=["patient"]) -class FacilityDischargedPatientViewSet(GenericViewSet, mixins.ListModelMixin): +class FacilityDischargedPatientViewSet( + CSVExportViewSetMixin, GenericViewSet, mixins.ListModelMixin +): permission_classes = (IsAuthenticated, DRYPermissions) lookup_field = "external_id" serializer_class = PatientListSerializer diff --git a/care/utils/exports/__init__.py b/care/utils/exports/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/utils/exports/mixins.py b/care/utils/exports/mixins.py new file mode 100644 index 0000000000..4e80310dbe --- /dev/null +++ b/care/utils/exports/mixins.py @@ -0,0 +1,129 @@ +from django.conf import settings +from django.db import models +from django_filters import rest_framework as filters +from djqscsv import render_to_csv_response +from rest_framework.exceptions import ValidationError + + +class CSVExportViewSetMixin: + """Mixin that adds CSV export functionality to a viewset""" + + csv_export_limit = 7 + date_range_fields = [] + + def get_model(self): + """Get model class from viewset's queryset or model attribute""" + if hasattr(self, "queryset"): + return self.queryset.model + if hasattr(self, "model"): + return self.model + msg = ( + "Cannot determine model class from viewset, set model or queryset attribute" + ) + raise ValueError(msg) + + def get_date_range_fields(self): + """Get date range fields from model and filterset""" + if self.date_range_fields: + return self.date_range_fields + + model = self.get_model() + date_fields = [] + + # Get fields from model that are DateField/DateTimeField + for field in model._meta.fields: # noqa: SLF001 + if isinstance(field, (models.DateField, models.DateTimeField)): + date_fields.append(field.name) + + # Get date range fields from filterset if defined + if hasattr(self, "filterset_class"): + for name, field in self.filterset_class.declared_filters.items(): + if isinstance(field, filters.DateFromToRangeFilter): + date_fields.append(name) + + return list(set(date_fields)) + + def get_csv_settings(self): + """Get CSV export configuration from model""" + model = self.get_model() + + # Try to get settings from model + annotations = getattr(model, "CSV_ANNOTATE_FIELDS", {}) + field_mapping = getattr(model, "CSV_MAPPING", {}) + field_serializers = getattr(model, "CSV_MAKE_PRETTY", {}) + + if not field_mapping: + # Auto-generate field mapping from model fields + field_mapping = {f.name: f.verbose_name.title() for f in model._meta.fields} # noqa: SLF001 + + fields = list(field_mapping.keys()) + + return { + "annotations": annotations, + "field_mapping": field_mapping, + "field_serializers": field_serializers, + "fields": fields, + } + + def validate_date_ranges(self, request): + """Validates that at least one date range filter is within limits""" + filterset = filters.DjangoFilterBackend().get_filterset( + request, self.queryset, self + ) + if not filterset.is_valid(): + raise ValidationError(filterset.errors) + + within_limits = False + for field in self.get_date_range_fields(): + slice_obj = filterset.form.cleaned_data.get(field) + if slice_obj: + if not slice_obj.start or not slice_obj.stop: + raise ValidationError( + { + field: "both starting and ending date must be provided for export" + } + ) + + days_difference = ( + filterset.form.cleaned_data.get(field).stop + - filterset.form.cleaned_data.get(field).start + ).days + + if days_difference <= self.csv_export_limit: + within_limits = True + else: + raise ValidationError( + { + field: f"Cannot export more than {self.csv_export_limit} days at a time" + } + ) + + if not within_limits: + raise ValidationError( + { + "date": f"At least one date field must be filtered to be within {self.csv_export_limit} days" + } + ) + + def export_as_csv(self, request): + """Exports queryset as CSV""" + self.validate_date_ranges(request) + + csv_settings = self.get_csv_settings() + queryset = self.filter_queryset(self.get_queryset()) + + if csv_settings["annotations"]: + queryset = queryset.annotate(**csv_settings["annotations"]) + + queryset = queryset.values(*csv_settings["fields"]) + + return render_to_csv_response( + queryset, + field_header_map=csv_settings["field_mapping"], + field_serializer_map=csv_settings["field_serializers"], + ) + + def list(self, request, *args, **kwargs): + if settings.CSV_REQUEST_PARAMETER in request.GET: + return self.export_as_csv(request) + return super().list(request, *args, **kwargs) diff --git a/pyproject.toml b/pyproject.toml index 93d676ccd2..6a7a5c5089 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ ignore = [ "FBT001", # why not! "S106", "S105", + "UP038" # this results in slower code ] From d3d402db6cc20df70cc32c68723528a5446d6819 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Mon, 25 Nov 2024 20:24:34 +0530 Subject: [PATCH 11/19] Add workflow steps to upload dummy db artifact for tests (#2616) Add workflow steps to upload dummy db artifact for tests (#2616) --- .github/workflows/reusable-test.yml | 16 +++++++++++++++- .gitignore | 2 ++ Makefile | 19 ++++++++++++++++--- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/.github/workflows/reusable-test.yml b/.github/workflows/reusable-test.yml index 27d5301ce7..8ecc633d01 100644 --- a/.github/workflows/reusable-test.yml +++ b/.github/workflows/reusable-test.yml @@ -38,7 +38,7 @@ jobs: *.cache-to=type=local,dest=/tmp/.buildx-cache-new files: docker-compose.yaml,docker-compose.local.yaml env: - DOCKER_BUILD_NO_SUMMARY: true + DOCKER_BUILD_SUMMARY: false - name: Start services run: | @@ -57,6 +57,10 @@ jobs: - name: Validate integrity of fixtures run: make load-dummy-data + - name: Dump db + if: ${{ inputs.event_name == 'push' || github.event_name == 'push' }} + run: make dump-db + - name: Run tests run: make test-coverage @@ -76,3 +80,13 @@ jobs: with: path: /tmp/.buildx-cache key: ${{ runner.os }}-buildx-${{ hashFiles('Pipfile.lock', 'docker/dev.Dockerfile') }} + + - name: Upload db artifact + if: ${{ inputs.event_name == 'push' || github.event_name == 'push' }} + uses: actions/upload-artifact@v4 + with: + name: care-db-dump + path: care_db.dump + retention-days: 30 + compression-level: 0 # file is already compressed + overwrite: true # keep only the last artifact diff --git a/.gitignore b/.gitignore index 162fc2bcba..84a84d610f 100644 --- a/.gitignore +++ b/.gitignore @@ -354,3 +354,5 @@ secrets.sh *.rdb jwks.b64.txt + +care_db.dump diff --git a/Makefile b/Makefile index aa4ff85e2a..12d25fc83b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build, re-build, up, down, list, logs, test, makemigrations, reset_db +.PHONY: logs DOCKER_VERSION := $(shell docker --version 2>/dev/null) @@ -19,6 +19,9 @@ re-build: build: docker compose -f docker-compose.yaml -f $(docker_config_file) build +pull: + docker compose -f docker-compose.yaml -f $(docker_config_file) pull + up: docker compose -f docker-compose.yaml -f $(docker_config_file) up -d --wait @@ -40,6 +43,9 @@ checkmigration: makemigrations: docker compose exec backend bash -c "python manage.py makemigrations" +migrate: + docker compose exec backend bash -c "python manage.py migrate" + test: docker compose exec backend bash -c "python manage.py test --keepdb --parallel --shuffle" @@ -48,9 +54,16 @@ test-coverage: docker compose exec backend bash -c "coverage combine || true; coverage xml" docker compose cp backend:/app/coverage.xml coverage.xml -reset_db: +dump-db: + docker compose exec db sh -c "pg_dump -U postgres -Fc care > /tmp/care_db.dump" + docker compose cp db:/tmp/care_db.dump care_db.dump + +load-db: + docker compose cp care_db.dump db:/tmp/care_db.dump + docker compose exec db sh -c "pg_restore -U postgres --clean --if-exists -d care /tmp/care_db.dump" + +reset-db: docker compose exec backend bash -c "python manage.py reset_db --noinput" - docker compose exec backend bash -c "python manage.py migrate" ruff-all: ruff check . From 43e22ca9320261e23d4a46f1fbd94dcf57a98bde Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Mon, 25 Nov 2024 20:25:07 +0530 Subject: [PATCH 12/19] Remove event handlers (#2593) Remove event handlers (#2593) --- care/facility/api/serializers/daily_round.py | 20 +- .../api/serializers/encounter_symptom.py | 30 +- .../api/serializers/patient_consultation.py | 30 +- .../api/viewsets/consultation_diagnosis.py | 35 +- care/facility/api/viewsets/patient.py | 8 - care/facility/events/__init__.py | 0 care/facility/events/handler.py | 116 ------- .../management/commands/load_event_types.py | 302 ------------------ care/utils/event_utils.py | 54 ---- 9 files changed, 6 insertions(+), 589 deletions(-) delete mode 100644 care/facility/events/__init__.py delete mode 100644 care/facility/events/handler.py delete mode 100644 care/facility/management/commands/load_event_types.py diff --git a/care/facility/api/serializers/daily_round.py b/care/facility/api/serializers/daily_round.py index 768ddbabfd..bd8f105903 100644 --- a/care/facility/api/serializers/daily_round.py +++ b/care/facility/api/serializers/daily_round.py @@ -7,7 +7,6 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError -from care.facility.events.handler import create_consultation_events from care.facility.models import ( CATEGORY_CHOICES, COVID_CATEGORY_CHOICES, @@ -167,17 +166,7 @@ def update(self, instance, validated_data): facility=instance.consultation.patient.facility, ).generate() - instance = super().update(instance, validated_data) - - create_consultation_events( - instance.consultation_id, - instance, - instance.created_by_id, - instance.created_date, - fields_to_store=set(validated_data.keys()), - ) - - return instance + return super().update(instance, validated_data) def update_last_daily_round(self, daily_round_obj): consultation = daily_round_obj.consultation @@ -279,13 +268,6 @@ def create(self, validated_data): if daily_round_obj.rounds_type != DailyRound.RoundsType.AUTOMATED.value: self.update_last_daily_round(daily_round_obj) - create_consultation_events( - daily_round_obj.consultation_id, - daily_round_obj, - daily_round_obj.created_by_id, - daily_round_obj.created_date, - taken_at=daily_round_obj.taken_at, - ) return daily_round_obj def validate(self, attrs): diff --git a/care/facility/api/serializers/encounter_symptom.py b/care/facility/api/serializers/encounter_symptom.py index d669dd0aab..f0f04729e5 100644 --- a/care/facility/api/serializers/encounter_symptom.py +++ b/care/facility/api/serializers/encounter_symptom.py @@ -1,10 +1,6 @@ -from copy import copy - -from django.db import transaction from django.utils.timezone import now from rest_framework import serializers -from care.facility.events.handler import create_consultation_events from care.facility.models.encounter_symptom import ( ClinicalImpressionStatus, EncounterSymptom, @@ -90,34 +86,12 @@ def create(self, validated_data): validated_data["consultation"] = self.context["consultation"] validated_data["created_by"] = self.context["request"].user - with transaction.atomic(): - instance: EncounterSymptom = super().create(validated_data) - - create_consultation_events( - instance.consultation_id, - instance, - instance.created_by_id, - instance.created_date, - ) - - return instance + return super().create(validated_data) def update(self, instance, validated_data): validated_data["updated_by"] = self.context["request"].user - with transaction.atomic(): - old_instance = copy(instance) - instance = super().update(instance, validated_data) - - create_consultation_events( - instance.consultation_id, - instance, - instance.updated_by_id, - instance.modified_date, - old=old_instance, - ) - - return instance + return super().update(instance, validated_data) class EncounterCreateSymptomSerializer(serializers.ModelSerializer): diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index 25405b6f7e..ac2fdf5d00 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -1,4 +1,3 @@ -from copy import copy from datetime import timedelta from django.conf import settings @@ -24,7 +23,6 @@ EncounterSymptomSerializer, ) from care.facility.api.serializers.facility import FacilityBasicInfoSerializer -from care.facility.events.handler import create_consultation_events from care.facility.models import ( CATEGORY_CHOICES, COVID_CATEGORY_CHOICES, @@ -219,7 +217,6 @@ def validate_bed_number(self, bed_number): return bed_number def update(self, instance, validated_data): - old_instance = copy(instance) instance.last_edited_by = self.context["request"].user if instance.discharge_date: @@ -269,14 +266,6 @@ def update(self, instance, validated_data): consultation = super().update(instance, validated_data) - create_consultation_events( - consultation.id, - consultation, - self.context["request"].user.id, - consultation.modified_date, - old=old_instance, - ) - if ( "assigned_to" in validated_data and validated_data["assigned_to"] != _temp @@ -415,7 +404,7 @@ def create(self, validated_data): # noqa: PLR0915 PLR0912 ): consultation.is_readmission = True - diagnosis = ConsultationDiagnosis.objects.bulk_create( + ConsultationDiagnosis.objects.bulk_create( [ ConsultationDiagnosis( consultation=consultation, @@ -428,7 +417,7 @@ def create(self, validated_data): # noqa: PLR0915 PLR0912 ] ) - symptoms = EncounterSymptom.objects.bulk_create( + EncounterSymptom.objects.bulk_create( EncounterSymptom( consultation=consultation, symptom=obj.get("symptom"), @@ -469,13 +458,6 @@ def create(self, validated_data): # noqa: PLR0915 PLR0912 consultation.save() patient.save() - create_consultation_events( - consultation.id, - (consultation, *diagnosis, *symptoms), - consultation.created_by.id, - consultation.created_date, - ) - NotificationGenerator( event=Notification.Event.PATIENT_CONSULTATION_CREATED, caused_by=user, @@ -793,7 +775,6 @@ def validate(self, attrs): return attrs def update(self, instance: PatientConsultation, validated_data): - old_instance = copy(instance) with transaction.atomic(): instance = super().update(instance, validated_data) patient: PatientRegistration = instance.patient @@ -804,13 +785,6 @@ def update(self, instance: PatientConsultation, validated_data): ConsultationBed.objects.filter( consultation=self.instance, end_date__isnull=True ).update(end_date=now()) - create_consultation_events( - instance.id, - instance, - self.context["request"].user.id, - instance.modified_date, - old=old_instance, - ) return instance diff --git a/care/facility/api/viewsets/consultation_diagnosis.py b/care/facility/api/viewsets/consultation_diagnosis.py index cc76358be6..73d218bc3e 100644 --- a/care/facility/api/viewsets/consultation_diagnosis.py +++ b/care/facility/api/viewsets/consultation_diagnosis.py @@ -1,17 +1,13 @@ -from copy import copy - from django.shortcuts import get_object_or_404 from django_filters import rest_framework as filters from dry_rest_permissions.generics import DRYPermissions from rest_framework import mixins from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet from care.facility.api.serializers.consultation_diagnosis import ( ConsultationDiagnosisSerializer, ) -from care.facility.events.handler import create_consultation_events from care.facility.models import ( ConditionVerificationStatus, ConsultationDiagnosis, @@ -56,33 +52,4 @@ def get_queryset(self): def perform_create(self, serializer): consultation = self.get_consultation_obj() - diagnosis = serializer.save( - consultation=consultation, created_by=self.request.user - ) - create_consultation_events( - consultation.id, - diagnosis, - caused_by=self.request.user.id, - created_date=diagnosis.created_date, - ) - - def perform_update(self, serializer): - return serializer.save() - - def update(self, request, *args, **kwargs): - partial = kwargs.pop("partial", False) - instance = self.get_object() - old_instance = copy(instance) - serializer = self.get_serializer(instance, data=request.data, partial=partial) - serializer.is_valid(raise_exception=True) - instance = self.perform_update(serializer) - - create_consultation_events( - instance.consultation_id, - instance, - caused_by=self.request.user.id, - created_date=instance.created_date, - old=old_instance, - ) - - return Response(serializer.data) + serializer.save(consultation=consultation, created_by=self.request.user) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 585d13cdee..027a6c8241 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -49,7 +49,6 @@ ) from care.facility.api.serializers.patient_icmr import PatientICMRSerializer from care.facility.api.viewsets.mixins.history import HistoryMixin -from care.facility.events.handler import create_consultation_events from care.facility.models import ( CATEGORY_CHOICES, COVID_CATEGORY_CHOICES, @@ -982,13 +981,6 @@ def perform_create(self, serializer): created_by=self.request.user, ) - create_consultation_events( - instance.consultation_id, - instance, - self.request.user.id, - instance.created_date, - ) - message = { "facility_id": str(patient.facility.external_id), "patient_id": str(patient.external_id), diff --git a/care/facility/events/__init__.py b/care/facility/events/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/care/facility/events/handler.py b/care/facility/events/handler.py deleted file mode 100644 index a674c39e8e..0000000000 --- a/care/facility/events/handler.py +++ /dev/null @@ -1,116 +0,0 @@ -from contextlib import suppress -from datetime import datetime - -from django.core.exceptions import FieldDoesNotExist -from django.db import transaction -from django.db.models import Model -from django.db.models.query import QuerySet -from django.utils.timezone import now - -from care.facility.models.events import ChangeType, EventType, PatientConsultationEvent -from care.utils.event_utils import get_changed_fields, serialize_field - - -def create_consultation_event_entry( - consultation_id: int, - object_instance: Model, - caused_by: int, - created_date: datetime, - taken_at: datetime, - old_instance: Model | None = None, - fields_to_store: set[str] | None = None, -): - change_type = ChangeType.UPDATED if old_instance else ChangeType.CREATED - - fields: set[str] = ( - get_changed_fields(old_instance, object_instance) - if old_instance - else {field.name for field in object_instance._meta.fields} # noqa: SLF001 - ) - - fields_to_store = fields_to_store & fields if fields_to_store else fields - - batch = [] - groups = EventType.objects.filter( - model=object_instance.__class__.__name__, fields__len__gt=0, is_active=True - ).values_list("id", "fields") - for group_id, group_fields in groups: - if fields_to_store & {field.split("__", 1)[0] for field in group_fields}: - value = {} - for field in group_fields: - with suppress(FieldDoesNotExist): - value[field] = serialize_field(object_instance, field) - - if all(not v for v in value.values()): - continue - - PatientConsultationEvent.objects.select_for_update().filter( - consultation_id=consultation_id, - event_type=group_id, - is_latest=True, - object_model=object_instance.__class__.__name__, - object_id=object_instance.id, - taken_at__lt=taken_at, - ).update(is_latest=False) - batch.append( - PatientConsultationEvent( - consultation_id=consultation_id, - caused_by_id=caused_by, - event_type_id=group_id, - is_latest=True, - created_date=created_date, - taken_at=taken_at, - object_model=object_instance.__class__.__name__, - object_id=object_instance.id, - value=value, - change_type=change_type, - meta={ - "external_id": str(getattr(object_instance, "external_id", "")) - or None - }, - ) - ) - - PatientConsultationEvent.objects.bulk_create(batch) - return len(batch) - - -def create_consultation_events( - consultation_id: int, - objects: list | QuerySet | Model, - caused_by: int, - created_date: datetime | None = None, - taken_at: datetime | None = None, - old: Model | None = None, - fields_to_store: list[str] | set[str] | None = None, -): - if created_date is None: - created_date = now() - - if taken_at is None: - taken_at = created_date - - with transaction.atomic(): - if isinstance(objects, QuerySet | list | tuple): - if old is not None: - msg = "diff is not available when objects is a list or queryset" - raise ValueError(msg) - for obj in objects: - create_consultation_event_entry( - consultation_id, - obj, - caused_by, - created_date, - taken_at, - fields_to_store=set(fields_to_store) if fields_to_store else None, - ) - else: - create_consultation_event_entry( - consultation_id, - objects, - caused_by, - created_date, - taken_at, - old, - fields_to_store=set(fields_to_store) if fields_to_store else None, - ) diff --git a/care/facility/management/commands/load_event_types.py b/care/facility/management/commands/load_event_types.py deleted file mode 100644 index e0999e21aa..0000000000 --- a/care/facility/management/commands/load_event_types.py +++ /dev/null @@ -1,302 +0,0 @@ -from typing import TypedDict - -from django.core.management import BaseCommand - -from care.facility.models.events import EventType - - -class EventTypeDef(TypedDict, total=False): - name: str - model: str | None - children: tuple["EventType", ...] - fields: tuple[str, ...] - - -class Command(BaseCommand): - """ - Management command to load event types - """ - - consultation_event_types: tuple[EventTypeDef, ...] = ( - { - "name": "CONSULTATION", - "model": "PatientConsultation", - "children": ( - { - "name": "ENCOUNTER", - "children": ( - {"name": "PATIENT_NO", "fields": ("patient_no",)}, - {"name": "MEDICO_LEGAL_CASE", "fields": ("medico_legal_case",)}, - {"name": "ROUTE_TO_FACILITY", "fields": ("route_to_facility",)}, - ), - }, - { - "name": "CLINICAL", - "children": ( - { - "name": "DEATH", - "fields": ("death_datetime", "death_confirmed_doctor"), - }, - {"name": "SUGGESTION", "fields": ("suggestion",)}, - {"name": "CATEGORY", "fields": ("category",)}, - {"name": "EXAMINATION", "fields": ("examination_details",)}, - { - "name": "HISTORY_OF_PRESENT_ILLNESS", - "fields": ("history_of_present_illness",), - }, - {"name": "TREATMENT_PLAN", "fields": ("treatment_plan",)}, - { - "name": "CONSULTATION_NOTES", - "fields": ("consultation_notes",), - }, - { - "name": "COURSE_IN_FACILITY", - "fields": ("course_in_facility",), - }, - { - "name": "INVESTIGATION", - "fields": ("investigation",), - }, - { - "name": "TREATING_PHYSICIAN", - "fields": ( - "treating_physician__username", - "treating_physician__full_name", - ), - }, - ), - }, - { - "name": "HEALTH", - "children": ( - {"name": "HEIGHT", "fields": ("height",)}, - {"name": "WEIGHT", "fields": ("weight",)}, - ), - }, - { - "name": "INTERNAL_TRANSFER", - "children": ( - { - "name": "DISCHARGE", - "fields": ( - "discharge_date", - "discharge_reason", - "discharge_notes", - ), - }, - ), - }, - ), - }, - { - "name": "DAILY_ROUND", - "model": "DailyRound", - "children": ( - { - "name": "DAILY_ROUND_DETAILS", - "fields": ( - "taken_at", - "round_type", - "other_details", - "action", - "review_after", - ), - "children": ( - { - "name": "PHYSICAL_EXAMINATION", - "fields": ("physical_examination_info",), - }, - { - "name": "PATIENT_CATEGORY", - "fields": ("patient_category",), - }, - ), - }, - { - "name": "VITALS", - "children": ( - {"name": "TEMPERATURE", "fields": ("temperature",)}, - {"name": "PULSE", "fields": ("pulse",)}, - {"name": "BLOOD_PRESSURE", "fields": ("bp",)}, - {"name": "RESPIRATORY_RATE", "fields": ("resp",)}, - {"name": "RHYTHM", "fields": ("rhythm", "rhythm_detail")}, - {"name": "PAIN_SCALE", "fields": ("pain_scale_enhanced",)}, - ), - }, - { - "name": "NEUROLOGICAL", - "fields": ( - "left_pupil_size", - "left_pupil_size_detail", - "left_pupil_light_reaction", - "left_pupil_light_reaction_detail", - "right_pupil_size", - "right_pupil_size_detail", - "right_pupil_light_reaction", - "right_pupil_light_reaction_detail", - "glasgow_eye_open", - "glasgow_verbal_response", - "glasgow_motor_response", - "glasgow_total_calculated", - "limb_response_upper_extremity_left", - "limb_response_upper_extremity_right", - "limb_response_lower_extremity_left", - "limb_response_lower_extremity_right", - "consciousness_level", - "consciousness_level_detail", - "in_prone_position", - ), - }, - { - "name": "RESPIRATORY_SUPPORT", - "fields": ( - "bilateral_air_entry", - "etco2", - "ventilator_fio2", - "ventilator_interface", - "ventilator_mean_airway_pressure", - "ventilator_mode", - "ventilator_oxygen_modality", - "ventilator_oxygen_modality_flow_rate", - "ventilator_oxygen_modality_oxygen_rate", - "ventilator_peep", - "ventilator_pip", - "ventilator_pressure_support", - "ventilator_resp_rate", - "ventilator_spo2", - "ventilator_tidal_volume", - ), - }, - { - "name": "ARTERIAL_BLOOD_GAS_ANALYSIS", - "fields": ( - "base_excess", - "hco3", - "lactate", - "pco2", - "ph", - "po2", - "potassium", - "sodium", - ), - }, - { - "name": "BLOOD_GLUCOSE", - "fields": ( - "blood_sugar_level", - "insulin_intake_dose", - "insulin_intake_frequency", - ), - }, - { - "name": "IO_BALANCE", - "children": ( - {"name": "INFUSIONS", "fields": ("infusions",)}, - {"name": "IV_FLUIDS", "fields": ("iv_fluids",)}, - {"name": "FEEDS", "fields": ("feeds",)}, - {"name": "OUTPUT", "fields": ("output",)}, - { - "name": "TOTAL_INTAKE", - "fields": ("total_intake_calculated",), - }, - { - "name": "TOTAL_OUTPUT", - "fields": ("total_output_calculated",), - }, - ), - }, - { - "name": "DIALYSIS", - "fields": ( - "dialysis_fluid_balance", - "dialysis_net_balance", - ), - "children": ( - {"name": "PRESSURE_SORE", "fields": ("pressure_sore",)}, - ), - }, - {"name": "NURSING", "fields": ("nursing",)}, - { - "name": "ROUTINE", - "children": ( - {"name": "SLEEP_ROUTINE", "fields": ("sleep",)}, - {"name": "BOWEL_ROUTINE", "fields": ("bowel_issue",)}, - { - "name": "BLADDER_ROUTINE", - "fields": ( - "bladder_drainage", - "bladder_issue", - "experiences_dysuria", - "urination_frequency", - ), - }, - { - "name": "NUTRITION_ROUTINE", - "fields": ("nutrition_route", "oral_issue", "appetite"), - }, - ), - }, - ), - }, - { - "name": "PATIENT_NOTES", - "model": "PatientNotes", - "fields": ("note", "user_type"), - }, - { - "name": "DIAGNOSIS", - "model": "ConsultationDiagnosis", - "fields": ("diagnosis__label", "verification_status", "is_principal"), - }, - { - "name": "SYMPTOMS", - "model": "EncounterSymptom", - "fields": ( - "symptom", - "other_symptom", - "onset_date", - "cure_date", - "clinical_impression_status", - ), - }, - ) - - inactive_event_types: tuple[str, ...] = ( - "RESPIRATORY", - "INTAKE_OUTPUT", - "VENTILATOR_MODES", - "SYMPTOMS", - "ROUND_SYMPTOMS", - "SPO2", - ) - - def create_objects( - self, - types: tuple[EventType, ...], - model: str | None = None, - parent: EventType = None, - ): - for event_type in types: - model = event_type.get("model", model) - obj, _ = EventType.objects.update_or_create( - name=event_type["name"], - defaults={ - "parent": parent, - "model": model, - "fields": event_type.get("fields", []), - "is_active": True, - }, - ) - if children := event_type.get("children"): - self.create_objects(children, model, obj) - - def handle(self, *args, **options): - self.stdout.write("Loading Event Types... ", ending="") - - EventType.objects.filter(name__in=self.inactive_event_types).update( - is_active=False - ) - - self.create_objects(self.consultation_event_types) - - self.stdout.write(self.style.SUCCESS("OK")) diff --git a/care/utils/event_utils.py b/care/utils/event_utils.py index 55f1a183a5..316dfae8d8 100644 --- a/care/utils/event_utils.py +++ b/care/utils/event_utils.py @@ -2,63 +2,9 @@ from json import JSONEncoder from logging import getLogger -from django.core.exceptions import FieldDoesNotExist -from django.db.models import Field, Model - logger = getLogger(__name__) -def is_null(data): - return data is None or data == "" - - -def get_changed_fields(old: Model, new: Model) -> set[str]: - changed_fields: set[str] = set() - for field in new._meta.fields: # noqa: SLF001 - field_name = field.name - if getattr(old, field_name, None) != getattr(new, field_name, None): - changed_fields.add(field_name) - return changed_fields - - -def serialize_field(obj: Model, field_name: str): - if "__" in field_name: - field_name, sub_field = field_name.split("__", 1) - related_obj = getattr(obj, field_name, None) - return serialize_field(related_obj, sub_field) - - value = None - try: - value = getattr(obj, field_name) - except AttributeError: - if obj is not None: - logger.warning( - "Field %s not found in %s", field_name, obj.__class__.__name__ - ) - return None - - try: - # serialize choice fields with display value - field = obj._meta.get_field(field_name) # noqa: SLF001 - if issubclass(field.__class__, Field) and field.choices: - value = getattr(obj, f"get_{field_name}_display", lambda: value)() - except FieldDoesNotExist: - # the required field is a property and not a model field - pass - - return value - - -def model_diff(old, new): - diff = {} - for field in new._meta.fields: # noqa: SLF001 - field_name = field.name - if getattr(old, field_name, None) != getattr(new, field_name, None): - diff[field_name] = getattr(new, field_name, None) - - return diff - - class CustomJSONEncoder(JSONEncoder): def default(self, o): if isinstance(o, set): From a7130cff6b26ef7e2c80b6b0447d2ccbe110c312 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 20:25:29 +0530 Subject: [PATCH 13/19] Bump codecov/codecov-action from 4 to 5 (#2603) Bump codecov/codecov-action from 4 to 5 (#2603) --- .github/workflows/reusable-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-test.yml b/.github/workflows/reusable-test.yml index 8ecc633d01..baec75ebbb 100644 --- a/.github/workflows/reusable-test.yml +++ b/.github/workflows/reusable-test.yml @@ -65,7 +65,7 @@ jobs: run: make test-coverage - name: Upload coverage report - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} From fbc6157961b3cfed81942c34835359094d497032 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Mon, 25 Nov 2024 20:29:40 +0530 Subject: [PATCH 14/19] Update dependencies with security fixes (#2596) Update dependencies with security fixes (#2596) --- .devcontainer/devcontainer.json | 4 +- .pre-commit-config.yaml | 4 +- Pipfile | 32 +- Pipfile.lock | 1137 +++++++++++++++---------------- care/users/tests/test_auth.py | 7 +- docker-compose.pre-built.yaml | 6 +- docker/dev.Dockerfile | 24 +- docker/prod.Dockerfile | 30 +- scripts/celery-dev.sh | 30 +- scripts/celery_beat-ecs.sh | 38 +- scripts/celery_beat.sh | 35 +- scripts/celery_worker-ecs.sh | 11 +- scripts/celery_worker.sh | 17 +- scripts/start-dev.sh | 9 +- scripts/start-ecs.sh | 37 +- scripts/start.sh | 39 +- scripts/wait_for_db.sh | 26 + scripts/wait_for_redis.sh | 27 + 18 files changed, 715 insertions(+), 798 deletions(-) create mode 100755 scripts/wait_for_db.sh create mode 100755 scripts/wait_for_redis.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f7f05c0e56..e0becbbd6d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,7 @@ "hostRequirements": { "cpus": 4 }, - "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bookworm", + "image": "mcr.microsoft.com/devcontainers/python:1-3.13-bookworm", "features": { "ghcr.io/devcontainers/features/docker-in-docker:2": {}, "ghcr.io/devcontainers/features/node:1": {}, @@ -20,5 +20,5 @@ }, "postCreateCommand": "echo 'eval \"$(direnv hook bash)\"' >> ~/.bashrc && cp .env.example .env", "postStartCommand": "make up", - "forwardPorts": [8000, 9000, 4000] + "forwardPorts": [4566, 8000, 9000, 4000] } diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2969bbc4da..94f35fad12 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -default_stages: [commit] +default_stages: [pre-commit] repos: - repo: https://github.com/pre-commit/pre-commit-hooks @@ -17,7 +17,7 @@ repos: - id: check-toml - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.0 + rev: v0.7.3 hooks: - id: ruff args: [ --fix ] diff --git a/Pipfile b/Pipfile index b18cab9908..d4e99c5648 100644 --- a/Pipfile +++ b/Pipfile @@ -6,17 +6,17 @@ name = "pypi" [packages] argon2-cffi = "==23.1.0" authlib = "==1.3.2" -boto3 = "==1.35.49" +boto3 = "==1.35.59" celery = "==5.4.0" -django = "==5.1.2" +django = "==5.1.3" django-environ = "==0.11.2" -django-cors-headers = "==4.5.0" +django-cors-headers = "==4.6.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.2" +django-rest-passwordreset = "==1.5.0" django-simple-history = "==3.7.0" djangoql = "==0.18.1" djangorestframework = "==3.15.2" @@ -35,34 +35,34 @@ pydantic = "==1.10.18" # fix for fhir.resources < 7.0.2 pyjwt = "==2.9.0" python-slugify = "==8.0.4" 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 +redis = { extras = ["hiredis"], version = "==5.2.0" } +redis-om = "==0.3.3" requests = "==2.32.3" -sentry-sdk = "==2.17.0" -whitenoise = "==6.7.0" +sentry-sdk = "==2.18.0" +whitenoise = "==6.8.2" [dev-packages] -boto3-stubs = { extras = ["s3", "boto3"], version = "==1.35.49" } +boto3-stubs = { extras = ["s3", "boto3"], version = "*" } coverage = "==7.6.4" -debugpy = "==1.8.7" +debugpy = "==1.8.8" 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.28.0" -mypy = "==1.12.1" +ipython = "==8.29.0" +mypy = "==1.13.0" pre-commit = "==4.0.1" requests-mock = "==1.12.1" tblib = "==3.0.0" -watchdog = "==5.0.3" -werkzeug = "==3.0.6" -ruff = "==0.7.0" +watchdog = "==6.0.0" +werkzeug = "==3.1.3" +ruff = "==0.7.3" [docs] furo = "==2024.8.6" -sphinx = "==8.0.2" +sphinx = "==8.1.3" myst-parser = "==4.0.0" [requires] diff --git a/Pipfile.lock b/Pipfile.lock index 94a791a397..0426211c8d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e1007c202923fb8d82dc686ed4d9f3ff7e05afa8cbd6e81ea857baf3c0397a75" + "sha256": "30774623b65643fb52d6b2ed7bda5b20700df41f85a1aea8d25148461d9e00ad" }, "pipfile-spec": 6, "requires": { @@ -26,100 +26,85 @@ }, "aiohttp": { "hashes": [ - "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" + "sha256:024409c1b1d6076d0ed933dcebd7e4fc6f3320a227bfa0c1b6b93a8b5a146f04", + "sha256:04b24497b3baf15035730de5f207ade88a67d4483a5f16ced7ece348933a5b47", + "sha256:08474e71772a516ba2e2167b4707af8361d2c452b3d8a5364c984f4867869499", + "sha256:0e7a0762cc29cd3acd01a4d2b547b3af7956ad230ebb80b529a8e4f3e4740fe8", + "sha256:104deb7873681273c5daa13c41924693df394043a118dae90387d35bc5531788", + "sha256:104ea21994b1403e4c1b398866f1187c1694fa291314ad7216ec1d8ec6b49f38", + "sha256:113bf06b029143e94a47c4f36e11a8b7e396e9d1f1fc8cea58e6b7e370cfed38", + "sha256:12071dd2cc95ba81e0f2737bebcb98b2a8656015e87772e84e8fb9e635b5da6e", + "sha256:170fb2324826bb9f08055a8291f42192ae5ee2f25b2966c8f0f4537c61d73a7b", + "sha256:21b4545e8d96870da9652930c5198366605ff8f982757030e2148cf341e5746b", + "sha256:229ae13959a5f499d90ffbb4b9eac2255d8599315027d6f7c22fa9803a94d5b1", + "sha256:2ec5efbc872b00ddd85e3904059d274f284cff314e13f48776050ca2c58f451d", + "sha256:31b91ff3a1fcb206a1fa76e0de1f08c9ffb1dc0deb7296fa2618adfe380fc676", + "sha256:329f5059e0bf6983dceebac8e6ed20e75eaff6163b3414f4a4cb59e0d7037672", + "sha256:37f8cf3c43f292d9bb3e6760476c2b55b9663a581fad682a586a410c43a7683e", + "sha256:3e1ed8d152cccceffb1ee7a2ac227c16372e453fb11b3aeaa56783049b85d3f6", + "sha256:3ed360d6672a9423aad39902a4e9fe305464d20ed7931dbdba30a4625782d875", + "sha256:40dc9446cff326672fcbf93efdb8ef7e949824de1097624efe4f61ac7f0d2c43", + "sha256:4d218d3eca40196384ad3b481309c56fd60e664128885d1734da0a8aa530d433", + "sha256:4e4e155968040e32c124a89852a1a5426d0e920a35f4331e1b3949037bfe93a3", + "sha256:4f698aa61879df64425191d41213dfd99efdc1627e6398e6d7aa5c312fac9702", + "sha256:508cfcc99534b1282595357592d8367b44392b21f6eb5d4dc021f8d0d809e94d", + "sha256:577c7429f8869fa30186fc2c9eee64d75a30b51b61f26aac9725866ae5985cfd", + "sha256:57e17c6d71f2dc857a8a1d09be1be7802e35d90fb4ba4b06cf1aab6414a57894", + "sha256:5ecc2fb1a0a9d48cf773add34196cddf7e488e48e9596e090849751bf43098f4", + "sha256:600b1d9f86a130131915e2f2127664311b33902c486b21a747d626f5144b4471", + "sha256:62502b8ffee8c6a4b5c6bf99d1de277d42bf51b2fb713975d9b63b560150b7ac", + "sha256:62a2f5268b672087c45b33479ba1bb1d5a48c6d76c133cfce3a4f77410c200d1", + "sha256:6362f50a6f0e5482c4330d2151cb682779230683da0e155c15ec9fc58cb50b6a", + "sha256:6533dd06df3d17d1756829b68b365b1583929b54082db8f65083a4184bf68322", + "sha256:6c5a6958f4366496004cf503d847093d464814543f157ef3b738bbf604232415", + "sha256:72cd984f7f14e8c01b3e38f18f39ea85dba84e52ea05e37116ba5e2a72eef396", + "sha256:76d6ee8bb132f8ee0fcb0e205b4708ddb6fba524eb515ee168113063d825131b", + "sha256:7867d0808614f04e78e0a8d5a2c1f8ac6bc626a0c0e2f62be48be6b749e2f8b2", + "sha256:7d664e5f937c08adb7908ea9f391fbf2928a9b09cb412ac0aba602bde9e499e4", + "sha256:85ae6f182be72c3531915e90625cc65afce4df8a0fc4988bd52d8a5d5faaeb68", + "sha256:89a96a0696dc67d548f69cb518c581a7a33cc1f26ab42229dea1709217c9d926", + "sha256:8b323b5d3aef7dd811424c269322eec58a977c0c8152e650159e47210d900504", + "sha256:8c47a0ba6c2b3d3e5715f8338d657badd21f778c6be16701922c65521c5ecfc9", + "sha256:8fef105113d56e817cb9bcc609667ee461321413a7b972b03f5b4939f40f307c", + "sha256:900ff74d78eb580ae4aa5883242893b123a0c442a46570902500f08d6a7e6696", + "sha256:9095580806d9ed07c0c29b23364a0b1fb78258ef9f4bddf7e55bac0e475d4edf", + "sha256:91d3991fad8b65e5dbc13cd95669ea689fe0a96ff63e4e64ac24ed724e4f8103", + "sha256:9231d610754724273a6ac05a1f177979490bfa6f84d49646df3928af2e88cfd5", + "sha256:97056d3422594e0787733ac4c45bef58722d452f4dc6615fee42f59fe51707dd", + "sha256:a896059b6937d1a22d8ee8377cdcd097bd26cd8c653b8f972051488b9baadee9", + "sha256:aabc4e92cb153636d6be54e84dad1b252ddb9aebe077942b6dcffe5e468d476a", + "sha256:ad14cdc0fba4df31c0f6e06c21928c5b924725cbf60d0ccc5f6e7132636250e9", + "sha256:ae36ae52b0c22fb69fb8b744eff82a20db512a29eafc6e3a4ab43b17215b219d", + "sha256:b3e4fb7f5354d39490d8209aefdf5830b208d01c7293a2164e404312c3d8bc55", + "sha256:b40c304ab01e89ad0aeeecf91bbaa6ae3b00e27b796c9e8d50b71a4a7e885cc8", + "sha256:b7349205bb163318dcc102329d30be59a647a3d24c82c3d91ed35b7e7301ea7e", + "sha256:b8b95a63a8e8b5f0464bd8b1b0d59d2bec98a59b6aacc71e9be23df6989b3dfb", + "sha256:bb2e82e515e268b965424ecabebd91834a41b36260b6ef5db015ee12ddb28ef3", + "sha256:c0315978b2a4569e03fb59100f6a7e7d23f718a4521491f5c13d946d37549f3d", + "sha256:c1828e10c3a49e2b234b87600ecb68a92b8a8dcf8b99bca9447f16c4baaa1630", + "sha256:c1c49bc393d854d4421ebc174a0a41f9261f50d3694d8ca277146cbbcfd24ee7", + "sha256:c415b9601ff50709d6050c8a9281733a9b042b9e589265ac40305b875cf9c463", + "sha256:c54c635d1f52490cde7ef3a423645167a8284e452a35405d5c7dc1242a8e75c9", + "sha256:c5e6a1f8b0268ffa1c84d7c3558724956002ba8361176e76406233e704bbcffb", + "sha256:c98a596ac20e8980cc6f34c0c92a113e98eb08f3997c150064d26d2aeb043e5a", + "sha256:cd0834e4260eab78671b81d34f110fbaac449563e48d419cec0030d9a8e58693", + "sha256:cdad66685fcf2ad14ce522cf849d4a025f4fd206d6cfc3f403d9873e4c243b03", + "sha256:d1ea006426edf7e1299c52a58b0443158012f7a56fed3515164b60bfcb1503a9", + "sha256:d33b4490026968bdc7f0729b9d87a3a6b1e09043557d2fc1c605c6072deb2f11", + "sha256:d5cae4cd271e20b7ab757e966cc919186b9f02535418ab36c471a5377ef4deaa", + "sha256:dd505a1121ad5b666191840b7bd1d8cb917df2647deeca6f3474331b72452362", + "sha256:e1668ef2f3a7ec9881f4b6a917e5f97c87a343fa6b0d5fc826b7b0297ddd0887", + "sha256:e7bcfcede95531589295f56e924702cef7f9685c9e4e5407592e04ded6a65bf3", + "sha256:ebf610c37df4f09c71c9bbf8309b4b459107e6fe889ac0d7e16f6e4ebd975f86", + "sha256:f3bf5c132eb48002bcc3825702d241d35b4e9585009e65e9dcf9c4635d0b7424", + "sha256:f40380c96dd407dfa84eb2d264e68aa47717b53bdbe210a59cc3c35a4635f195", + "sha256:f57a0de48dda792629e7952d34a0c7b81ea336bb9b721391c7c58145b237fe55", + "sha256:f6b925c7775ab857bdc1e52e1f5abcae7d18751c09b751aeb641a5276d9b990e", + "sha256:f8f0d79b923070f25674e4ea8f3d61c9d89d24d9598d50ff32c5b9b23c79a25b", + "sha256:feca9fafa4385aea6759c171cd25ea82f7375312fca04178dae35331be45e538" + ], + "markers": "python_version >= '3.9'", + "version": "==3.11.0" }, "aiosignal": { "hashes": [ @@ -131,11 +116,11 @@ }, "amqp": { "hashes": [ - "sha256:827cb12fb0baa892aad844fd95258143bce4027fdac4fccddbc43330fd281637", - "sha256:a1ecff425ad063ad42a486c902807d1482311481c8ad95a72694b2975e75f7fd" + "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", + "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432" ], "markers": "python_version >= '3.6'", - "version": "==5.2.0" + "version": "==5.3.1" }, "argon2-cffi": { "hashes": [ @@ -208,20 +193,20 @@ }, "boto3": { "hashes": [ - "sha256:b660c649a27a6b47a34f6f858f5bd7c3b0a798a16dec8dda7cbebeee80fd1f60", - "sha256:ddecb27f5699ca9f97711c52b6c0652c2e63bf6c2bfbc13b819b4f523b4d30ff" + "sha256:81f4d8d6eff3e26b82cabd42eda816cfac9482821fdef353f18d2ba2f6e75f2d", + "sha256:8f8ff97cb9cb2e1ec7374209d0c09c1926b75604d6464c34bafaffd6d6cf0529" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.49" + "version": "==1.35.59" }, "botocore": { "hashes": [ - "sha256:07d0c1325fdbfa49a4a054413dbdeab0a6030449b2aa66099241af2dac48afd8", - "sha256:aed4d3643afd702920792b68fbe712a8c3847993820d1048cd238a6469354da1" + "sha256:378f53037d817bed2c04a006b7319745e664030182211429c924647273b29bc9", + "sha256:ddccfc39a0a55ac0321191a36d29c2ea9be2c96ceefb3928dd3c91c79c494d50" ], "markers": "python_version >= '3.8'", - "version": "==1.35.49" + "version": "==1.35.60" }, "celery": { "hashes": [ @@ -310,7 +295,7 @@ "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" ], - "markers": "platform_python_implementation != 'PyPy'", + "markers": "python_version >= '3.8'", "version": "==1.17.1" }, "charset-normalizer": { @@ -490,21 +475,21 @@ }, "django": { "hashes": [ - "sha256:bd7376f90c99f96b643722eee676498706c9fd7dc759f55ebfaf2c08ebcdf4f0", - "sha256:f11aa87ad8d5617171e3f77e1d5d16f004b79a2cf5d2e1d2b97a6a1f8e9ba5ed" + "sha256:8b38a9a12da3ae00cb0ba72da985ec4b14de6345046b1e174b1fd7254398f818", + "sha256:c0fa0e619c39325a169208caef234f90baa925227032ad3f44842ba14d75234a" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==5.1.2" + "version": "==5.1.3" }, "django-cors-headers": { "hashes": [ - "sha256:28c1ded847aa70208798de3e42422a782f427b8b720e8d7319d34b654b5978e6", - "sha256:6c01a85cf1ec779a7bde621db853aa3ce5c065a5ba8e27df7a9f9e8dac310f4f" + "sha256:14d76b4b4c8d39375baeddd89e4f08899051eeaf177cb02a29bd6eae8cf63aa8", + "sha256:8edbc0497e611c24d5150e0055d3b178c6534b8ed826fb6f53b21c63f5d48ba3" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==4.5.0" + "version": "==4.6.0" }, "django-environ": { "hashes": [ @@ -559,11 +544,11 @@ }, "django-rest-passwordreset": { "hashes": [ - "sha256:52e0a5ce0729102a9f691153ce5b554dc63660d9375932f7bc59e7ec242f2575", - "sha256:b81bd309bfdc3f01355e70c8e9c09f188f84590ad9ed3fd0bdcb075a76193006" + "sha256:6da38dd00e0cdb1ed5c403e66beeb90f10f8a1a73ebc13fdd27f2b5d67d14181", + "sha256:862fce99b12198aa1473299aac68febe4f40760a39beabc47f941cff5c9e7280" ], "index": "pypi", - "version": "==1.4.2" + "version": "==1.5.0" }, "django-simple-history": { "hashes": [ @@ -638,6 +623,7 @@ "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7" ], + "markers": "python_version >= '3.8'", "version": "==2.2.0" }, "fhir.resources": { @@ -765,103 +751,103 @@ }, "hiredis": { "hashes": [ - "sha256:06815c3b9bf7225c4dcc9dd9dfb5a9fa91b4f680104443ef3fcd78410d7eb027", - "sha256:070a0198401bc567709b9edff7f01e94c136dcca69d0ded4747b116bb0b8b577", - "sha256:082ba6a3189d59f44bf75ca2c0467cdbc67c860eacd4bf564b9a927471888603", - "sha256:0a87a249124666db2b795a0eb77cea5b8af8b148566616a681304826b4405869", - "sha256:1537d13eefe4f48cb979362264851ee90d2bb7a221c8c350e9ceeda9f0392228", - "sha256:168de1672bd73f7f3cdf0097084b4a71651ac35f7d99d0229ea8f223358d3a79", - "sha256:1bfa50491d3222e3c2297b52c14e835ac52702ac8a91ec3fc1ff5201912623bb", - "sha256:1c0e706e0c3d1ec54d8243410e0fd5974b1c7b69db5c54cd9ae6a3a4b64fae33", - "sha256:1d16f5023c1d9971f284231eb7036a25d4d123138a5adc4512c92a73d83b9a77", - "sha256:2a21e2740c33347740dceb106b64b8a384e91da49aac7e8b3f2a25a9b33714b9", - "sha256:2b76a5600047387c73c1b3d950e4ae3feffaefd442b20ba2f5fea773881d9bcd", - "sha256:2b90d9861673b0ba04651ade62e0fe568df71bbff8468657406848e9abf3650a", - "sha256:2d7715598c9034369cf739475ccc2db53a8ca895ff398fef6b9c597c30960ea8", - "sha256:339f29542be968153afd6c6495c1222681c4b66b9a5a5573c11512378b7167c9", - "sha256:38dd931f1124bd9781d3027a0cd6fb6f5a75b5c4ba4fe5540584105239b1f901", - "sha256:39e1c7212dea1bbed0b075574808bc7c3192b324f54ea5d9ee522f6c35014ce7", - "sha256:3abc0936c1efc59b510c7eab3799119a6ce8da94cea1f891854a6c3678d711f0", - "sha256:3ced14fbec28fbabda7cb9f9094f2578c154c14f1a820a91c30fc8ee0bea1a0d", - "sha256:400a42b8d16206e45c8223cdaf5acc35839e10c35383b3fba3f43e7eb315c213", - "sha256:468efdcbad7349a44aace693aed8324a01de180fcd4ef5513199eedb9b4341c8", - "sha256:469c1a85017abf11d854fb16eca9a4093ebe1f2dacf777fed869d726f02b1389", - "sha256:48baae8fbebf3b11660db6e51a55ff51516ed32edcd44a57f51ea9b373aca330", - "sha256:4bf4b8513cea6e04ddee1b578ab306fb8bfa84b2f7e92ee3dbaf65652abb07d1", - "sha256:4da6d881033a1bcb31bba152ea0925344127f0a98f86a6cf2ceb01cf6ecd29e2", - "sha256:52d92df0eb5bba7f31f302a08174d628956d7216453da9d96498da9341179288", - "sha256:54409fbefebe26274170c1c54e1852d310d84b85e405258aea6a78bec03b3eba", - "sha256:5598afad9e2f8e4fc9a456d281a9cc80315b0e18f5064437223dbfe67f49bded", - "sha256:5b0b2463906cc4119187dfaad493c48a7b2e17120946feb3eb7c2328c8cb4bca", - "sha256:5bdb223e7c3b9470f126bb77879ee2593fd79b28e1e8b11ad9edd3f866556109", - "sha256:5cc3c59dd0cd67d0aa0481a43392848a60f1a81d12b38ce8d56d6a5d6c190de8", - "sha256:5e45171fd046bbed2ce6ac485071cd0575d18ae98b5bbcf6533356e443ec47ea", - "sha256:6033cc6caaf056969af9ce372282a6ef2838559f2eadffe7ddb73bf65dcb27d6", - "sha256:605fe35ebb482b7c8d5daadcf3d264dc5edd205a352d89ee3a983861ef73cda8", - "sha256:6494120d0a0f46a1d7dfc7def55782782856bdd5acb2f6039fb1eafecea2c2c0", - "sha256:668b02556d12046e7ce94ded5bfe0ad9989d26e6977ecc55941b9a1a4a49d7d5", - "sha256:68e39d2c0beed53e5361caacd0de98f864b3532344edb79e27e62efba2262de5", - "sha256:6c3f8e0c3a0744d843e3044ea76db8aa996a6cc7541693111acc2c9c30a05182", - "sha256:6ceaf7c6b593bf62e0567fd16547727f502ed704352392708a57c65bfd2feb73", - "sha256:6dac8a5be01d92707409feec61b98721b7b5c3e77fe7e9e5c7cfb9fdd28385af", - "sha256:6e38f66dd7fd07a9306ed37d6d02bc584b67e5945f2ddc98e5c78420cc66dbac", - "sha256:7236b26828e005435fb3013894eed6a40c6f9b1b11a48391a904eee693ded204", - "sha256:737585b122fca03273bbf1f4e98909254dba6f8cd85f1cb566d6c890d0389277", - "sha256:764032f2222d70a130445fd332cf45d46d8226f4b3a7bf8abc314aa93d5a8212", - "sha256:76503a0edaf3d1557518127511e69e5d9fa37b6ff15598b0d9d9c2db18b08a41", - "sha256:83538638a788b7b4a0b02de0eedcf0e71ae27474b031276e4c8ca88285281a2e", - "sha256:8767cae1474f8102ec3d362976f80c8dd4eafd4109c6072adee0a15e37ba919c", - "sha256:87a8ece3e893f45354395c6b9dc0479744c1c8c6ee4471b60945d96c9b5ce6c2", - "sha256:8b88390a5e31572e05e8eab476ed3176cc3d2f9622ccc059398ffdb02aaefec4", - "sha256:90d7af678056c7889d86821344d79fec3932a6a1480ebba3d644cb29a3135348", - "sha256:98148ecaa7836f76ed33429e84a23253ac00acbad90c62b8b4ad0f61de31da2b", - "sha256:9aabc6098ef00e158598489db5a8b9e12d57a55ea5a4ec35ba3b527dfb88d16e", - "sha256:9ae4b19cab270fae77d7f944d56bbb308c9886d9577891b347a8deea75563995", - "sha256:9b4039cd40335f66e55a8bee314b6a795f169fb02d70215d482023ec74613371", - "sha256:9fc1a6c78197eff8b4d125bb98410b661e732f3ec563c03264d2d7378cf9e613", - "sha256:a40f1d985047fe4654a1afb4702cbe0daeacde3868d52be9e4652615d387e05b", - "sha256:a459b7ff3d802792254d6fc6a622e53ca9cf9f002ed79db7e4dee536b2e20e5d", - "sha256:a4f733882b67407d4b667eafd61fce86e8e204b158258cc1d0cb0843f6bb4708", - "sha256:a56a35e2e0b7eda39957ccd33059b79bb2fc57f54c501a917d1092c895f56d08", - "sha256:a5c3a32af789b0ec413a606c99b55579abbcb6c86220610a5c5041da8688e7ca", - "sha256:a5d2776c7cd6a338cd9338fb50f2a38a7ca3e16250b40ab2d0c41eb1697ebc12", - "sha256:a816f732f695261798a8a0fc1e0232a3638933b8ddfc574c00f9ef70d9f34cb8", - "sha256:a9d559775a95aee0ff06c0aaac638691619d6342b7cde85c62ad228804f82829", - "sha256:ac9d91b4d9c306e66a1abd224524fada07684a57f7da72a675e4b8bee9302b38", - "sha256:ae340c41024b9be566f600f364c8d286217f2975fd765fb3fb4dd6dfbdbec825", - "sha256:aeb60452d5b6150075974bc36e1cc74a46bd4b125cd5e72a86a04f4d6abf4e67", - "sha256:aee6c4e8f670ea685345ce4ca01c574a52e0a4318af2b8cdd563de9567731056", - "sha256:b027b53adb1df11923753d85587e3ab611fe70bc69596e9eb3269acab809c376", - "sha256:b0adbe8f33f57f2b6bfa8a2ea18f3e4ed91676503673f70f796bfbd06a1a2214", - "sha256:b30dcfbc5ab2fc932a723a39c2cb52d4f5c8b1705aa05a0bae23f28f70e06982", - "sha256:b385fc7fc7b0811c3fcac4b0a35e5606eca693efba0d1446623ef0158a078034", - "sha256:b4e5e9d1f84bbc01bf6a32a4704920c72e37d9090b3e0e29bd1574d06b3249f1", - "sha256:b50ad622d8a71c8b72582dc84a990f3f079775edc1bcf0f43ed59bb2277fca2f", - "sha256:b544a1a78e0812134572cc13f5ee330bfb6bfe6dda58d2e26c20557bb0e0cec9", - "sha256:b8472151e6f7ae90d7fd231a1ac16d2e628b93ce20d0f8063da25bd8bfdeb9e5", - "sha256:b868b7fc24dd8ab4762b59a533bdbd096ebba7eabc853c7f78af8edce46d1390", - "sha256:b8eee5d25efee64e172ed0d60ebcf6bca92b0b26a7fd048bb946b32fb90dbdc0", - "sha256:bae7f07731c6c285b87111c7d5c5efa65f8b48016a98bcc57eebc24a3c7d854d", - "sha256:beb0f7f8371d933072e9bdc00c6df7eb5fdf76b93f08bfe73094f60c3f011f57", - "sha256:c2676e2a934e046200faf0dc26ffa48c4989c3561c9bb97832e79969a41b2afe", - "sha256:c77113fbdbd7ca5de72dd3b7d113856609a1b878f6164de09dd95d12e6a51de2", - "sha256:c85110f536e59fe19ea4b002d04228f57f55462add1630a0785cd6ec62e70415", - "sha256:c9f8827cd7a84f5344779754ebb633bca71c470e028f92ecc959e666ef5c5e3c", - "sha256:cb62c82a2518b8446be1cc5eb4319e282776bf96fdb2964e81ff2c15d632248b", - "sha256:d5c711c8ca8d5767ed8ecd5fb5602c12eaf8fb256a5f4308ae36f2dc79e6f853", - "sha256:d851b7ff732ebc9d823de3c7cc95a5ed4261a0226acd46861a18369ac9568f36", - "sha256:e2a917ab420cd88b040ec85b5abc1244ab82b34d56461e2ffff58e0c7d018bae", - "sha256:e3215b43632a23b5b99165097949ce51dd093ab33d410bcf8aa901cdbc64d9cd", - "sha256:e71386f89dc2db805b4c9518dee6d81abddb8e79e4d9313cecdb702c924b8187", - "sha256:f34b39057956305935c71f51a0860709b6124c92281dc03841587dd45a86322c", - "sha256:f44715d6a3313d614ff7550e52ecff67a283776909d960f338701b57e6013542", - "sha256:f74bfa9f1b91718d6664d4708d092f7d44e2f0f825a5fab82819d43d41e0302d", - "sha256:f76fcf2867d19259b53680c08314435b46f632d20a4d7b9f0ccbb5dd3e925e79", - "sha256:fa4842977924209ae653e856238a30b1c68e579ecde5cf1c16c4de471b35cec7", - "sha256:fc8d3edbc9f32da930da6ea33d43ce0c3239e6b2018a77907fbf4e9836bd6def" - ], - "markers": "python_version >= '3.8'", - "version": "==2.4.0" + "sha256:00018f22f38530768b73ea86c11f47e8d4df65facd4e562bd78773bd1baef35e", + "sha256:034925b5fb514f7b11aac38cd55b3fd7e9d3af23bd6497f3f20aa5b8ba58e232", + "sha256:038756db735e417ab36ee6fd7725ce412385ed2bd0767e8179a4755ea11b804f", + "sha256:04ccae6dcd9647eae6025425ab64edb4d79fde8b9e6e115ebfabc6830170e3b2", + "sha256:0aacc0a78e1d94d843a6d191f224a35893e6bdfeb77a4a89264155015c65f126", + "sha256:0bb6f9fd92f147ba11d338ef5c68af4fd2908739c09e51f186e1d90958c68cc1", + "sha256:0dcfa684966f25b335072115de2f920228a3c2caf79d4bfa2b30f6e4f674a948", + "sha256:100431e04d25a522ef2c3b94f294c4219c4de3bfc7d557b6253296145a144c11", + "sha256:120f2dda469b28d12ccff7c2230225162e174657b49cf4cd119db525414ae281", + "sha256:122171ff47d96ed8dd4bba6c0e41d8afaba3e8194949f7720431a62aa29d8895", + "sha256:13c275b483a052dd645eb2cb60d6380f1f5215e4c22d6207e17b86be6dd87ffa", + "sha256:13c345e7278c210317e77e1934b27b61394fee0dec2e8bd47e71570900f75823", + "sha256:1f669212c390eebfbe03c4e20181f5970b82c5d0a0ad1df1785f7ffbe7d61150", + "sha256:1fb8de899f0145d6c4d5d4bd0ee88a78eb980a7ffabd51e9889251b8f58f1785", + "sha256:204b79b30a0e6be0dc2301a4d385bb61472809f09c49f400497f1cdd5a165c66", + "sha256:22c17c96143c2a62dfd61b13803bc5de2ac526b8768d2141c018b965d0333b66", + "sha256:23142a8af92a13fc1e3f2ca1d940df3dcf2af1d176be41fe8d89e30a837a0b60", + "sha256:3d22c53f0ec5c18ecb3d92aa9420563b1c5d657d53f01356114978107b00b860", + "sha256:3dc8043959b50141df58ab4f398e8ae84c6f9e673a2c9407be65fc789138f4a6", + "sha256:3ea635101b739c12effd189cc19b2671c268abb03013fd1f6321ca29df3ca625", + "sha256:41afc0d3c18b59eb50970479a9c0e5544fb4b95e3a79cf2fbaece6ddefb926fe", + "sha256:4664dedcd5933364756d7251a7ea86d60246ccf73a2e00912872dacbfcef8978", + "sha256:466f836dbcf86de3f9692097a7a01533dc9926986022c6617dc364a402b265c5", + "sha256:467d28112c7faa29b7db743f40803d927c8591e9da02b6ce3d5fadc170a542a2", + "sha256:47de0bbccf4c8a9f99d82d225f7672b9dd690d8fd872007b933ef51a302c9fa6", + "sha256:484025d2eb8f6348f7876fc5a2ee742f568915039fcb31b478fd5c242bb0fe3a", + "sha256:48727d7d405d03977d01885f317328dc21d639096308de126c2c4e9950cbd3c9", + "sha256:4b182791c41c5eb1d9ed736f0ff81694b06937ca14b0d4dadde5dadba7ff6dae", + "sha256:4c6efcbb5687cf8d2aedcc2c3ed4ac6feae90b8547427d417111194873b66b06", + "sha256:4ea3a86405baa8eb0d3639ced6926ad03e07113de54cb00fd7510cb0db76a89d", + "sha256:50a196af0ce657fcde9bf8a0bbe1032e22c64d8fcec2bc926a35e7ff68b3a166", + "sha256:50da7a9edf371441dfcc56288d790985ee9840d982750580710a9789b8f4a290", + "sha256:51b99cfac514173d7b8abdfe10338193e8a0eccdfe1870b646009d2fb7cbe4b5", + "sha256:54a6dd7b478e6eb01ce15b3bb5bf771e108c6c148315bf194eb2ab776a3cac4d", + "sha256:562eaf820de045eb487afaa37e6293fe7eceb5b25e158b5a1974b7e40bf04543", + "sha256:5a8dffb5f5b3415a4669d25de48b617fd9d44b0bccfc4c2ab24b06406ecc9ecb", + "sha256:5b5cff42a522a0d81c2ae7eae5e56d0ee7365e0c4ad50c4de467d8957aff4414", + "sha256:63482db3fadebadc1d01ad33afa6045ebe2ea528eb77ccaabd33ee7d9c2bad48", + "sha256:6ca41fa40fa019cde42c21add74aadd775e71458051a15a352eabeb12eb4d084", + "sha256:6eecb343c70629f5af55a8b3e53264e44fa04e155ef7989de13668a0cb102a90", + "sha256:719c32147ba29528cb451f037bf837dcdda4ff3ddb6cdb12c4216b0973174718", + "sha256:77c8006c12154c37691b24ff293c077300c22944018c3ff70094a33e10c1d795", + "sha256:793c80a3d6b0b0e8196a2d5de37a08330125668c8012922685e17aa9108c33ac", + "sha256:7d99b91e42217d7b4b63354b15b41ce960e27d216783e04c4a350224d55842a4", + "sha256:82f794d564f4bc76b80c50b03267fe5d6589e93f08e66b7a2f674faa2fa76ebc", + "sha256:83a29cc7b21b746cb6a480189e49f49b2072812c445e66a9e38d2004d496b81c", + "sha256:869f6d5537d243080f44253491bb30aa1ec3c21754003b3bddeadedeb65842b0", + "sha256:8854969e7480e8d61ed7549eb232d95082a743e94138d98d7222ba4e9f7ecacd", + "sha256:898636a06d9bf575d2c594129085ad6b713414038276a4bfc5db7646b8a5be78", + "sha256:8e0bb6102ebe2efecf8a3292c6660a0e6fac98176af6de67f020bea1c2343717", + "sha256:8fed69bbaa307040c62195a269f82fc3edf46b510a17abb6b30a15d7dab548df", + "sha256:9862db92ef67a8a02e0d5370f07d380e14577ecb281b79720e0d7a89aedb9ee5", + "sha256:98a152052b8878e5e43a2e3a14075218adafc759547c98668a21e9485882696c", + "sha256:99516d99316062824a24d145d694f5b0d030c80da693ea6f8c4ecf71a251d8bb", + "sha256:9b285ef6bf1581310b0d5e8f6ce64f790a1c40e89c660e1320b35f7515433672", + "sha256:a131377493a59fb0f5eaeb2afd49c6540cafcfba5b0b3752bed707be9e7c4eaf", + "sha256:a1c81c89ed765198da27412aa21478f30d54ef69bf5e4480089d9c3f77b8f882", + "sha256:a2537b2cd98192323fce4244c8edbf11f3cac548a9d633dbbb12b48702f379f4", + "sha256:a41be8af1fd78ca97bc948d789a09b730d1e7587d07ca53af05758f31f4b985d", + "sha256:a631e2990b8be23178f655cae8ac6c7422af478c420dd54e25f2e26c29e766f1", + "sha256:a6a49ef161739f8018c69b371528bdb47d7342edfdee9ddc75a4d8caddf45a6e", + "sha256:ac6d929cb33dd12ad3424b75725975f0a54b5b12dbff95f2a2d660c510aa106d", + "sha256:b23291951959141173eec10f8573538e9349fa27f47a0c34323d1970bf891ee5", + "sha256:ba9fc605ac558f0de67463fb588722878641e6fa1dabcda979e8e69ff581d0bd", + "sha256:bdc144d56333c52c853c31b4e2e52cfbdb22d3da4374c00f5f3d67c42158970f", + "sha256:c073848d2b1d5561f3903879ccf4e1a70c9b1e7566c7bdcc98d082fa3e7f0a1d", + "sha256:c1018cc7f12824506f165027eabb302735b49e63af73eb4d5450c66c88f47026", + "sha256:c3ece960008dab66c6b8bb3a1350764677ee7c74ccd6270aaf1b1caf9ccebb46", + "sha256:c3fdad75e7837a475900a1d3a5cc09aa024293c3b0605155da2d42f41bc0e482", + "sha256:c8a1df39d74ec507d79c7a82c8063eee60bf80537cdeee652f576059b9cdd15c", + "sha256:c8a91e9520fbc65a799943e5c970ffbcd67905744d8becf2e75f9f0a5e8414f0", + "sha256:d10fcd9e0eeab835f492832b2a6edb5940e2f1230155f33006a8dfd3bd2c94e4", + "sha256:d435ae89073d7cd51e6b6bf78369c412216261c9c01662e7008ff00978153729", + "sha256:d7a4c1791d7aa7e192f60fe028ae409f18ccdd540f8b1e6aeb0df7816c77e4a4", + "sha256:dc384874a719c767b50a30750f937af18842ee5e288afba95a5a3ed703b1515a", + "sha256:df274e3abb4df40f4c7274dd3e587dfbb25691826c948bc98d5fead019dfb001", + "sha256:e069967cbd5e1900aafc4b5943888f6d34937fc59bf8918a1a546cb729b4b1e4", + "sha256:e194a0d5df9456995d8f510eab9f529213e7326af6b94770abf8f8b7952ddcaa", + "sha256:e1a9c14ae9573d172dc050a6f63a644457df5d01ec4d35a6a0f097f812930f83", + "sha256:e241fab6332e8fb5f14af00a4a9c6aefa22f19a336c069b7ddbf28ef8341e8d6", + "sha256:e421ac9e4b5efc11705a0d5149e641d4defdc07077f748667f359e60dc904420", + "sha256:e43679eca508ba8240d016d8cca9d27342d70184773c15bea78a23c87a1922f1", + "sha256:e584fe5f4e6681d8762982be055f1534e0170f6308a7a90f58d737bab12ff6a8", + "sha256:f114a6c86edbf17554672b050cce72abf489fe58d583c7921904d5f1c9691605", + "sha256:f2f312eef8aafc2255e3585dcf94d5da116c43ef837db91db9ecdc1bc930072d", + "sha256:f359175197fd833c8dd7a8c288f1516be45415bb5c939862ab60c2918e1e1943", + "sha256:f75999ae00a920f7dce6ecae76fa5e8674a3110e5a75f12c7a2c75ae1af53396", + "sha256:f91456507427ba36fd81b2ca11053a8e112c775325acc74e993201ea912d63e9", + "sha256:fa1fcad89d8a41d8dc10b1e54951ec1e161deabd84ed5a2c95c3c7213bdb3514", + "sha256:fa86bf9a0ed339ec9e8a9a9d0ae4dccd8671625c83f9f9f2640729b15e07fbfd", + "sha256:fcdb552ffd97151dab8e7bc3ab556dfa1512556b48a367db94b5c20253a35ee1", + "sha256:fcecbd39bd42cef905c0b51c9689c39d0cc8b88b1671e7f40d4fb213423aef3a", + "sha256:fe91d62b0594db5ea7d23fc2192182b1a7b6973f628a9b8b2e0a42a2be721ac6", + "sha256:fed8581ae26345dea1f1e0d1a96e05041a727a45e7d8d459164583e23c6ac441" + ], + "markers": "python_version >= '3.8'", + "version": "==3.0.0" }, "http-ece": { "hashes": [ @@ -1062,11 +1048,11 @@ }, "packaging": { "hashes": [ - "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", - "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" ], "markers": "python_version >= '3.8'", - "version": "==24.1" + "version": "==24.2" }, "pillow": { "hashes": [ @@ -1284,6 +1270,7 @@ "hashes": [ "sha256:06ae7db8eaec1a3845960fa7f997f4ccdb1a7a7ab8dc593a680bcc74e1359671" ], + "markers": "python_version >= '3.8'", "version": "==3.2.3" }, "py-vapid": { @@ -1465,20 +1452,20 @@ "hiredis" ], "hashes": [ - "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870", - "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4" + "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0", + "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897" ], - "markers": "python_version >= '3.7'", - "version": "==5.0.8" + "markers": "python_version >= '3.8'", + "version": "==5.2.0" }, "redis-om": { "hashes": [ - "sha256:1a1eea45a507da3541a6afa982c7aecae2d58920c756525198917afc433504ee", - "sha256:c521b4e60d7bbdf537642f5b94d004330a095dcc1e4daf6efec8e46b0a2f2799" + "sha256:094fc5ae1bd0edd5e722edc1df13e97281e756356f4bc22ee39bb4a94fc8a66b", + "sha256:7cafd8ca2117f12ff276acf547f2c58261ce3b5dab4fa19b8a1569dbf0d97d28" ], "index": "pypi", "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==0.3.1" + "version": "==0.3.3" }, "referencing": { "hashes": [ @@ -1499,112 +1486,99 @@ }, "rpds-py": { "hashes": [ - "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c", - "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585", - "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5", - "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6", - "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef", - "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2", - "sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29", - "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318", - "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b", - "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399", - "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739", - "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee", - "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174", - "sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a", - "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344", - "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2", - "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03", - "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5", - "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22", - "sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e", - "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96", - "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91", - "sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752", - "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075", - "sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253", - "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee", - "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad", - "sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5", - "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce", - "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7", - "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b", - "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8", - "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57", - "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3", - "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec", - "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209", - "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921", - "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045", - "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074", - "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580", - "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7", - "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5", - "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3", - "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0", - "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24", - "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139", - "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db", - "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc", - "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789", - "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f", - "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2", - "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c", - "sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232", - "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6", - "sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c", - "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29", - "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489", - "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94", - "sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751", - "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2", - "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda", - "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9", - "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51", - "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c", - "sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8", - "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989", - "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511", - "sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1", - "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2", - "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150", - "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c", - "sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965", - "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f", - "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58", - "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b", - "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f", - "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d", - "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821", - "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de", - "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121", - "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855", - "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272", - "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60", - "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02", - "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1", - "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140", - "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879", - "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940", - "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364", - "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4", - "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e", - "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420", - "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5", - "sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24", - "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c", - "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf", - "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f", - "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e", - "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab", - "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08", - "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92", - "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a", - "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8" - ], - "markers": "python_version >= '3.8'", - "version": "==0.20.0" + "sha256:031819f906bb146561af051c7cef4ba2003d28cff07efacef59da973ff7969ba", + "sha256:0626238a43152918f9e72ede9a3b6ccc9e299adc8ade0d67c5e142d564c9a83d", + "sha256:085ed25baac88953d4283e5b5bd094b155075bb40d07c29c4f073e10623f9f2e", + "sha256:0a9e0759e7be10109645a9fddaaad0619d58c9bf30a3f248a2ea57a7c417173a", + "sha256:0c025820b78817db6a76413fff6866790786c38f95ea3f3d3c93dbb73b632202", + "sha256:1ff2eba7f6c0cb523d7e9cff0903f2fe1feff8f0b2ceb6bd71c0e20a4dcee271", + "sha256:20cc1ed0bcc86d8e1a7e968cce15be45178fd16e2ff656a243145e0b439bd250", + "sha256:241e6c125568493f553c3d0fdbb38c74babf54b45cef86439d4cd97ff8feb34d", + "sha256:2c51d99c30091f72a3c5d126fad26236c3f75716b8b5e5cf8effb18889ced928", + "sha256:2d6129137f43f7fa02d41542ffff4871d4aefa724a5fe38e2c31a4e0fd343fb0", + "sha256:30b912c965b2aa76ba5168fd610087bad7fcde47f0a8367ee8f1876086ee6d1d", + "sha256:30bdc973f10d28e0337f71d202ff29345320f8bc49a31c90e6c257e1ccef4333", + "sha256:320c808df533695326610a1b6a0a6e98f033e49de55d7dc36a13c8a30cfa756e", + "sha256:32eb88c30b6a4f0605508023b7141d043a79b14acb3b969aa0b4f99b25bc7d4a", + "sha256:3b766a9f57663396e4f34f5140b3595b233a7b146e94777b97a8413a1da1be18", + "sha256:3b929c2bb6e29ab31f12a1117c39f7e6d6450419ab7464a4ea9b0b417174f044", + "sha256:3e30a69a706e8ea20444b98a49f386c17b26f860aa9245329bab0851ed100677", + "sha256:3e53861b29a13d5b70116ea4230b5f0f3547b2c222c5daa090eb7c9c82d7f664", + "sha256:40c91c6e34cf016fa8e6b59d75e3dbe354830777fcfd74c58b279dceb7975b75", + "sha256:4991ca61656e3160cdaca4851151fd3f4a92e9eba5c7a530ab030d6aee96ec89", + "sha256:4ab2c2a26d2f69cdf833174f4d9d86118edc781ad9a8fa13970b527bf8236027", + "sha256:4e8921a259f54bfbc755c5bbd60c82bb2339ae0324163f32868f63f0ebb873d9", + "sha256:4eb2de8a147ffe0626bfdc275fc6563aa7bf4b6db59cf0d44f0ccd6ca625a24e", + "sha256:5145282a7cd2ac16ea0dc46b82167754d5e103a05614b724457cffe614f25bd8", + "sha256:520ed8b99b0bf86a176271f6fe23024323862ac674b1ce5b02a72bfeff3fff44", + "sha256:52c041802a6efa625ea18027a0723676a778869481d16803481ef6cc02ea8cb3", + "sha256:5555db3e618a77034954b9dc547eae94166391a98eb867905ec8fcbce1308d95", + "sha256:58a0e345be4b18e6b8501d3b0aa540dad90caeed814c515e5206bb2ec26736fd", + "sha256:590ef88db231c9c1eece44dcfefd7515d8bf0d986d64d0caf06a81998a9e8cab", + "sha256:5afb5efde74c54724e1a01118c6e5c15e54e642c42a1ba588ab1f03544ac8c7a", + "sha256:688c93b77e468d72579351a84b95f976bd7b3e84aa6686be6497045ba84be560", + "sha256:6b4ef7725386dc0762857097f6b7266a6cdd62bfd209664da6712cb26acef035", + "sha256:6bc0e697d4d79ab1aacbf20ee5f0df80359ecf55db33ff41481cf3e24f206919", + "sha256:6dcc4949be728ede49e6244eabd04064336012b37f5c2200e8ec8eb2988b209c", + "sha256:6f54e7106f0001244a5f4cf810ba8d3f9c542e2730821b16e969d6887b664266", + "sha256:808f1ac7cf3b44f81c9475475ceb221f982ef548e44e024ad5f9e7060649540e", + "sha256:8404b3717da03cbf773a1d275d01fec84ea007754ed380f63dfc24fb76ce4592", + "sha256:878f6fea96621fda5303a2867887686d7a198d9e0f8a40be100a63f5d60c88c9", + "sha256:8a7ff941004d74d55a47f916afc38494bd1cfd4b53c482b77c03147c91ac0ac3", + "sha256:95a5bad1ac8a5c77b4e658671642e4af3707f095d2b78a1fdd08af0dfb647624", + "sha256:97ef67d9bbc3e15584c2f3c74bcf064af36336c10d2e21a2131e123ce0f924c9", + "sha256:98486337f7b4f3c324ab402e83453e25bb844f44418c066623db88e4c56b7c7b", + "sha256:98e4fe5db40db87ce1c65031463a760ec7906ab230ad2249b4572c2fc3ef1f9f", + "sha256:998a8080c4495e4f72132f3d66ff91f5997d799e86cec6ee05342f8f3cda7dca", + "sha256:9afe42102b40007f588666bc7de82451e10c6788f6f70984629db193849dced1", + "sha256:9e20da3957bdf7824afdd4b6eeb29510e83e026473e04952dca565170cd1ecc8", + "sha256:a017f813f24b9df929674d0332a374d40d7f0162b326562daae8066b502d0590", + "sha256:a429b99337062877d7875e4ff1a51fe788424d522bd64a8c0a20ef3021fdb6ed", + "sha256:a58ce66847711c4aa2ecfcfaff04cb0327f907fead8945ffc47d9407f41ff952", + "sha256:a78d8b634c9df7f8d175451cfeac3810a702ccb85f98ec95797fa98b942cea11", + "sha256:a89a8ce9e4e75aeb7fa5d8ad0f3fecdee813802592f4f46a15754dcb2fd6b061", + "sha256:a8eeec67590e94189f434c6d11c426892e396ae59e4801d17a93ac96b8c02a6c", + "sha256:aaeb25ccfb9b9014a10eaf70904ebf3f79faaa8e60e99e19eef9f478651b9b74", + "sha256:ad116dda078d0bc4886cb7840e19811562acdc7a8e296ea6ec37e70326c1b41c", + "sha256:af04ac89c738e0f0f1b913918024c3eab6e3ace989518ea838807177d38a2e94", + "sha256:af4a644bf890f56e41e74be7d34e9511e4954894d544ec6b8efe1e21a1a8da6c", + "sha256:b21747f79f360e790525e6f6438c7569ddbfb1b3197b9e65043f25c3c9b489d8", + "sha256:b229ce052ddf1a01c67d68166c19cb004fb3612424921b81c46e7ea7ccf7c3bf", + "sha256:b4de1da871b5c0fd5537b26a6fc6814c3cc05cabe0c941db6e9044ffbb12f04a", + "sha256:b80b4690bbff51a034bfde9c9f6bf9357f0a8c61f548942b80f7b66356508bf5", + "sha256:b876f2bc27ab5954e2fd88890c071bd0ed18b9c50f6ec3de3c50a5ece612f7a6", + "sha256:b8f107395f2f1d151181880b69a2869c69e87ec079c49c0016ab96860b6acbe5", + "sha256:b9b76e2afd585803c53c5b29e992ecd183f68285b62fe2668383a18e74abe7a3", + "sha256:c2b2f71c6ad6c2e4fc9ed9401080badd1469fa9889657ec3abea42a3d6b2e1ed", + "sha256:c3761f62fcfccf0864cc4665b6e7c3f0c626f0380b41b8bd1ce322103fa3ef87", + "sha256:c38dbf31c57032667dd5a2f0568ccde66e868e8f78d5a0d27dcc56d70f3fcd3b", + "sha256:ca9989d5d9b1b300bc18e1801c67b9f6d2c66b8fd9621b36072ed1df2c977f72", + "sha256:cbd7504a10b0955ea287114f003b7ad62330c9e65ba012c6223dba646f6ffd05", + "sha256:d167e4dbbdac48bd58893c7e446684ad5d425b407f9336e04ab52e8b9194e2ed", + "sha256:d2132377f9deef0c4db89e65e8bb28644ff75a18df5293e132a8d67748397b9f", + "sha256:da52d62a96e61c1c444f3998c434e8b263c384f6d68aca8274d2e08d1906325c", + "sha256:daa8efac2a1273eed2354397a51216ae1e198ecbce9036fba4e7610b308b6153", + "sha256:dc5695c321e518d9f03b7ea6abb5ea3af4567766f9852ad1560f501b17588c7b", + "sha256:de552f4a1916e520f2703ec474d2b4d3f86d41f353e7680b597512ffe7eac5d0", + "sha256:de609a6f1b682f70bb7163da745ee815d8f230d97276db049ab447767466a09d", + "sha256:e12bb09678f38b7597b8346983d2323a6482dcd59e423d9448108c1be37cac9d", + "sha256:e168afe6bf6ab7ab46c8c375606298784ecbe3ba31c0980b7dcbb9631dcba97e", + "sha256:e78868e98f34f34a88e23ee9ccaeeec460e4eaf6db16d51d7a9b883e5e785a5e", + "sha256:e860f065cc4ea6f256d6f411aba4b1251255366e48e972f8a347cf88077b24fd", + "sha256:ea3a6ac4d74820c98fcc9da4a57847ad2cc36475a8bd9683f32ab6d47a2bd682", + "sha256:ebf64e281a06c904a7636781d2e973d1f0926a5b8b480ac658dc0f556e7779f4", + "sha256:ed6378c9d66d0de903763e7706383d60c33829581f0adff47b6535f1802fa6db", + "sha256:ee1e4fc267b437bb89990b2f2abf6c25765b89b72dd4a11e21934df449e0c976", + "sha256:ee4eafd77cc98d355a0d02f263efc0d3ae3ce4a7c24740010a8b4012bbb24937", + "sha256:efec946f331349dfc4ae9d0e034c263ddde19414fe5128580f512619abed05f1", + "sha256:f414da5c51bf350e4b7960644617c130140423882305f7574b6cf65a3081cecb", + "sha256:f71009b0d5e94c0e86533c0b27ed7cacc1239cb51c178fd239c3cfefefb0400a", + "sha256:f983e4c2f603c95dde63df633eec42955508eefd8d0f0e6d236d31a044c882d7", + "sha256:faa5e8496c530f9c71f2b4e1c49758b06e5f4055e17144906245c99fa6d45356", + "sha256:fed5dfefdf384d6fe975cc026886aece4f292feaf69d0eeb716cfd3c5a4dd8be" + ], + "markers": "python_version >= '3.9'", + "version": "==0.21.0" }, "s3transfer": { "hashes": [ @@ -1616,20 +1590,20 @@ }, "sentry-sdk": { "hashes": [ - "sha256:625955884b862cc58748920f9e21efdfb8e0d4f98cca4ab0d3918576d5b606ad", - "sha256:dd0a05352b78ffeacced73a94e86f38b32e2eae15fff5f30ca5abb568a72eacf" + "sha256:0dc21febd1ab35c648391c664df96f5f79fb0d92d7d4225cd9832e53a617cafd", + "sha256:ee70e27d1bbe4cd52a38e1bd28a5fadb9b17bc29d91b5f2b97ae29c0a7610442" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==2.17.0" + "version": "==2.18.0" }, "setuptools": { "hashes": [ - "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987", - "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32" + "sha256:5c4ccb41111392671f02bb5f8436dfc5a9a7185e80500531b133f5775c4163ef", + "sha256:87cb777c3b96d638ca02031192d40390e0ad97737e27b6b4fa831bea86f2f829" ], - "markers": "python_version >= '3.12'", - "version": "==69.5.1" + "markers": "python_version >= '3.9'", + "version": "==75.5.0" }, "six": { "hashes": [ @@ -1641,11 +1615,11 @@ }, "sqlparse": { "hashes": [ - "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", - "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e" + "sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f", + "sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e" ], "markers": "python_version >= '3.8'", - "version": "==0.5.1" + "version": "==0.5.2" }, "text-unidecode": { "hashes": [ @@ -1680,11 +1654,11 @@ }, "types-setuptools": { "hashes": [ - "sha256:2949913a518d5285ce00a3b7d88961c80a6e72ffb8f3da0a3f5650ea533bd45e", - "sha256:6721ac0f1a620321e2ccd87a9a747c4a383dc381f78d894ce37f2455b45fcf1c" + "sha256:78cb5fef4a6056d2f37114d27da90f4655a306e4e38042d7034a8a880bc3f5dd", + "sha256:f9e1ebd17a56f606e16395c4ee4efa1cdc394b9a2a0ee898a624058b4b62ef8f" ], "markers": "python_version >= '3.8'", - "version": "==75.2.0.20241025" + "version": "==75.3.0.20241112" }, "typing-extensions": { "hashes": [ @@ -1741,100 +1715,100 @@ }, "whitenoise": { "hashes": [ - "sha256:58c7a6cd811e275a6c91af22e96e87da0b1109e9a53bb7464116ef4c963bf636", - "sha256:a1ae85e01fdc9815d12fa33f17765bc132ed2c54fa76daf9e39e879dd93566f6" + "sha256:486bd7267a375fa9650b136daaec156ac572971acc8bf99add90817a530dd1d4", + "sha256:df12dce147a043d1956d81d288c6f0044147c6d2ab9726e5772ac50fb45d2280" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==6.7.0" + "markers": "python_version >= '3.9'", + "version": "==6.8.2" }, "yarl": { "hashes": [ - "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" + "sha256:06157fb3c58f2736a5e47c8fcbe1afc8b5de6fb28b14d25574af9e62150fcaac", + "sha256:067a63fcfda82da6b198fa73079b1ca40b7c9b7994995b6ee38acda728b64d47", + "sha256:0b1794853124e2f663f0ea54efb0340b457f08d40a1cef78edfa086576179c91", + "sha256:0bdff5e0995522706c53078f531fb586f56de9c4c81c243865dd5c66c132c3b5", + "sha256:117ed8b3732528a1e41af3aa6d4e08483c2f0f2e3d3d7dca7cf538b3516d93df", + "sha256:14bc88baa44e1f84164a392827b5defb4fa8e56b93fecac3d15315e7c8e5d8b3", + "sha256:1654ec814b18be1af2c857aa9000de7a601400bd4c9ca24629b18486c2e35463", + "sha256:16bca6678a83657dd48df84b51bd56a6c6bd401853aef6d09dc2506a78484c7b", + "sha256:1a3b91c44efa29e6c8ef8a9a2b583347998e2ba52c5d8280dbd5919c02dfc3b5", + "sha256:1a52a1ffdd824fb1835272e125385c32fd8b17fbdefeedcb4d543cc23b332d74", + "sha256:1ce36ded585f45b1e9bb36d0ae94765c6608b43bd2e7f5f88079f7a85c61a4d3", + "sha256:299f11b44d8d3a588234adbe01112126010bd96d9139c3ba7b3badd9829261c3", + "sha256:2b24ec55fad43e476905eceaf14f41f6478780b870eda5d08b4d6de9a60b65b4", + "sha256:2d374d70fdc36f5863b84e54775452f68639bc862918602d028f89310a034ab0", + "sha256:2d9f0606baaec5dd54cb99667fcf85183a7477f3766fbddbe3f385e7fc253299", + "sha256:2e7ba4c9377e48fb7b20dedbd473cbcbc13e72e1826917c185157a137dac9df2", + "sha256:2f0a6423295a0d282d00e8701fe763eeefba8037e984ad5de44aa349002562ac", + "sha256:327828786da2006085a4d1feb2594de6f6d26f8af48b81eb1ae950c788d97f61", + "sha256:380e6c38ef692b8fd5a0f6d1fa8774d81ebc08cfbd624b1bca62a4d4af2f9931", + "sha256:3b74ff4767d3ef47ffe0cd1d89379dc4d828d4873e5528976ced3b44fe5b0a21", + "sha256:3e844be8d536afa129366d9af76ed7cb8dfefec99f5f1c9e4f8ae542279a6dc3", + "sha256:459e81c2fb920b5f5df744262d1498ec2c8081acdcfe18181da44c50f51312f7", + "sha256:46ddf6e0b975cd680eb83318aa1d321cb2bf8d288d50f1754526230fcf59ba96", + "sha256:482c122b72e3c5ec98f11457aeb436ae4aecca75de19b3d1de7cf88bc40db82f", + "sha256:561c87fea99545ef7d692403c110b2f99dced6dff93056d6e04384ad3bc46243", + "sha256:578d00c9b7fccfa1745a44f4eddfdc99d723d157dad26764538fbdda37209857", + "sha256:58c8e9620eb82a189c6c40cb6b59b4e35b2ee68b1f2afa6597732a2b467d7e8f", + "sha256:5b29beab10211a746f9846baa39275e80034e065460d99eb51e45c9a9495bcca", + "sha256:5d1d42556b063d579cae59e37a38c61f4402b47d70c29f0ef15cee1acaa64488", + "sha256:5f236cb5999ccd23a0ab1bd219cfe0ee3e1c1b65aaf6dd3320e972f7ec3a39da", + "sha256:62a91aefff3d11bf60e5956d340eb507a983a7ec802b19072bb989ce120cd948", + "sha256:64cc6e97f14cf8a275d79c5002281f3040c12e2e4220623b5759ea7f9868d6a5", + "sha256:6f4c9156c4d1eb490fe374fb294deeb7bc7eaccda50e23775b2354b6a6739934", + "sha256:7294e38f9aa2e9f05f765b28ffdc5d81378508ce6dadbe93f6d464a8c9594473", + "sha256:7615058aabad54416ddac99ade09a5510cf77039a3b903e94e8922f25ed203d7", + "sha256:7e48cdb8226644e2fbd0bdb0a0f87906a3db07087f4de77a1b1b1ccfd9e93685", + "sha256:7f63d176a81555984e91f2c84c2a574a61cab7111cc907e176f0f01538e9ff6e", + "sha256:7f6595c852ca544aaeeb32d357e62c9c780eac69dcd34e40cae7b55bc4fb1147", + "sha256:7fac95714b09da9278a0b52e492466f773cfe37651cf467a83a1b659be24bf71", + "sha256:81713b70bea5c1386dc2f32a8f0dab4148a2928c7495c808c541ee0aae614d67", + "sha256:846dd2e1243407133d3195d2d7e4ceefcaa5f5bf7278f0a9bda00967e6326b04", + "sha256:84c063af19ef5130084db70ada40ce63a84f6c1ef4d3dbc34e5e8c4febb20822", + "sha256:881764d610e3269964fc4bb3c19bb6fce55422828e152b885609ec176b41cf11", + "sha256:8994b29c462de9a8fce2d591028b986dbbe1b32f3ad600b2d3e1c482c93abad6", + "sha256:8c79e9d7e3d8a32d4824250a9c6401194fb4c2ad9a0cec8f6a96e09a582c2cc0", + "sha256:8ee427208c675f1b6e344a1f89376a9613fc30b52646a04ac0c1f6587c7e46ec", + "sha256:949681f68e0e3c25377462be4b658500e85ca24323d9619fdc41f68d46a1ffda", + "sha256:9e275792097c9f7e80741c36de3b61917aebecc08a67ae62899b074566ff8556", + "sha256:9fb815155aac6bfa8d86184079652c9715c812d506b22cfa369196ef4e99d1b4", + "sha256:a2a64e62c7a0edd07c1c917b0586655f3362d2c2d37d474db1a509efb96fea1c", + "sha256:a7ac5b4984c468ce4f4a553df281450df0a34aefae02e58d77a0847be8d1e11f", + "sha256:aa46dce75078fceaf7cecac5817422febb4355fbdda440db55206e3bd288cfb8", + "sha256:ae3476e934b9d714aa8000d2e4c01eb2590eee10b9d8cd03e7983ad65dfbfcba", + "sha256:b0341e6d9a0c0e3cdc65857ef518bb05b410dbd70d749a0d33ac0f39e81a4258", + "sha256:b40d1bf6e6f74f7c0a567a9e5e778bbd4699d1d3d2c0fe46f4b717eef9e96b95", + "sha256:b5c4804e4039f487e942c13381e6c27b4b4e66066d94ef1fae3f6ba8b953f383", + "sha256:b5d6a6c9602fd4598fa07e0389e19fe199ae96449008d8304bf5d47cb745462e", + "sha256:b5f1ac7359e17efe0b6e5fec21de34145caef22b260e978336f325d5c84e6938", + "sha256:c0167540094838ee9093ef6cc2c69d0074bbf84a432b4995835e8e5a0d984374", + "sha256:c180ac742a083e109c1a18151f4dd8675f32679985a1c750d2ff806796165b55", + "sha256:c73df5b6e8fabe2ddb74876fb82d9dd44cbace0ca12e8861ce9155ad3c886139", + "sha256:c7e177c619342e407415d4f35dec63d2d134d951e24b5166afcdfd1362828e17", + "sha256:cbad927ea8ed814622305d842c93412cb47bd39a496ed0f96bfd42b922b4a217", + "sha256:cc353841428d56b683a123a813e6a686e07026d6b1c5757970a877195f880c2d", + "sha256:cc7c92c1baa629cb03ecb0c3d12564f172218fb1739f54bf5f3881844daadc6d", + "sha256:cc7d768260f4ba4ea01741c1b5fe3d3a6c70eb91c87f4c8761bbcce5181beafe", + "sha256:d0eea830b591dbc68e030c86a9569826145df485b2b4554874b07fea1275a199", + "sha256:d216e5d9b8749563c7f2c6f7a0831057ec844c68b4c11cb10fc62d4fd373c26d", + "sha256:d401f07261dc5aa36c2e4efc308548f6ae943bfff20fcadb0a07517a26b196d8", + "sha256:d6324274b4e0e2fa1b3eccb25997b1c9ed134ff61d296448ab8269f5ac068c4c", + "sha256:d8a8b74d843c2638f3864a17d97a4acda58e40d3e44b6303b8cc3d3c44ae2d29", + "sha256:d9b6b28a57feb51605d6ae5e61a9044a31742db557a3b851a74c13bc61de5172", + "sha256:de599af166970d6a61accde358ec9ded821234cbbc8c6413acfec06056b8e860", + "sha256:e594b22688d5747b06e957f1ef822060cb5cb35b493066e33ceac0cf882188b7", + "sha256:e5b078134f48552c4d9527db2f7da0b5359abd49393cdf9794017baec7506170", + "sha256:eb6dce402734575e1a8cc0bb1509afca508a400a57ce13d306ea2c663bad1138", + "sha256:f1790a4b1e8e8e028c391175433b9c8122c39b46e1663228158e61e6f915bf06", + "sha256:f5efe0661b9fcd6246f27957f6ae1c0eb29bc60552820f01e970b4996e016004", + "sha256:f9cbfbc5faca235fbdf531b93aa0f9f005ec7d267d9d738761a4d42b744ea159", + "sha256:fbea1751729afe607d84acfd01efd95e3b31db148a181a441984ce9b3d3469da", + "sha256:fca4b4307ebe9c3ec77a084da3a9d1999d164693d16492ca2b64594340999988", + "sha256:ff5c6771c7e3511a06555afa317879b7db8d640137ba55d6ab0d0c50425cab75" ], "markers": "python_version >= '3.9'", - "version": "==1.16.0" + "version": "==1.17.1" } }, "develop": { @@ -1863,12 +1837,12 @@ }, "boto3": { "hashes": [ - "sha256:b660c649a27a6b47a34f6f858f5bd7c3b0a798a16dec8dda7cbebeee80fd1f60", - "sha256:ddecb27f5699ca9f97711c52b6c0652c2e63bf6c2bfbc13b819b4f523b4d30ff" + "sha256:81f4d8d6eff3e26b82cabd42eda816cfac9482821fdef353f18d2ba2f6e75f2d", + "sha256:8f8ff97cb9cb2e1ec7374209d0c09c1926b75604d6464c34bafaffd6d6cf0529" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.49" + "version": "==1.35.59" }, "boto3-stubs": { "extras": [ @@ -1876,27 +1850,27 @@ "s3" ], "hashes": [ - "sha256:2a2e08ba2383df6f478127f9754a02a590131249b40c59d7c6ca9fce76906785", - "sha256:daad87dcff906f7c09dde4ef3c252e2c47b6e1e8e669f5a8311658ac0d1182c0" + "sha256:65b52800dc7ff1579c1d9f46d1176f4e6e4a883483a4f5b338bde114f24c8a5c", + "sha256:984e705d354cb969645b8f6384a4f167620afc239e52f998a7287fd7c9bb0b68" ], "markers": "python_version >= '3.8'", - "version": "==1.35.49" + "version": "==1.35.59" }, "botocore": { "hashes": [ - "sha256:07d0c1325fdbfa49a4a054413dbdeab0a6030449b2aa66099241af2dac48afd8", - "sha256:aed4d3643afd702920792b68fbe712a8c3847993820d1048cd238a6469354da1" + "sha256:378f53037d817bed2c04a006b7319745e664030182211429c924647273b29bc9", + "sha256:ddccfc39a0a55ac0321191a36d29c2ea9be2c96ceefb3928dd3c91c79c494d50" ], "markers": "python_version >= '3.8'", - "version": "==1.35.49" + "version": "==1.35.60" }, "botocore-stubs": { "hashes": [ - "sha256:367ce067e003de7e9b76320f551ba4fc8369a4b7ef10210f6071d3593fea2605", - "sha256:c5006e31d77e290eca215e6a71292ea7b029b54900310ed0f87da8e844f1db38" + "sha256:52b414b326df21a094ccd0c7840b5e82c80fa59a4decbb4594647ba2a250b3fc", + "sha256:ae2b94b099d43204db6d056b763fff47ebc5974fab24420e7f4a01526a590048" ], "markers": "python_version >= '3.8'", - "version": "==1.35.49" + "version": "==1.35.60" }, "certifi": { "hashes": [ @@ -2096,36 +2070,36 @@ }, "debugpy": { "hashes": [ - "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" + "sha256:09cc7b162586ea2171eea055985da2702b0723f6f907a423c9b2da5996ad67ba", + "sha256:0cc94186340be87b9ac5a707184ec8f36547fb66636d1029ff4f1cc020e53996", + "sha256:143ef07940aeb8e7316de48f5ed9447644da5203726fca378f3a6952a50a9eae", + "sha256:19ffbd84e757a6ca0113574d1bf5a2298b3947320a3e9d7d8dc3377f02d9f864", + "sha256:26b461123a030e82602a750fb24d7801776aa81cd78404e54ab60e8b5fecdad5", + "sha256:3a9c013077a3a0000e83d97cf9cc9328d2b0bbb31f56b0e99ea3662d29d7a6a2", + "sha256:4b93e4832fd4a759a0c465c967214ed0c8a6e8914bced63a28ddb0dd8c5f078b", + "sha256:535f4fb1c024ddca5913bb0eb17880c8f24ba28aa2c225059db145ee557035e9", + "sha256:53709d4ec586b525724819dc6af1a7703502f7e06f34ded7157f7b1f963bb854", + "sha256:5c0e5a38c7f9b481bf31277d2f74d2109292179081f11108e668195ef926c0f9", + "sha256:5c6e885dbf12015aed73770f29dec7023cb310d0dc2ba8bfbeb5c8e43f80edc9", + "sha256:64674e95916e53c2e9540a056e5f489e0ad4872645399d778f7c598eacb7b7f9", + "sha256:705cd123a773d184860ed8dae99becd879dfec361098edbefb5fc0d3683eb804", + "sha256:890fd16803f50aa9cb1a9b9b25b5ec321656dd6b78157c74283de241993d086f", + "sha256:90244598214bbe704aa47556ec591d2f9869ff9e042e301a2859c57106649add", + "sha256:a6531d952b565b7cb2fbd1ef5df3d333cf160b44f37547a4e7cf73666aca5d8d", + "sha256:b01f4a5e5c5fb1d34f4ccba99a20ed01eabc45a4684f4948b5db17a319dfb23f", + "sha256:c399023146e40ae373753a58d1be0a98bf6397fadc737b97ad612886b53df318", + "sha256:d4483836da2a533f4b1454dffc9f668096ac0433de855f0c22cdce8c9f7e10c4", + "sha256:e59b1607c51b71545cb3496876544f7186a7a27c00b436a62f285603cc68d1c6", + "sha256:e6355385db85cbd666be703a96ab7351bc9e6c61d694893206f8001e22aee091", + "sha256:ec684553aba5b4066d4de510859922419febc710df7bba04fe9e7ef3de15d34f", + "sha256:eea8821d998ebeb02f0625dd0d76839ddde8cbf8152ebbe289dd7acf2cdc6b98", + "sha256:f3cbf1833e644a3100eadb6120f25be8a532035e8245584c4f7532937edc652a", + "sha256:f95651bdcbfd3b27a408869a53fbefcc2bcae13b694daee5f1365b1b83a00113", + "sha256:ffe94dd5e9a6739a75f0b85316dc185560db3e97afa6b215628d1b6a17561cb2" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.8.7" + "version": "==1.8.8" }, "decorator": { "hashes": [ @@ -2144,12 +2118,12 @@ }, "django": { "hashes": [ - "sha256:bd7376f90c99f96b643722eee676498706c9fd7dc759f55ebfaf2c08ebcdf4f0", - "sha256:f11aa87ad8d5617171e3f77e1d5d16f004b79a2cf5d2e1d2b97a6a1f8e9ba5ed" + "sha256:8b38a9a12da3ae00cb0ba72da985ec4b14de6345046b1e174b1fd7254398f818", + "sha256:c0fa0e619c39325a169208caef234f90baa925227032ad3f44842ba14d75234a" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==5.1.2" + "version": "==5.1.3" }, "django-coverage-plugin": { "hashes": [ @@ -2221,11 +2195,11 @@ }, "faker": { "hashes": [ - "sha256:4f7f133560b9d4d2a915581f4ba86f9a6a83421b89e911f36c4c96cff58135a5", - "sha256:93e8b70813f76d05d98951154681180cb795cfbcff3eced7680d963bcc0da2a9" + "sha256:aac536ba04e6b7beb2332c67df78485fc29c1880ff723beac6d1efd45e2f10f5", + "sha256:c77522577863c264bdc9dad3a2a750ad3f7ee43ff8185072e482992288898814" ], "markers": "python_version >= '3.8'", - "version": "==30.8.1" + "version": "==32.1.0" }, "filelock": { "hashes": [ @@ -2254,11 +2228,11 @@ }, "identify": { "hashes": [ - "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", - "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98" + "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3", + "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd" ], - "markers": "python_version >= '3.8'", - "version": "==2.6.1" + "markers": "python_version >= '3.9'", + "version": "==2.6.2" }, "idna": { "hashes": [ @@ -2270,20 +2244,20 @@ }, "ipython": { "hashes": [ - "sha256:0d0d15ca1e01faeb868ef56bc7ee5a0de5bd66885735682e8a322ae289a13d1a", - "sha256:530ef1e7bb693724d3cdc37287c80b07ad9b25986c007a53aa1857272dac3f35" + "sha256:0188a1bd83267192123ccea7f4a8ed0a78910535dbaa3f37671dca76ebd429c8", + "sha256:40b60e15b22591450eef73e40a027cf77bd652e757523eebc5bd7c7c498290eb" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==8.28.0" + "version": "==8.29.0" }, "jedi": { "hashes": [ - "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd", - "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0" + "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", + "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9" ], "markers": "python_version >= '3.6'", - "version": "==0.19.1" + "version": "==0.19.2" }, "jmespath": { "hashes": [ @@ -2370,48 +2344,49 @@ }, "mypy": { "hashes": [ - "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" + "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc", + "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", + "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f", + "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74", + "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a", + "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", + "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", + "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", + "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", + "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", + "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d", + "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6", + "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", + "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", + "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", + "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", + "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a", + "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc", + "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7", + "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb", + "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", + "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732", + "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80", + "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", + "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", + "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", + "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", + "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24", + "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", + "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b", + "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372", + "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.12.1" + "version": "==1.13.0" }, "mypy-boto3-s3": { "hashes": [ "sha256:34d19dfba400f5b9bd6b64f09eb8f8eedef60545b410a3753fe99fec0c41ba78", "sha256:f0087a3765d103b2db565cd8065ebc2b0f70f2dd4e92c132f64b8945dd869940" ], + "markers": "python_version >= '3.8'", "version": "==1.35.46" }, "mypy-extensions": { @@ -2588,28 +2563,28 @@ }, "ruff": { "hashes": [ - "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" + "sha256:10ebce7696afe4644e8c1a23b3cf8c0f2193a310c18387c06e583ae9ef284de2", + "sha256:1713e2c5545863cdbfe2cbce21f69ffaf37b813bfd1fb3b90dc9a6f1963f5a8c", + "sha256:34f2339dc22687ec7e7002792d1f50712bf84a13d5152e75712ac08be565d344", + "sha256:37d0b619546103274e7f62643d14e1adcbccb242efda4e4bdb9544d7764782e9", + "sha256:3f36d56326b3aef8eeee150b700e519880d1aab92f471eefdef656fd57492aa2", + "sha256:44eb93c2499a169d49fafd07bc62ac89b1bc800b197e50ff4633aed212569299", + "sha256:4ba81a5f0c5478aa61674c5a2194de8b02652f17addf8dfc40c8937e6e7d79fc", + "sha256:588a9ff2fecf01025ed065fe28809cd5a53b43505f48b69a1ac7707b1b7e4088", + "sha256:5d024301109a0007b78d57ab0ba190087b43dce852e552734ebf0b0b85e4fb16", + "sha256:5d59f0c3ee4d1a6787614e7135b72e21024875266101142a09a61439cb6e38a5", + "sha256:61b46049d6edc0e4317fb14b33bd693245281a3007288b68a3f5b74a22a0746d", + "sha256:6b6224af8b5e09772c2ecb8dc9f3f344c1aa48201c7f07e7315367f6dd90ac29", + "sha256:6d0242ce53f3a576c35ee32d907475a8d569944c0407f91d207c8af5be5dae4e", + "sha256:7f3eff9961b5d2644bcf1616c606e93baa2d6b349e8aa8b035f654df252c8c67", + "sha256:b8963cab06d130c4df2fd52c84e9f10d297826d2e8169ae0c798b6221be1d1d2", + "sha256:c50f95a82b94421c964fae4c27c0242890a20fe67d203d127e84fbb8013855f5", + "sha256:e1d1ba2e40b6e71a61b063354d04be669ab0d39c352461f3d789cac68b54a313", + "sha256:fb397332a1879b9764a3455a0bb1087bda876c2db8aca3a3cbb67b3dbce8cda0" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.7.0" + "version": "==0.7.3" }, "s3transfer": { "hashes": [ @@ -2629,11 +2604,11 @@ }, "sqlparse": { "hashes": [ - "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", - "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e" + "sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f", + "sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e" ], "markers": "python_version >= '3.8'", - "version": "==0.5.1" + "version": "==0.5.2" }, "stack-data": { "hashes": [ @@ -2709,48 +2684,48 @@ }, "virtualenv": { "hashes": [ - "sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2", - "sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655" + "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba", + "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4" ], "markers": "python_version >= '3.8'", - "version": "==20.27.0" + "version": "==20.27.1" }, "watchdog": { "hashes": [ - "sha256:0f9332243355643d567697c3e3fa07330a1d1abf981611654a1f2bf2175612b7", - "sha256:1021223c08ba8d2d38d71ec1704496471ffd7be42cfb26b87cd5059323a389a1", - "sha256:108f42a7f0345042a854d4d0ad0834b741d421330d5f575b81cb27b883500176", - "sha256:1e9679245e3ea6498494b3028b90c7b25dbb2abe65c7d07423ecfc2d6218ff7c", - "sha256:223160bb359281bb8e31c8f1068bf71a6b16a8ad3d9524ca6f523ac666bb6a1e", - "sha256:26dd201857d702bdf9d78c273cafcab5871dd29343748524695cecffa44a8d97", - "sha256:294b7a598974b8e2c6123d19ef15de9abcd282b0fbbdbc4d23dfa812959a9e05", - "sha256:349c9488e1d85d0a58e8cb14222d2c51cbc801ce11ac3936ab4c3af986536926", - "sha256:49f4d36cb315c25ea0d946e018c01bb028048023b9e103d3d3943f58e109dd45", - "sha256:53a3f10b62c2d569e260f96e8d966463dec1a50fa4f1b22aec69e3f91025060e", - "sha256:53adf73dcdc0ef04f7735066b4a57a4cd3e49ef135daae41d77395f0b5b692cb", - "sha256:560135542c91eaa74247a2e8430cf83c4342b29e8ad4f520ae14f0c8a19cfb5b", - "sha256:720ef9d3a4f9ca575a780af283c8fd3a0674b307651c1976714745090da5a9e8", - "sha256:752fb40efc7cc8d88ebc332b8f4bcbe2b5cc7e881bccfeb8e25054c00c994ee3", - "sha256:78864cc8f23dbee55be34cc1494632a7ba30263951b5b2e8fc8286b95845f82c", - "sha256:85527b882f3facda0579bce9d743ff7f10c3e1e0db0a0d0e28170a7d0e5ce2ea", - "sha256:90a67d7857adb1d985aca232cc9905dd5bc4803ed85cfcdcfcf707e52049eda7", - "sha256:91b522adc25614cdeaf91f7897800b82c13b4b8ac68a42ca959f992f6990c490", - "sha256:9413384f26b5d050b6978e6fcd0c1e7f0539be7a4f1a885061473c5deaa57221", - "sha256:94d11b07c64f63f49876e0ab8042ae034674c8653bfcdaa8c4b32e71cfff87e8", - "sha256:950f531ec6e03696a2414b6308f5c6ff9dab7821a768c9d5788b1314e9a46ca7", - "sha256:a2e8f3f955d68471fa37b0e3add18500790d129cc7efe89971b8a4cc6fdeb0b2", - "sha256:ae6deb336cba5d71476caa029ceb6e88047fc1dc74b62b7c4012639c0b563906", - "sha256:b8ca4d854adcf480bdfd80f46fdd6fb49f91dd020ae11c89b3a79e19454ec627", - "sha256:c66f80ee5b602a9c7ab66e3c9f36026590a0902db3aea414d59a2f55188c1f49", - "sha256:d52db5beb5e476e6853da2e2d24dbbbed6797b449c8bf7ea118a4ee0d2c9040e", - "sha256:dd021efa85970bd4824acacbb922066159d0f9e546389a4743d56919b6758b91", - "sha256:e25adddab85f674acac303cf1f5835951345a56c5f7f582987d266679979c75b", - "sha256:f00b4cf737f568be9665563347a910f8bdc76f88c2970121c86243c8cfdf90e9", - "sha256:f01f4a3565a387080dc49bdd1fefe4ecc77f894991b88ef927edbfa45eb10818" + "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", + "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", + "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", + "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", + "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", + "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", + "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", + "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", + "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", + "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", + "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", + "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", + "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", + "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", + "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", + "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", + "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", + "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", + "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", + "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", + "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", + "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", + "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", + "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", + "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", + "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", + "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", + "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", + "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", + "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==5.0.3" + "version": "==6.0.0" }, "wcwidth": { "hashes": [ @@ -2761,12 +2736,12 @@ }, "werkzeug": { "hashes": [ - "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17", - "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d" + "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", + "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==3.0.6" + "markers": "python_version >= '3.9'", + "version": "==3.1.3" } }, "docs": { @@ -3056,11 +3031,11 @@ }, "packaging": { "hashes": [ - "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", - "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" ], "markers": "python_version >= '3.8'", - "version": "==24.1" + "version": "==24.2" }, "pygments": { "hashes": [ @@ -3155,12 +3130,12 @@ }, "sphinx": { "hashes": [ - "sha256:0cce1ddcc4fd3532cf1dd283bc7d886758362c5c1de6598696579ce96d8ffa5b", - "sha256:56173572ae6c1b9a38911786e206a110c9749116745873feae4f9ce88e59391d" + "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", + "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==8.0.2" + "version": "==8.1.3" }, "sphinx-basic-ng": { "hashes": [ diff --git a/care/users/tests/test_auth.py b/care/users/tests/test_auth.py index 912e5da010..c2015c6ddb 100644 --- a/care/users/tests/test_auth.py +++ b/care/users/tests/test_auth.py @@ -136,7 +136,12 @@ def test_forgot_password_with_valid_input(self): self.assertTrue(ResetPasswordToken.objects.filter(user=self.user).exists()) self.assertTrue(ResetPasswordToken.objects.filter(user=self.user).exists()) - @override_settings(IS_PRODUCTION=True) + @override_settings( + IS_PRODUCTION=True, + EMAIL_HOST="", + EMAIL_HOST_USER="", + EMAIL_HOST_PASSWORD="", + ) def test_forgot_password_without_email_configration(self): response = self.client.post( "/api/v1/password_reset/", diff --git a/docker-compose.pre-built.yaml b/docker-compose.pre-built.yaml index 1b4ed4cfe8..93b7072c88 100644 --- a/docker-compose.pre-built.yaml +++ b/docker-compose.pre-built.yaml @@ -4,7 +4,7 @@ services: image: "ghcr.io/ohcnetwork/care:latest" env_file: - ./docker/.prebuilt.env - entrypoint: [ "bash", "start-ecs.sh" ] + entrypoint: [ "bash", "start.sh" ] restart: unless-stopped depends_on: db: @@ -20,7 +20,7 @@ services: image: "ghcr.io/ohcnetwork/care:latest" env_file: - ./docker/.prebuilt.env - entrypoint: [ "bash", "celery_worker-ecs.sh" ] + entrypoint: [ "bash", "celery_worker.sh" ] restart: unless-stopped depends_on: db: @@ -34,7 +34,7 @@ services: image: "ghcr.io/ohcnetwork/care:latest" env_file: - ./docker/.prebuilt.env - entrypoint: [ "bash", "celery_beat-ecs.sh" ] + entrypoint: [ "bash", "celery_beat.sh" ] restart: unless-stopped depends_on: - db diff --git a/docker/dev.Dockerfile b/docker/dev.Dockerfile index 198f40d824..17d5c8df59 100644 --- a/docker/dev.Dockerfile +++ b/docker/dev.Dockerfile @@ -1,8 +1,12 @@ FROM python:3.13-slim-bookworm ARG TYPST_VERSION=0.11.0 +ARG APP_HOME=/app -ENV PATH=/venv/bin:$PATH +WORKDIR $APP_HOME + +ENV PATH=/.venv/bin:$PATH +ENV PIPENV_CACHE_DIR=/root/.cache/pip RUN apt-get update && apt-get install --no-install-recommends -y \ build-essential libjpeg-dev zlib1g-dev libgmp-dev \ @@ -27,21 +31,21 @@ RUN ARCH=$(dpkg --print-architecture) && \ rm -rf typst.tar.xz typst-${TYPST_ARCH} # use pipenv to manage virtualenv -RUN python -m venv /venv -RUN pip install pipenv==2024.2.0 +RUN pip install pipenv==2024.4.0 -COPY Pipfile Pipfile.lock ./ -RUN pipenv install --system --categories "packages dev-packages" +RUN python -m venv /.venv +COPY Pipfile Pipfile.lock $APP_HOME/ +RUN --mount=type=cache,target=/root/.cache/pip pipenv install --system --categories "packages dev-packages docs" -COPY . /app +COPY plugs/ $APP_HOME/plugs/ +COPY install_plugins.py plug_config.py $APP_HOME/ +RUN --mount=type=cache,target=/root/.cache/pip python3 $APP_HOME/install_plugins.py -RUN python3 /app/install_plugins.py +COPY . $APP_HOME/ HEALTHCHECK \ --interval=10s \ --timeout=5s \ --start-period=10s \ --retries=48 \ - CMD ["/app/scripts/healthcheck.sh"] - -WORKDIR /app + CMD ["./scripts/healthcheck.sh"] diff --git a/docker/prod.Dockerfile b/docker/prod.Dockerfile index 2aef7be6d8..b2fe64f7fe 100644 --- a/docker/prod.Dockerfile +++ b/docker/prod.Dockerfile @@ -7,18 +7,24 @@ ARG BUILD_ENVIRONMENT="production" ARG APP_VERSION="unknown" ARG ADDITIONAL_PLUGS="" +WORKDIR $APP_HOME + ENV BUILD_ENVIRONMENT=$BUILD_ENVIRONMENT ENV APP_VERSION=$APP_VERSION ENV ADDITIONAL_PLUGS=$ADDITIONAL_PLUGS ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 -ENV PATH=/venv/bin:$PATH +ENV PIPENV_VENV_IN_PROJECT=1 +ENV PIPENV_CACHE_DIR=/root/.cache/pip +ENV PATH=$APP_HOME/.venv/bin:$PATH -WORKDIR $APP_HOME # --- FROM base AS builder +RUN addgroup --system django \ + && adduser --system --ingroup django django + RUN apt-get update && apt-get install --no-install-recommends -y \ build-essential libjpeg-dev zlib1g-dev libgmp-dev libpq-dev git wget \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ @@ -40,14 +46,14 @@ RUN ARCH=$(dpkg --print-architecture) && \ rm -rf typst.tar.xz typst-${TYPST_ARCH} # use pipenv to manage virtualenv -RUN python -m venv /venv -RUN pip install pipenv==2024.2.0 +RUN pip install pipenv==2024.4.0 -COPY Pipfile Pipfile.lock $APP_HOME -RUN pipenv sync --system --categories "packages" +RUN python -m venv $APP_HOME/.venv +COPY Pipfile Pipfile.lock $APP_HOME/ +RUN pipenv install --deploy --categories "packages" COPY plugs/ $APP_HOME/plugs/ -COPY install_plugins.py plug_config.py $APP_HOME +COPY install_plugins.py plug_config.py $APP_HOME/ RUN python3 $APP_HOME/install_plugins.py # --- @@ -60,17 +66,19 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ COPY --from=builder --chmod=0755 /usr/local/bin/typst /usr/local/bin/typst -COPY --from=builder /venv /venv +COPY --from=builder --chown=django:django $APP_HOME/.venv $APP_HOME/.venv + +COPY --chmod=0755 --chown=django:django ./scripts/*.sh $APP_HOME -COPY --chmod=0755 ./scripts/*.sh $APP_HOME +COPY --chown=django:django . $APP_HOME -COPY . $APP_HOME +USER django HEALTHCHECK \ --interval=30s \ --timeout=5s \ --start-period=10s \ --retries=12 \ - CMD ["/app/healthcheck.sh"] + CMD ["./healthcheck.sh"] EXPOSE 9000 diff --git a/scripts/celery-dev.sh b/scripts/celery-dev.sh index 75bc859d71..1f827484a8 100755 --- a/scripts/celery-dev.sh +++ b/scripts/celery-dev.sh @@ -1,35 +1,13 @@ #!/bin/bash - printf "celery" > /tmp/container-role -if [ -z "${DATABASE_URL}" ]; then - export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" -fi - -postgres_ready() { -python << END -import sys - -import psycopg - -try: - psycopg.connect(conninfo="${DATABASE_URL}") -except psycopg.OperationalError as e: - print(e) - sys.exit(-1) -sys.exit(0) - -END -} +set -euo pipefail -until postgres_ready; do - >&2 echo 'Waiting for PostgreSQL to become available...' - sleep 1 -done ->&2 echo 'PostgreSQL is available' +./scripts/wait_for_db.sh +./scripts/wait_for_redis.sh python manage.py migrate --noinput -python manage.py compilemessages +python manage.py compilemessages -v 0 python manage.py load_redis_index diff --git a/scripts/celery_beat-ecs.sh b/scripts/celery_beat-ecs.sh index 6c48e1e9bb..9650b682fe 100755 --- a/scripts/celery_beat-ecs.sh +++ b/scripts/celery_beat-ecs.sh @@ -1,37 +1,3 @@ #!/bin/bash -printf "celery-beat" > /tmp/container-role - -if [ -z "${DATABASE_URL}" ]; then - export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" -fi - -postgres_ready() { -python << END -import sys - -import psycopg - -try: - psycopg.connect(conninfo="${DATABASE_URL}") -except psycopg.OperationalError as e: - print(e) - sys.exit(-1) -sys.exit(0) - -END -} - -until postgres_ready; do - >&2 echo 'Waiting for PostgreSQL to become available...' - sleep 1 -done ->&2 echo 'PostgreSQL is available' - -python manage.py migrate --noinput -python manage.py compilemessages -python manage.py load_redis_index -python manage.py load_event_types - -touch /tmp/healthy - -celery --app=config.celery_app beat --loglevel=info +echo "This script is deprecated. Use celery_beat.sh instead." +exec "$(dirname "$0")/celery_beat.sh" diff --git a/scripts/celery_beat.sh b/scripts/celery_beat.sh index 136f095f59..dd1a5f36e5 100755 --- a/scripts/celery_beat.sh +++ b/scripts/celery_beat.sh @@ -1,38 +1,21 @@ #!/bin/bash printf "celery-beat" > /tmp/container-role -if [ -z "${DATABASE_URL}" ]; then - export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" -fi - -postgres_ready() { -python << END -import sys - -import psycopg +set -euo pipefail -try: - psycopg.connect(conninfo="${DATABASE_URL}") -except psycopg.OperationalError as e: - print(e) - sys.exit(-1) -sys.exit(0) - -END -} - -until postgres_ready; do - >&2 echo 'Waiting for PostgreSQL to become available...' - sleep 1 -done ->&2 echo 'PostgreSQL is available' +./wait_for_db.sh +./wait_for_redis.sh python manage.py migrate --noinput -python manage.py compilemessages +python manage.py compilemessages -v 0 python manage.py load_redis_index python manage.py load_event_types touch /tmp/healthy export NEW_RELIC_CONFIG_FILE=/etc/newrelic.ini -newrelic-admin run-program celery --app=config.celery_app beat --loglevel=info +if [[ -f "$NEW_RELIC_CONFIG_FILE" ]]; then + newrelic-admin run-program celery --app=config.celery_app beat --loglevel=info +else + celery --app=config.celery_app beat --loglevel=info +fi diff --git a/scripts/celery_worker-ecs.sh b/scripts/celery_worker-ecs.sh index 619f7a9f13..5e41bd2c7f 100755 --- a/scripts/celery_worker-ecs.sh +++ b/scripts/celery_worker-ecs.sh @@ -1,10 +1,3 @@ #!/bin/bash -printf "celery-worker" > /tmp/container-role - -if [ -z "${DATABASE_URL}" ]; then - export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" -fi - -python manage.py collectstatic --noinput -python manage.py compilemessages -celery --app=config.celery_app worker --max-tasks-per-child=6 --loglevel=info +echo "This script is deprecated. Use celery_worker.sh instead." +exec "$(dirname "$0")/celery_worker.sh" diff --git a/scripts/celery_worker.sh b/scripts/celery_worker.sh index a7f0a887d6..6093fec4d9 100755 --- a/scripts/celery_worker.sh +++ b/scripts/celery_worker.sh @@ -1,12 +1,17 @@ #!/bin/bash printf "celery-worker" > /tmp/container-role -if [ -z "${DATABASE_URL}" ]; then - export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" -fi +set -euo pipefail +./wait_for_db.sh +./wait_for_redis.sh -export NEW_RELIC_CONFIG_FILE=/etc/newrelic.ini python manage.py collectstatic --noinput -python manage.py compilemessages -newrelic-admin run-program celery --app=config.celery_app worker --max-tasks-per-child=6 --loglevel=info +python manage.py compilemessages -v 0 + +export NEW_RELIC_CONFIG_FILE=/etc/newrelic.ini +if [[ -f "$NEW_RELIC_CONFIG_FILE" ]]; then + newrelic-admin run-program celery --app=config.celery_app worker --max-tasks-per-child=6 --loglevel=info +else + celery --app=config.celery_app worker --max-tasks-per-child=6 --loglevel=info +fi diff --git a/scripts/start-dev.sh b/scripts/start-dev.sh index 39abcfa4a0..1071bc6f67 100755 --- a/scripts/start-dev.sh +++ b/scripts/start-dev.sh @@ -1,13 +1,14 @@ #!/usr/bin/env bash -set -euo pipefail - printf "api" > /tmp/container-role -cd /app +set -euo pipefail + +./scripts/wait_for_db.sh +./scripts/wait_for_redis.sh echo "running collectstatic..." python manage.py collectstatic --noinput -python manage.py compilemessages +python manage.py compilemessages -v 0 echo "starting server..." if [[ "${DJANGO_DEBUG,,}" == "true" ]]; then diff --git a/scripts/start-ecs.sh b/scripts/start-ecs.sh index c360864d9b..7a88558e16 100755 --- a/scripts/start-ecs.sh +++ b/scripts/start-ecs.sh @@ -1,36 +1,3 @@ #!/bin/bash - -set -o errexit -set -o pipefail -set -o nounset - -printf "api" > /tmp/container-role - -if [ -z "${DATABASE_URL}" ]; then - export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" -fi - -postgres_ready() { -python << END -import sys - -import psycopg - -try: - psycopg.connect(conninfo="${DATABASE_URL}") -except psycopg.OperationalError: - sys.exit(-1) -sys.exit(0) - -END -} - -until postgres_ready; do - >&2 echo 'Waiting for PostgreSQL to become available...' - sleep 1 -done ->&2 echo 'PostgreSQL is available' - -python manage.py collectstatic --noinput -python manage.py compilemessages -gunicorn config.wsgi:application --bind 0.0.0.0:9000 --chdir=/app --workers 2 +echo "This script is deprecated. Use start.sh instead." +exec "$(dirname "$0")/start.sh" diff --git a/scripts/start.sh b/scripts/start.sh index 7d119b375a..7de824a3a7 100755 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -1,38 +1,17 @@ #!/bin/bash - -set -o errexit -set -o pipefail -set -o nounset - printf "api" > /tmp/container-role -if [ -z "${DATABASE_URL}" ]; then - export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" -fi - -postgres_ready() { -python << END -import sys +set -euo pipefail -import psycopg - -try: - psycopg.connect(conninfo="${DATABASE_URL}") -except psycopg.OperationalError: - sys.exit(-1) -sys.exit(0) - -END -} -until postgres_ready; do - >&2 echo 'Waiting for PostgreSQL to become available...' - sleep 1 -done ->&2 echo 'PostgreSQL is available' +./wait_for_db.sh +./wait_for_redis.sh python manage.py collectstatic --noinput +python manage.py compilemessages -v 0 export NEW_RELIC_CONFIG_FILE=/etc/newrelic.ini - -python manage.py compilemessages -newrelic-admin run-program gunicorn config.wsgi:application --bind 0.0.0.0:9000 --chdir=/app +if [[ -f "$NEW_RELIC_CONFIG_FILE" ]]; then + newrelic-admin run-program gunicorn config.wsgi:application --bind 0.0.0.0:9000 --chdir=/app +else + gunicorn config.wsgi:application --bind 0.0.0.0:9000 --chdir=/app --workers 2 +fi diff --git a/scripts/wait_for_db.sh b/scripts/wait_for_db.sh new file mode 100755 index 0000000000..6babf993cd --- /dev/null +++ b/scripts/wait_for_db.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +postgres_ready() { +python << END +import sys +import psycopg +try: + psycopg.connect(conninfo="${DATABASE_URL}") +except psycopg.OperationalError: + sys.exit(-1) +sys.exit(0) +END +} + +MAX_RETRIES=30 +RETRY_COUNT=0 +until postgres_ready; do + if [ "$RETRY_COUNT" -ge "$MAX_RETRIES" ]; then + >&2 echo 'Failed to connect to PostgreSQL after 30 attempts. Exiting.' + exit 1 + fi + >&2 echo 'Waiting for PostgreSQL to become available...' + sleep 1 + RETRY_COUNT=$((RETRY_COUNT + 1)) +done +>&2 echo 'PostgreSQL is available' diff --git a/scripts/wait_for_redis.sh b/scripts/wait_for_redis.sh new file mode 100755 index 0000000000..6e17d3c9e9 --- /dev/null +++ b/scripts/wait_for_redis.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +redis_ready() { +python << END +import sys +import redis +try: + redis_client = redis.Redis.from_url("${REDIS_URL}") + redis_client.ping() +except (redis.exceptions.ConnectionError, redis.exceptions.ResponseError): + sys.exit(-1) +sys.exit(0) +END +} + +MAX_RETRIES=30 +RETRY_COUNT=0 +until redis_ready; do + if [ "$RETRY_COUNT" -ge "$MAX_RETRIES" ]; then + >&2 echo 'Failed to connect to Redis after 30 attempts. Exiting.' + exit 1 + fi + >&2 echo 'Waiting for Redis to become available...' + sleep 1 + RETRY_COUNT=$((RETRY_COUNT + 1)) +done +>&2 echo 'Redis is available' From 3ab5d9066244e535a39200e77306a89265d7c39f Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Mon, 25 Nov 2024 20:30:47 +0530 Subject: [PATCH 15/19] add tests and validation for filterset (#2588) add tests and validation for filterset (#2588) --- care/facility/api/viewsets/patient.py | 40 +++++--- care/facility/tests/test_patient_api.py | 129 ++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 13 deletions(-) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 027a6c8241..6f30ab8c25 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -3,6 +3,7 @@ from django.conf import settings from django.contrib.postgres.search import TrigramSimilarity +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import ( Case, @@ -76,7 +77,7 @@ NewDischargeReasonEnum, ) from care.facility.models.patient_consultation import PatientConsultation -from care.users.models import User +from care.users.models import GENDER_CHOICES, User from care.utils.cache.cache_allowed_facilities import get_accessible_facilities from care.utils.exports.mixins import CSVExportViewSetMixin from care.utils.filters.choicefilter import CareChoiceFilter @@ -107,14 +108,22 @@ class PatientFilterSet(filters.FilterSet): phone_number = filters.CharFilter(field_name="phone_number") emergency_phone_number = filters.CharFilter(field_name="emergency_phone_number") allow_transfer = filters.BooleanFilter(field_name="allow_transfer") - name = filters.CharFilter(field_name="name", lookup_expr="icontains") + name = filters.CharFilter( + field_name="name", lookup_expr="icontains", max_length=200 + ) patient_no = filters.CharFilter( - field_name=f"{last_consultation_field}__patient_no", lookup_expr="iexact" + field_name=f"{last_consultation_field}__patient_no", + lookup_expr="iexact", + max_length=100, + ) + gender = filters.ChoiceFilter(field_name="gender", choices=GENDER_CHOICES) + age = filters.NumberFilter(field_name="age", validators=[MinValueValidator(0)]) + age_min = filters.NumberFilter( + field_name="age", lookup_expr="gte", validators=[MinValueValidator(0)] + ) + age_max = filters.NumberFilter( + field_name="age", lookup_expr="lte", validators=[MinValueValidator(0)] ) - gender = filters.NumberFilter(field_name="gender") - age = filters.NumberFilter(field_name="age") - age_min = filters.NumberFilter(field_name="age", lookup_expr="gte") - age_max = filters.NumberFilter(field_name="age", lookup_expr="lte") deprecated_covid_category = filters.ChoiceFilter( field_name=f"{last_consultation_field}__deprecated_covid_category", choices=COVID_CATEGORY_CHOICES, @@ -141,7 +150,7 @@ def filter_by_category(self, queryset, name, value): created_date = filters.DateFromToRangeFilter(field_name="created_date") modified_date = filters.DateFromToRangeFilter(field_name="modified_date") - srf_id = filters.CharFilter(field_name="srf_id") + srf_id = filters.CharFilter(field_name="srf_id", max_length=200) is_declared_positive = filters.BooleanFilter(field_name="is_declared_positive") date_declared_positive = filters.DateFromToRangeFilter( field_name="date_declared_positive" @@ -159,14 +168,16 @@ def filter_by_category(self, queryset, name, value): # Location Based Filtering district = filters.NumberFilter(field_name="district__id") district_name = filters.CharFilter( - field_name="district__name", lookup_expr="icontains" + field_name="district__name", lookup_expr="icontains", max_length=255 ) local_body = filters.NumberFilter(field_name="local_body__id") local_body_name = filters.CharFilter( - field_name="local_body__name", lookup_expr="icontains" + field_name="local_body__name", lookup_expr="icontains", max_length=255 ) state = filters.NumberFilter(field_name="state__id") - state_name = filters.CharFilter(field_name="state__name", lookup_expr="icontains") + state_name = filters.CharFilter( + field_name="state__name", lookup_expr="icontains", max_length=255 + ) # Consultation Fields is_kasp = filters.BooleanFilter(field_name=f"{last_consultation_field}__is_kasp") last_consultation_kasp_enabled_date = filters.DateFromToRangeFilter( @@ -225,9 +236,12 @@ def filter_by_bed_type(self, queryset, name, value): ) # Vaccination Filters - covin_id = filters.CharFilter(field_name="covin_id") + covin_id = filters.CharFilter(field_name="covin_id", max_length=15) is_vaccinated = filters.BooleanFilter(field_name="is_vaccinated") - number_of_doses = filters.NumberFilter(field_name="number_of_doses") + number_of_doses = filters.NumberFilter( + field_name="number_of_doses", + validators=[MinValueValidator(0), MaxValueValidator(3)], + ) # Permission Filters assigned_to = filters.NumberFilter(field_name="assigned_to") # Other Filters diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index 37accc7c2c..6046435ddb 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -728,6 +728,135 @@ def test_filter_by_has_consents(self): self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertEqual(res.json()["count"], 3) + def test_filter_by_invalid_params(self): + self.client.force_authenticate(user=self.user) + + # name length > 200 words + invalid_name_param = "a" * 201 + res = self.client.get(self.get_base_url() + f"?name={invalid_name_param}") + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + "Ensure this value has at most 200 characters (it has 201).", + res.json()["name"], + ) + + # invalid gender choice + invalid_gender = 4 + res = self.client.get(self.get_base_url() + f"?gender={invalid_gender}") + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + "Select a valid choice. 4 is not one of the available choices.", + res.json()["gender"], + ) + + # invalid value for age, age max , age min filter (i.e <0) + invalid_age = -2 + res = self.client.get(self.get_base_url() + f"?age={invalid_age}") + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + "Ensure this value is greater than or equal to 0.", res.json()["age"] + ) + + invalid_min_age = -2 + res = self.client.get(self.get_base_url() + f"?age_min={invalid_min_age}") + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + "Ensure this value is greater than or equal to 0.", res.json()["age_min"] + ) + + invalid_max_age = -2 + res = self.client.get(self.get_base_url() + f"?age_max={invalid_max_age}") + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + "Ensure this value is greater than or equal to 0.", res.json()["age_max"] + ) + + # invalid number_of_doses param >3 or <0 + invalid_number_of_doses = -2 + res = self.client.get( + self.get_base_url() + f"?number_of_doses={invalid_number_of_doses}" + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + "Ensure this value is greater than or equal to 0.", + res.json()["number_of_doses"], + ) + + invalid_number_of_doses = 4 + res = self.client.get( + self.get_base_url() + f"?number_of_doses={invalid_number_of_doses}" + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + "Ensure this value is less than or equal to 3.", + res.json()["number_of_doses"], + ) + + # invalid srf id length > 200 words + invalid_srf_param = "a" * 201 + res = self.client.get(self.get_base_url() + f"?srf_id={invalid_srf_param}") + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + "Ensure this value has at most 200 characters (it has 201).", + res.json()["srf_id"], + ) + + # invalid district_name length > 255 words + invalid_district_name_param = "a" * 256 + res = self.client.get( + self.get_base_url() + f"?district_name={invalid_district_name_param}" + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + "Ensure this value has at most 255 characters (it has 256).", + res.json()["district_name"], + ) + + # invalid local_body_name length > 255 words + invalid_local_body_name_param = "a" * 256 + res = self.client.get( + self.get_base_url() + f"?local_body_name={invalid_local_body_name_param}" + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + "Ensure this value has at most 255 characters (it has 256).", + res.json()["local_body_name"], + ) + + # invalid state_name length > 255 words + invalid_state_name_param = "a" * 256 + res = self.client.get( + self.get_base_url() + f"?state_name={invalid_state_name_param}" + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + "Ensure this value has at most 255 characters (it has 256).", + res.json()["state_name"], + ) + + # invalid patient no value > 100 + invalid_patient_no_param = "A" * 101 + res = self.client.get( + self.get_base_url() + f"?patient_no={invalid_patient_no_param}" + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + "Ensure this value has at most 100 characters (it has 101).", + res.json()["patient_no"], + ) + + def test_invalid_covin_id_param(self): + self.client.force_authenticate(user=self.user) + + # Test invalid covin_id length > 15 characters + invalid_covin_id = "A" * 16 + res = self.client.get(self.get_base_url() + f"?covin_id={invalid_covin_id}") + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + "Ensure this value has at most 15 characters (it has 16).", + res.json()["covin_id"], + ) + class DischargePatientFilterTestCase(TestUtils, APITestCase): @classmethod From f0f0c8b5a25128c7cd1a23dd49f328ab1729bcbd Mon Sep 17 00:00:00 2001 From: Muneer <103816437+dumbstertruck3@users.noreply.github.com> Date: Mon, 25 Nov 2024 20:31:57 +0530 Subject: [PATCH 16/19] Add scripts and docs to configure docker backups (#2534) Add scripts and docs to configure docker backups (#2534) --- .env.example | 2 + CONTRIBUTING.md | 6 +- README.md | 1 + docker-compose.yaml | 1 + docs/databases/backup.rst | 121 ++++++++++++++++++++++++++++++++++++++ scripts/backup.sh | 34 +++++++++++ 6 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 docs/databases/backup.rst create mode 100755 scripts/backup.sh diff --git a/.env.example b/.env.example index 057f853951..186c26419e 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,8 @@ POSTGRES_HOST=db POSTGRES_DB=care POSTGRES_PORT=5432 DATABASE_URL=postgres://postgres:postgres@localhost:5433/care +BACKUP_DIR="./care-backups" +DB_BACKUP_RETENTION_PERIOD=7 REDIS_URL=redis://localhost:6380 CELERY_BROKER_URL=redis://localhost:6380/0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e32e20002a..6854688469 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ Any support is welcome. You could help by writing documentation, pull-requests, ### Getting Started -An issue wih the [good first](https://github.com/ohcnetwork/care/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) or [help wanted](https://github.com/ohcnetwork/care/issues?q=is%3Aissue+sort%3Aupdated-desc+label%3A%22help+wanted%22+is%3Aopen) label might be a good place to start with. +An issue with the [good first](https://github.com/ohcnetwork/care/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22) or [help wanted](https://github.com/ohcnetwork/care/issues?q=is%3Aissue+sort%3Aupdated-desc+label%3A%22help+wanted%22+is%3Aopen) label might be a good place to start with. ### Setting up the development environment @@ -34,11 +34,13 @@ Make sure you have docker and docker-compose installed. Then run: make build ``` + + #### Using Virtualenv Make sure you have Postgres and Redis installed on your system. -##### Setting up postgtres for the first time +##### Setting up postgres for the first time ```bash sudo -u postgres psql diff --git a/README.md b/README.md index 38d0df9fb0..8aeaf21375 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ make load-dummy-data Prebuilt docker images for server deployments are available on [ghcr](https://github.com/ohcnetwork/care/pkgs/container/care) +For backup and restore use [this](/docs/databases/backup.rst) documentation. ## Contributing We welcome contributions from everyone. Please read our [contributing guidelines](./CONTRIBUTING.md) to get started. diff --git a/docker-compose.yaml b/docker-compose.yaml index b361cbdd8b..25b651fbd6 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,6 +11,7 @@ services: - ./docker/.prebuilt.env volumes: - postgres-data:/var/lib/postgresql/data + - ${BACKUP_DIR:-./care-backups}:/backups ports: - "5433:5432" diff --git a/docs/databases/backup.rst b/docs/databases/backup.rst new file mode 100644 index 0000000000..e0b7018003 --- /dev/null +++ b/docs/databases/backup.rst @@ -0,0 +1,121 @@ +Docker database backup +====================== + +This page explains how to automate the backup process of a Docker database on a daily basis and restore the backup snapshot created by the `backup script <../../scripts/backup.sh>`_. + + Note: This documentation assumes that you are using a Linux-based system. +------------------------------------------------------------------------------- + +Here's how the script works +--------------------------- + +The script automates the process of creating PostgreSQL database backups from a Docker container. It generates a backup file(``.dump``) using the pg_dump utility in PostgreSQL and stores these files in the path configured in ``$BACKUP_DIR`` environment variable which is binded to ``/backups`` in the docker container. Backup files older than ``$DB_BACKUP_RETENTION_PERIOD`` days are deleted when the script is executed by default it is set to 7 days. The backup file is saved with the name ``care_backup_%Y%m%d%H%M%S.sql``. + +Set up a cronjob +---------------- + +Backup your database running on docker automatically everyday by initiating a cronjob. + +Install the package +~~~~~~~~~~~~~~~~~~~ + +For a fedora based system: + +.. code:: bash + + sudo dnf install crond + +For a debian based system: + +.. code:: bash + + sudo apt install cron + +Automate the cronjob +~~~~~~~~~~~~~~~~~~~~ + Note: Make sure you are inside the care directory at the time of executing the following. +------------------------------------------------------------------------------- + +Open up a crontab: + +.. code:: bash + + crontab -e + +Add the cronjob: + +.. code:: bash + + 0 0 * * * "/scripts/backup.sh" + +List the cron jobs +~~~~~~~~~~~~~~~~~~ + +.. code:: bash + + crontab -l + +Check the status of cron +~~~~~~~~~~~~~~~~~~~~~~~~ + +For a fedora based os: + +.. code:: bash + + sudo systemctl status crond + +For a debian based os: + +.. code:: bash + + sudo systemctl status cron + +Verify the cron job +~~~~~~~~~~~~~~~~~ +To verify the cron job is working: + +1. Check the system logs for cron activity, which is usually somewhere in + + .. code:: bash + + /var/log/ + +2. Monitor the backup directory for new files after the scheduled time + +Restoration of the Database +=========================== + +We are basically deleting the container's existing database and creating a new database with the same name. Then we will use ``pg_restore`` to restore the database. Run the following commands in your terminal. + + Make sure you have stopped all the containers except the db before proceeding. And be inside the care directory at the time of executing the following. +------------------------------------------------------------------------------ + +Delete the existing database: + +.. code:: bash + + docker exec -it $(docker ps --format '{{.Names}}' | grep 'care-db') psql -U postgres -c "DROP DATABASE IF EXISTS care;" + +Create the new database: + +.. code:: bash + + docker exec -it $(docker ps --format '{{.Names}}' | grep 'care-db') psql -U postgres -c "CREATE DATABASE care;" + +Execute and copy the name of the file you want to restore the database with: + +.. code:: bash + + sudo ls ./care-backups + +Restore the database: + + Replace with your file name which looks like this ``care_backup_%Y%m%d%H%M%S.sql`` + +.. code:: bash + + docker exec -it $(docker ps --format '{{.Names}}' | grep 'care-db') pg_restore -U postgres -d care /backups/.dump + +------------------------------------------------------------------------------------------------------------------ + + There are way easier ways to do this. If anyone has any particular idea, feel free to make a PR :) diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100755 index 0000000000..56efda45c4 --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -ueo pipefail +# Ensure we can find the .env file +ENV_FILE="$(dirname "$(readlink -f "$0")")/../.env" +if [[ ! -f "${ENV_FILE}" ]]; then + echo "Error: .env file not found at ${ENV_FILE}" >&2 + exit 1 +fi +source "${ENV_FILE}" + +container_name="$(docker ps --format '{{.Names}}' | grep 'care-db')" +if [[ -z "${container_name}" ]]; then + echo "Error: PostgreSQL container 'care-db' is not running" >&2 + exit 1 + elif [[ $(echo "${container_name}" | wc -l) -gt 1 ]]; then + echo "Error: Multiple containers matched 'care-db'" >&2 + exit 1 +fi + +date=$(date +%Y%m%d%H%M%S) +#name the file +backup_file="${POSTGRES_DB}_backup_${date}.dump" + +# Remove old backup/backups +docker exec -t ${container_name} find "/backups" -name "${POSTGRES_DB}_backup_*.dump" -type f -mtime +${DB_BACKUP_RETENTION_PERIOD} -exec rm {} \; + +#backup the database +docker exec -t ${container_name} pg_dump -U ${POSTGRES_USER} -Fc -f /backups/${backup_file} ${POSTGRES_DB} + +if ! docker exec -t ${container_name} pg_dump -U ${POSTGRES_USER} -Fc -f /backups/${backup_file} ${POSTGRES_DB}; then + echo "Error: Database backup failed" >&2 + exit 1 +fi +echo "Backup of database '${POSTGRES_DB}' completed and saved as /backups/${backup_file}" From f217f3a3d661b60e1d579b01714e24436e7740bf Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Tue, 26 Nov 2024 13:43:10 +0530 Subject: [PATCH 17/19] Fix prod docker image (#2618) --- docker/prod.Dockerfile | 11 +++++++---- scripts/celery_beat.sh | 1 - 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docker/prod.Dockerfile b/docker/prod.Dockerfile index b2fe64f7fe..ca99cf3767 100644 --- a/docker/prod.Dockerfile +++ b/docker/prod.Dockerfile @@ -22,9 +22,6 @@ ENV PATH=$APP_HOME/.venv/bin:$PATH # --- FROM base AS builder -RUN addgroup --system django \ - && adduser --system --ingroup django django - RUN apt-get update && apt-get install --no-install-recommends -y \ build-essential libjpeg-dev zlib1g-dev libgmp-dev libpq-dev git wget \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ @@ -59,11 +56,16 @@ RUN python3 $APP_HOME/install_plugins.py # --- FROM base AS runtime +RUN addgroup --system django \ + && adduser --system --ingroup django django + RUN apt-get update && apt-get install --no-install-recommends -y \ 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/* +RUN chown django:django $APP_HOME + COPY --from=builder --chmod=0755 /usr/local/bin/typst /usr/local/bin/typst COPY --from=builder --chown=django:django $APP_HOME/.venv $APP_HOME/.venv @@ -75,9 +77,10 @@ COPY --chown=django:django . $APP_HOME USER django HEALTHCHECK \ + --start-period=20s \ + --start-interval=1s \ --interval=30s \ --timeout=5s \ - --start-period=10s \ --retries=12 \ CMD ["./healthcheck.sh"] diff --git a/scripts/celery_beat.sh b/scripts/celery_beat.sh index dd1a5f36e5..96f552b9ea 100755 --- a/scripts/celery_beat.sh +++ b/scripts/celery_beat.sh @@ -9,7 +9,6 @@ set -euo pipefail python manage.py migrate --noinput python manage.py compilemessages -v 0 python manage.py load_redis_index -python manage.py load_event_types touch /tmp/healthy From 8cb5fd31799f85ca1ffb25a51f73fec15b387668 Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Thu, 28 Nov 2024 19:45:54 +0530 Subject: [PATCH 18/19] fixed dockerfile for local plugin installation (#2622) fixed DockerFile for local plugin installation --- docker/dev.Dockerfile | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docker/dev.Dockerfile b/docker/dev.Dockerfile index 17d5c8df59..9bbb9e492a 100644 --- a/docker/dev.Dockerfile +++ b/docker/dev.Dockerfile @@ -37,12 +37,10 @@ RUN python -m venv /.venv COPY Pipfile Pipfile.lock $APP_HOME/ RUN --mount=type=cache,target=/root/.cache/pip pipenv install --system --categories "packages dev-packages docs" -COPY plugs/ $APP_HOME/plugs/ -COPY install_plugins.py plug_config.py $APP_HOME/ -RUN --mount=type=cache,target=/root/.cache/pip python3 $APP_HOME/install_plugins.py - COPY . $APP_HOME/ +RUN --mount=type=cache,target=/root/.cache/pip python3 $APP_HOME/install_plugins.py + HEALTHCHECK \ --interval=10s \ --timeout=5s \ From 907e87a08a502bd911c64183aeed339683b4654b Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Mon, 2 Dec 2024 14:17:57 +0530 Subject: [PATCH 19/19] =?UTF-8?q?Enforce=20`/scripts`=20directory=20uses?= =?UTF-8?q?=C2=A0LF=20line=20endings=20on=20all=20systems=20(#2625)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitattributes | 1 + README.md | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/.gitattributes b/.gitattributes index 176a458f94..a8d4103c3d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ * text=auto +scripts/* text eol=lf diff --git a/README.md b/README.md index 8aeaf21375..17455a29ea 100644 --- a/README.md +++ b/README.md @@ -60,13 +60,6 @@ to load dummy data for testing run: make load-dummy-data ``` -> [!NOTE] -> If you are unable to compose up care in windows, ensure line endings are set to `LF` (`docker-entrypoint.sh` won't -> work with `CRLF` line endings). -> ``` -> git config core.autocrlf false -> ``` - #### Docker Prebuilt docker images for server deployments are available