From 5d0158d2a3d1470210f6de562b9221a9d388a407 Mon Sep 17 00:00:00 2001 From: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Date: Wed, 15 May 2024 13:53:00 +0530 Subject: [PATCH] Merge Develop to Staging v24.21.0 (#2168) * Update max_length for phone numbers to adhere to ITU-T E.164 standard (#2138) * Update max_length for phone numbers to adhere to ITU-T E.164 standard * Clean up an empty line * Adds missing migrations --------- Co-authored-by: rithviknishad Co-authored-by: Vignesh Hari * Adds support for doctors and nurses discussions threads in Discussion Notes (#2137) * Adds support for doctors and nurses discussions threads in Discussion Notes * switch to using small integer field * updated and fixes based on test cases * rebase migrations * Remake migrations --------- Co-authored-by: Vignesh Hari * Allow `audio/mp4` by default (#2094) Co-authored-by: Vignesh Hari * Bump Dependencies (#2143) * Vigneshhari/bump dependencies (#2145) * Bump Dependencies * Bump Dependencies * Bump Dependencies * Bump Dependencies * Skip creating event group if all fields are null (#2134) * ABDM M3 (#1829) Co-authored-by: Aakash Singh Co-authored-by: Gigin George * Increase area of specialisation in doctors (#2102) * update doctor types * Add Migration --------- Co-authored-by: Rithvik Nishad Co-authored-by: Gigin George * Added discharge patient filters (#2124) * added discharge patient filters * updated filters --------- Co-authored-by: Vignesh Hari * converted investigation and investigation group data into JSON (#1912) * converted investigation and investigation group data into JSON * created json files for investigations and investigation_groups * updated investigations.json * fixed json data,command and migrations * fixing migrations * fixing lint issue * update migrations * update migrations --------- Co-authored-by: Aakash Singh * Drops support for cloning previous log update (#2127) * Drop support for cloning previous log update * undo unrelated changes --------- Co-authored-by: Vignesh Hari * Change devcontainer make up command to run after start (#2146) Co-authored-by: Vignesh Hari * Removes unused `current_health` field from Daily Rounds (#2162) Removes unused `current_health` from Daily Rounds Co-authored-by: Vignesh Hari * Display users list based on user access limitation (#1742) * Refactor get_queryset method in UserViewSet * Used get_accessible_facilities instead of subquery to fetch the facilities linked to the user * fixed failing tests * fix queryset --------- Co-authored-by: Aakash Singh Co-authored-by: khavinshankar Co-authored-by: Vignesh Hari * adds missing merge migrations (#2169) merge migrations --------- Co-authored-by: Bodhish Thomas Co-authored-by: rithviknishad Co-authored-by: Vignesh Hari Co-authored-by: Rithvik Nishad Co-authored-by: Aakash Singh Co-authored-by: Khavin Shankar Co-authored-by: Gigin George Co-authored-by: Rashmik <146672184+rash-27@users.noreply.github.com> Co-authored-by: Shivank Kacker Co-authored-by: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Co-authored-by: Gokulram A --- .devcontainer/devcontainer.json | 2 +- .gitignore | 4 + Pipfile | 12 +- Pipfile.lock | 621 +++--- aws/backend.json | 4 + aws/celery.json | 8 + care/abdm/api/serializers/consent.py | 33 + care/abdm/api/viewsets/auth.py | 12 +- care/abdm/api/viewsets/consent.py | 284 +++ care/abdm/api/viewsets/health_information.py | 160 ++ care/abdm/api/viewsets/patients.py | 77 + ...1_alter_abhanumber_abha_number_and_more.py | 429 ++++ .../migrations/0012_consentrequest_status.py | 27 + care/abdm/models/__init__.py | 3 + .../abdm/{models.py => models/abha_number.py} | 20 +- care/abdm/models/base.py | 43 + care/abdm/models/consent.py | 162 ++ care/abdm/models/health_facility.py | 15 + care/abdm/models/json_schema.py | 15 + .../permissions/health_facility.py} | 0 care/abdm/service/gateway.py | 214 ++ care/abdm/service/request.py | 92 + care/abdm/urls.py | 42 + care/abdm/utils/api_call.py | 15 +- care/abdm/utils/cipher.py | 63 +- care/facility/api/serializers/daily_round.py | 67 +- care/facility/api/serializers/patient.py | 10 + care/facility/api/viewsets/patient.py | 112 +- care/facility/events/handler.py | 17 +- .../management/commands/load_event_types.py | 1 - .../0430_alter_asset_support_phone.py | 27 + .../migrations/0431_patientnotes_thread.py | 19 + .../0432_alter_fileupload_file_type.py | 30 + .../0433_alter_hospitaldoctors_area.py | 68 + .../0433_remove_dailyround_current_health.py | 16 + .../migrations/0434_unique_investigations.py | 39 + .../migrations/0435_merge_20240515_1301.py | 12 + care/facility/models/asset.py | 2 +- care/facility/models/daily_round.py | 5 +- care/facility/models/facility.py | 49 +- care/facility/models/file_upload.py | 1 + care/facility/models/icd11_diagnosis.py | 4 +- care/facility/models/patient.py | 10 + care/facility/models/patient_base.py | 8 - care/facility/models/patient_investigation.py | 2 +- care/facility/tests/test_patient_api.py | 9 +- .../tests/test_patient_daily_rounds_api.py | 1 - .../tests/test_unlink_district_admins.py | 11 +- care/users/api/viewsets/users.py | 33 +- .../commands/populate_investigations.py | 245 +-- care/users/tests/test_api.py | 24 +- config/api_router.py | 12 + config/settings/base.py | 2 + data/investigation_groups.json | 30 + data/investigations.json | 1830 +++++++++++++++++ 55 files changed, 4393 insertions(+), 660 deletions(-) create mode 100644 care/abdm/api/serializers/consent.py create mode 100644 care/abdm/api/viewsets/consent.py create mode 100644 care/abdm/api/viewsets/health_information.py create mode 100644 care/abdm/api/viewsets/patients.py create mode 100644 care/abdm/migrations/0011_alter_abhanumber_abha_number_and_more.py create mode 100644 care/abdm/migrations/0012_consentrequest_status.py create mode 100644 care/abdm/models/__init__.py rename care/abdm/{models.py => models/abha_number.py} (63%) create mode 100644 care/abdm/models/base.py create mode 100644 care/abdm/models/consent.py create mode 100644 care/abdm/models/health_facility.py create mode 100644 care/abdm/models/json_schema.py rename care/abdm/{permissions.py => models/permissions/health_facility.py} (100%) create mode 100644 care/abdm/service/gateway.py create mode 100644 care/abdm/service/request.py create mode 100644 care/facility/migrations/0430_alter_asset_support_phone.py create mode 100644 care/facility/migrations/0431_patientnotes_thread.py create mode 100644 care/facility/migrations/0432_alter_fileupload_file_type.py create mode 100644 care/facility/migrations/0433_alter_hospitaldoctors_area.py create mode 100644 care/facility/migrations/0433_remove_dailyround_current_health.py create mode 100644 care/facility/migrations/0434_unique_investigations.py create mode 100644 care/facility/migrations/0435_merge_20240515_1301.py create mode 100644 data/investigation_groups.json create mode 100644 data/investigations.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d487346c8d..f7f05c0e56 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -19,6 +19,6 @@ } }, "postCreateCommand": "echo 'eval \"$(direnv hook bash)\"' >> ~/.bashrc && cp .env.example .env", - "postAttachCommand": "make up", + "postStartCommand": "make up", "forwardPorts": [8000, 9000, 4000] } diff --git a/.gitignore b/.gitignore index 4acdd07d9f..b4a2f6477d 100644 --- a/.gitignore +++ b/.gitignore @@ -356,3 +356,7 @@ secrets.sh /.idea/modules.xml /.idea/vcs.xml /.idea/ruff.xml + + +# Redis +*.rdb diff --git a/Pipfile b/Pipfile index 55b0861a8e..2d91f60a26 100644 --- a/Pipfile +++ b/Pipfile @@ -25,7 +25,7 @@ djangoql = "==0.17.1" djangorestframework = "==3.14.0" djangorestframework-simplejwt = "==5.3.1" dry-rest-permissions = "==0.1.10" -drf-nested-routers = "==0.93.4" +drf-nested-routers = "==0.94.1" drf-spectacular = "==0.26.4" "fhir.resources" = "==6.5.0" gunicorn = "==22.0.0" @@ -36,18 +36,18 @@ newrelic = "==9.3.0" pillow = "==10.3.0" psycopg = "==3.1.18" pycryptodome = "==3.20.0" -pydantic = "==1.10.12" # fix for fhir.resources < 7.0.2 +pydantic = "==1.10.15" # fix for fhir.resources < 7.0.2 pyjwt = "==2.8.0" python-slugify = "==8.0.1" pywebpush = "==1.14.0" redis = {extras = ["hiredis"], version = "<5.0.0"} # constraint for redis-om requests = "==2.31.0" sentry-sdk = "==1.30.0" -whitenoise = "==6.5.0" +whitenoise = "==6.6.0" redis-om = "==0.2.1" [dev-packages] -black = "==24.3.0" +black = "==24.4.2" boto3-stubs = {extras = ["s3", "boto3"], version = "==1.34.84"} coverage = "==7.4.0" debugpy = "==1.8.1" @@ -60,14 +60,14 @@ djangorestframework-stubs = "==3.14.2" factory-boy = "==3.3.0" flake8 = "==7.0.0" freezegun = "==1.2.2" -ipython = "==8.15.0" +ipython = "==8.24.0" isort = "==5.12.0" mypy = "==1.9.0" pre-commit = "==3.4.0" requests-mock = "==1.12.1" tblib = "==2.0.0" watchdog = "==3.0.0" -werkzeug = "==2.3.8" +werkzeug = "==3.0.3" [docs] furo = "==2023.9.10" diff --git a/Pipfile.lock b/Pipfile.lock index db18bde303..1007041312 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f03a9449ab0a958ac22c6dc8bc3b80bb87bd70eb03fb763da57af273f98c928b" + "sha256": "3225dfd3c9b038bca4b17ef7ea788a08cbaf926bf1e12d78c4d6add45a675816" }, "pipfile-spec": 6, "requires": { @@ -104,11 +104,11 @@ }, "botocore": { "hashes": [ - "sha256:a2b309bf5594f0eb6f63f355ade79ba575ce8bf672e52e91da1a7933caa245e6", - "sha256:da1ae0a912e69e10daee2a34dafd6c6c106450d20b8623665feceb2d96c173eb" + "sha256:0330d139f18f78d38127e65361859e24ebd6a8bcba184f903c01bb999a3fa431", + "sha256:5f07e2c7302c0a9f469dcd08b4ddac152e9f5888b12220242c20056255010939" ], "markers": "python_version >= '3.8'", - "version": "==1.34.84" + "version": "==1.34.103" }, "celery": { "hashes": [ @@ -314,41 +314,41 @@ }, "cryptography": { "hashes": [ - "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee", - "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576", - "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d", - "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30", - "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413", - "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb", - "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da", - "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4", - "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd", - "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc", - "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8", - "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1", - "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc", - "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e", - "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8", - "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940", - "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400", - "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7", - "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16", - "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278", - "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74", - "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec", - "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1", - "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2", - "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c", - "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922", - "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a", - "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6", - "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1", - "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e", - "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac", - "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7" + "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55", + "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785", + "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b", + "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886", + "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82", + "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1", + "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda", + "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f", + "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68", + "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60", + "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7", + "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd", + "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582", + "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc", + "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858", + "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b", + "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2", + "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678", + "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13", + "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4", + "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8", + "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604", + "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477", + "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e", + "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a", + "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9", + "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14", + "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda", + "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da", + "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562", + "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2", + "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9" ], "markers": "python_version >= '3.7'", - "version": "==42.0.5" + "version": "==42.0.7" }, "django": { "hashes": [ @@ -496,12 +496,12 @@ }, "drf-nested-routers": { "hashes": [ - "sha256:01aa556b8c08608bb74fb34f6ca065a5183f2cda4dc0478192cc17a2581d71b0", - "sha256:996b77f3f4dfaf64569e7b8f04e3919945f90f95366838ca5b8bed9dd709d6c5" + "sha256:2b846385ed95c9f17bf4242db3b264ac826b5af00dda6c737d3fe7cc7bf2c7db", + "sha256:3a8ec45a025c0f39188ec1ec415244beb875a6f4db87911a1f5a606d09b68c9f" ], "index": "pypi", - "markers": "python_version >= '3.5'", - "version": "==0.93.4" + "markers": "python_version >= '3.8'", + "version": "==0.94.1" }, "drf-spectacular": { "hashes": [ @@ -927,46 +927,46 @@ "email" ], "hashes": [ - "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303", - "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe", - "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47", - "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494", - "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33", - "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86", - "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d", - "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c", - "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a", - "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565", - "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb", - "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62", - "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62", - "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0", - "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523", - "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d", - "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405", - "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f", - "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b", - "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718", - "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed", - "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb", - "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5", - "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc", - "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942", - "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe", - "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246", - "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350", - "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303", - "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09", - "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33", - "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8", - "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a", - "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1", - "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6", - "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d" + "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de", + "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986", + "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55", + "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4", + "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58", + "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3", + "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12", + "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d", + "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7", + "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53", + "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb", + "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51", + "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948", + "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022", + "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed", + "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383", + "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4", + "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b", + "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2", + "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528", + "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf", + "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8", + "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc", + "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f", + "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0", + "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7", + "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c", + "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44", + "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654", + "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0", + "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb", + "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00", + "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1", + "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c", + "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22", + "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.10.12" + "version": "==1.10.15" }, "pyjwt": { "hashes": [ @@ -982,7 +982,7 @@ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "python-fsutil": { @@ -1102,11 +1102,11 @@ }, "referencing": { "hashes": [ - "sha256:5773bd84ef41799a5a8ca72dc34590c041eb01bf9aa02632b4a973fb0181a844", - "sha256:d53ae300ceddd3169f1ffa9caf2cb7b769e92657e4fafb23d34b93679116dfd4" + "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c", + "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de" ], "markers": "python_version >= '3.8'", - "version": "==0.34.0" + "version": "==0.35.1" }, "requests": { "hashes": [ @@ -1119,108 +1119,108 @@ }, "rpds-py": { "hashes": [ - "sha256:01e36a39af54a30f28b73096dd39b6802eddd04c90dbe161c1b8dbe22353189f", - "sha256:044a3e61a7c2dafacae99d1e722cc2d4c05280790ec5a05031b3876809d89a5c", - "sha256:08231ac30a842bd04daabc4d71fddd7e6d26189406d5a69535638e4dcb88fe76", - "sha256:08f9ad53c3f31dfb4baa00da22f1e862900f45908383c062c27628754af2e88e", - "sha256:0ab39c1ba9023914297dd88ec3b3b3c3f33671baeb6acf82ad7ce883f6e8e157", - "sha256:0af039631b6de0397ab2ba16eaf2872e9f8fca391b44d3d8cac317860a700a3f", - "sha256:0b8612cd233543a3781bc659c731b9d607de65890085098986dfd573fc2befe5", - "sha256:11a8c85ef4a07a7638180bf04fe189d12757c696eb41f310d2426895356dcf05", - "sha256:1374f4129f9bcca53a1bba0bb86bf78325a0374577cf7e9e4cd046b1e6f20e24", - "sha256:1d4acf42190d449d5e89654d5c1ed3a4f17925eec71f05e2a41414689cda02d1", - "sha256:1d9a5be316c15ffb2b3c405c4ff14448c36b4435be062a7f578ccd8b01f0c4d8", - "sha256:1df3659d26f539ac74fb3b0c481cdf9d725386e3552c6fa2974f4d33d78e544b", - "sha256:22806714311a69fd0af9b35b7be97c18a0fc2826e6827dbb3a8c94eac6cf7eeb", - "sha256:2644e47de560eb7bd55c20fc59f6daa04682655c58d08185a9b95c1970fa1e07", - "sha256:2e6d75ab12b0bbab7215e5d40f1e5b738aa539598db27ef83b2ec46747df90e1", - "sha256:30f43887bbae0d49113cbaab729a112251a940e9b274536613097ab8b4899cf6", - "sha256:34b18ba135c687f4dac449aa5157d36e2cbb7c03cbea4ddbd88604e076aa836e", - "sha256:36b3ee798c58ace201289024b52788161e1ea133e4ac93fba7d49da5fec0ef9e", - "sha256:39514da80f971362f9267c600b6d459bfbbc549cffc2cef8e47474fddc9b45b1", - "sha256:39f5441553f1c2aed4de4377178ad8ff8f9d733723d6c66d983d75341de265ab", - "sha256:3a96e0c6a41dcdba3a0a581bbf6c44bb863f27c541547fb4b9711fd8cf0ffad4", - "sha256:3f26b5bd1079acdb0c7a5645e350fe54d16b17bfc5e71f371c449383d3342e17", - "sha256:41ef53e7c58aa4ef281da975f62c258950f54b76ec8e45941e93a3d1d8580594", - "sha256:42821446ee7a76f5d9f71f9e33a4fb2ffd724bb3e7f93386150b61a43115788d", - "sha256:43fbac5f22e25bee1d482c97474f930a353542855f05c1161fd804c9dc74a09d", - "sha256:4457a94da0d5c53dc4b3e4de1158bdab077db23c53232f37a3cb7afdb053a4e3", - "sha256:465a3eb5659338cf2a9243e50ad9b2296fa15061736d6e26240e713522b6235c", - "sha256:482103aed1dfe2f3b71a58eff35ba105289b8d862551ea576bd15479aba01f66", - "sha256:4832d7d380477521a8c1644bbab6588dfedea5e30a7d967b5fb75977c45fd77f", - "sha256:4901165d170a5fde6f589acb90a6b33629ad1ec976d4529e769c6f3d885e3e80", - "sha256:5307def11a35f5ae4581a0b658b0af8178c65c530e94893345bebf41cc139d33", - "sha256:5417558f6887e9b6b65b4527232553c139b57ec42c64570569b155262ac0754f", - "sha256:56a737287efecafc16f6d067c2ea0117abadcd078d58721f967952db329a3e5c", - "sha256:586f8204935b9ec884500498ccc91aa869fc652c40c093bd9e1471fbcc25c022", - "sha256:5b4e7d8d6c9b2e8ee2d55c90b59c707ca59bc30058269b3db7b1f8df5763557e", - "sha256:5ddcba87675b6d509139d1b521e0c8250e967e63b5909a7e8f8944d0f90ff36f", - "sha256:618a3d6cae6ef8ec88bb76dd80b83cfe415ad4f1d942ca2a903bf6b6ff97a2da", - "sha256:635dc434ff724b178cb192c70016cc0ad25a275228f749ee0daf0eddbc8183b1", - "sha256:661d25cbffaf8cc42e971dd570d87cb29a665f49f4abe1f9e76be9a5182c4688", - "sha256:66e6a3af5a75363d2c9a48b07cb27c4ea542938b1a2e93b15a503cdfa8490795", - "sha256:67071a6171e92b6da534b8ae326505f7c18022c6f19072a81dcf40db2638767c", - "sha256:685537e07897f173abcf67258bee3c05c374fa6fff89d4c7e42fb391b0605e98", - "sha256:69e64831e22a6b377772e7fb337533c365085b31619005802a79242fee620bc1", - "sha256:6b0817e34942b2ca527b0e9298373e7cc75f429e8da2055607f4931fded23e20", - "sha256:6c81e5f372cd0dc5dc4809553d34f832f60a46034a5f187756d9b90586c2c307", - "sha256:6d7faa6f14017c0b1e69f5e2c357b998731ea75a442ab3841c0dbbbfe902d2c4", - "sha256:6ef0befbb5d79cf32d0266f5cff01545602344eda89480e1dd88aca964260b18", - "sha256:6ef687afab047554a2d366e112dd187b62d261d49eb79b77e386f94644363294", - "sha256:7223a2a5fe0d217e60a60cdae28d6949140dde9c3bcc714063c5b463065e3d66", - "sha256:77f195baa60a54ef9d2de16fbbfd3ff8b04edc0c0140a761b56c267ac11aa467", - "sha256:793968759cd0d96cac1e367afd70c235867831983f876a53389ad869b043c948", - "sha256:7bd339195d84439cbe5771546fe8a4e8a7a045417d8f9de9a368c434e42a721e", - "sha256:7cd863afe7336c62ec78d7d1349a2f34c007a3cc6c2369d667c65aeec412a5b1", - "sha256:7f2facbd386dd60cbbf1a794181e6aa0bd429bd78bfdf775436020172e2a23f0", - "sha256:84ffab12db93b5f6bad84c712c92060a2d321b35c3c9960b43d08d0f639d60d7", - "sha256:8c8370641f1a7f0e0669ddccca22f1da893cef7628396431eb445d46d893e5cd", - "sha256:8db715ebe3bb7d86d77ac1826f7d67ec11a70dbd2376b7cc214199360517b641", - "sha256:8e8916ae4c720529e18afa0b879473049e95949bf97042e938530e072fde061d", - "sha256:8f03bccbd8586e9dd37219bce4d4e0d3ab492e6b3b533e973fa08a112cb2ffc9", - "sha256:8f2fc11e8fe034ee3c34d316d0ad8808f45bc3b9ce5857ff29d513f3ff2923a1", - "sha256:923d39efa3cfb7279a0327e337a7958bff00cc447fd07a25cddb0a1cc9a6d2da", - "sha256:93df1de2f7f7239dc9cc5a4a12408ee1598725036bd2dedadc14d94525192fc3", - "sha256:998e33ad22dc7ec7e030b3df701c43630b5bc0d8fbc2267653577e3fec279afa", - "sha256:99f70b740dc04d09e6b2699b675874367885217a2e9f782bdf5395632ac663b7", - "sha256:9a00312dea9310d4cb7dbd7787e722d2e86a95c2db92fbd7d0155f97127bcb40", - "sha256:9d54553c1136b50fd12cc17e5b11ad07374c316df307e4cfd6441bea5fb68496", - "sha256:9dbbeb27f4e70bfd9eec1be5477517365afe05a9b2c441a0b21929ee61048124", - "sha256:a1ce3ba137ed54f83e56fb983a5859a27d43a40188ba798993812fed73c70836", - "sha256:a34d557a42aa28bd5c48a023c570219ba2593bcbbb8dc1b98d8cf5d529ab1434", - "sha256:a5f446dd5055667aabaee78487f2b5ab72e244f9bc0b2ffebfeec79051679984", - "sha256:ad36cfb355e24f1bd37cac88c112cd7730873f20fb0bdaf8ba59eedf8216079f", - "sha256:aec493917dd45e3c69d00a8874e7cbed844efd935595ef78a0f25f14312e33c6", - "sha256:b316144e85316da2723f9d8dc75bada12fa58489a527091fa1d5a612643d1a0e", - "sha256:b34ae4636dfc4e76a438ab826a0d1eed2589ca7d9a1b2d5bb546978ac6485461", - "sha256:b34b7aa8b261c1dbf7720b5d6f01f38243e9b9daf7e6b8bc1fd4657000062f2c", - "sha256:bc362ee4e314870a70f4ae88772d72d877246537d9f8cb8f7eacf10884862432", - "sha256:bed88b9a458e354014d662d47e7a5baafd7ff81c780fd91584a10d6ec842cb73", - "sha256:c0013fe6b46aa496a6749c77e00a3eb07952832ad6166bd481c74bda0dcb6d58", - "sha256:c0b5dcf9193625afd8ecc92312d6ed78781c46ecbf39af9ad4681fc9f464af88", - "sha256:c4325ff0442a12113a6379af66978c3fe562f846763287ef66bdc1d57925d337", - "sha256:c463ed05f9dfb9baebef68048aed8dcdc94411e4bf3d33a39ba97e271624f8f7", - "sha256:c8362467a0fdeccd47935f22c256bec5e6abe543bf0d66e3d3d57a8fb5731863", - "sha256:cd5bf1af8efe569654bbef5a3e0a56eca45f87cfcffab31dd8dde70da5982475", - "sha256:cf1ea2e34868f6fbf070e1af291c8180480310173de0b0c43fc38a02929fc0e3", - "sha256:d62dec4976954a23d7f91f2f4530852b0c7608116c257833922a896101336c51", - "sha256:d68c93e381010662ab873fea609bf6c0f428b6d0bb00f2c6939782e0818d37bf", - "sha256:d7c36232a90d4755b720fbd76739d8891732b18cf240a9c645d75f00639a9024", - "sha256:dd18772815d5f008fa03d2b9a681ae38d5ae9f0e599f7dda233c439fcaa00d40", - "sha256:ddc2f4dfd396c7bfa18e6ce371cba60e4cf9d2e5cdb71376aa2da264605b60b9", - "sha256:e003b002ec72c8d5a3e3da2989c7d6065b47d9eaa70cd8808b5384fbb970f4ec", - "sha256:e32a92116d4f2a80b629778280103d2a510a5b3f6314ceccd6e38006b5e92dcb", - "sha256:e4461d0f003a0aa9be2bdd1b798a041f177189c1a0f7619fe8c95ad08d9a45d7", - "sha256:e541ec6f2ec456934fd279a3120f856cd0aedd209fc3852eca563f81738f6861", - "sha256:e546e768d08ad55b20b11dbb78a745151acbd938f8f00d0cfbabe8b0199b9880", - "sha256:ea7d4a99f3b38c37eac212dbd6ec42b7a5ec51e2c74b5d3223e43c811609e65f", - "sha256:ed4eb745efbff0a8e9587d22a84be94a5eb7d2d99c02dacf7bd0911713ed14dd", - "sha256:f8a2f084546cc59ea99fda8e070be2fd140c3092dc11524a71aa8f0f3d5a55ca", - "sha256:fcb25daa9219b4cf3a0ab24b0eb9a5cc8949ed4dc72acb8fa16b7e1681aa3c58", - "sha256:fdea4952db2793c4ad0bdccd27c1d8fdd1423a92f04598bc39425bcc2b8ee46e" - ], - "markers": "python_version >= '3.8'", - "version": "==0.18.0" + "sha256:05f3d615099bd9b13ecf2fc9cf2d839ad3f20239c678f461c753e93755d629ee", + "sha256:06d218939e1bf2ca50e6b0ec700ffe755e5216a8230ab3e87c059ebb4ea06afc", + "sha256:07f2139741e5deb2c5154a7b9629bc5aa48c766b643c1a6750d16f865a82c5fc", + "sha256:08d74b184f9ab6289b87b19fe6a6d1a97fbfea84b8a3e745e87a5de3029bf944", + "sha256:0abeee75434e2ee2d142d650d1e54ac1f8b01e6e6abdde8ffd6eeac6e9c38e20", + "sha256:154bf5c93d79558b44e5b50cc354aa0459e518e83677791e6adb0b039b7aa6a7", + "sha256:17c6d2155e2423f7e79e3bb18151c686d40db42d8645e7977442170c360194d4", + "sha256:1805d5901779662d599d0e2e4159d8a82c0b05faa86ef9222bf974572286b2b6", + "sha256:19ba472b9606c36716062c023afa2484d1e4220548751bda14f725a7de17b4f6", + "sha256:19e515b78c3fc1039dd7da0a33c28c3154458f947f4dc198d3c72db2b6b5dc93", + "sha256:1d54f74f40b1f7aaa595a02ff42ef38ca654b1469bef7d52867da474243cc633", + "sha256:207c82978115baa1fd8d706d720b4a4d2b0913df1c78c85ba73fe6c5804505f0", + "sha256:2625f03b105328729f9450c8badda34d5243231eef6535f80064d57035738360", + "sha256:27bba383e8c5231cd559affe169ca0b96ec78d39909ffd817f28b166d7ddd4d8", + "sha256:2c3caec4ec5cd1d18e5dd6ae5194d24ed12785212a90b37f5f7f06b8bedd7139", + "sha256:2cc7c1a47f3a63282ab0f422d90ddac4aa3034e39fc66a559ab93041e6505da7", + "sha256:2fc24a329a717f9e2448f8cd1f960f9dac4e45b6224d60734edeb67499bab03a", + "sha256:312fe69b4fe1ffbe76520a7676b1e5ac06ddf7826d764cc10265c3b53f96dbe9", + "sha256:32b7daaa3e9389db3695964ce8e566e3413b0c43e3394c05e4b243a4cd7bef26", + "sha256:338dee44b0cef8b70fd2ef54b4e09bb1b97fc6c3a58fea5db6cc083fd9fc2724", + "sha256:352a88dc7892f1da66b6027af06a2e7e5d53fe05924cc2cfc56495b586a10b72", + "sha256:35b2b771b13eee8729a5049c976197ff58a27a3829c018a04341bcf1ae409b2b", + "sha256:38e14fb4e370885c4ecd734f093a2225ee52dc384b86fa55fe3f74638b2cfb09", + "sha256:3c20f05e8e3d4fc76875fc9cb8cf24b90a63f5a1b4c5b9273f0e8225e169b100", + "sha256:3dd3cd86e1db5aadd334e011eba4e29d37a104b403e8ca24dcd6703c68ca55b3", + "sha256:489bdfe1abd0406eba6b3bb4fdc87c7fa40f1031de073d0cfb744634cc8fa261", + "sha256:48c2faaa8adfacefcbfdb5f2e2e7bdad081e5ace8d182e5f4ade971f128e6bb3", + "sha256:4a98a1f0552b5f227a3d6422dbd61bc6f30db170939bd87ed14f3c339aa6c7c9", + "sha256:4adec039b8e2928983f885c53b7cc4cda8965b62b6596501a0308d2703f8af1b", + "sha256:4e0ee01ad8260184db21468a6e1c37afa0529acc12c3a697ee498d3c2c4dcaf3", + "sha256:51584acc5916212e1bf45edd17f3a6b05fe0cbb40482d25e619f824dccb679de", + "sha256:531796fb842b53f2695e94dc338929e9f9dbf473b64710c28af5a160b2a8927d", + "sha256:5463c47c08630007dc0fe99fb480ea4f34a89712410592380425a9b4e1611d8e", + "sha256:5c45a639e93a0c5d4b788b2613bd637468edd62f8f95ebc6fcc303d58ab3f0a8", + "sha256:6031b25fb1b06327b43d841f33842b383beba399884f8228a6bb3df3088485ff", + "sha256:607345bd5912aacc0c5a63d45a1f73fef29e697884f7e861094e443187c02be5", + "sha256:618916f5535784960f3ecf8111581f4ad31d347c3de66d02e728de460a46303c", + "sha256:636a15acc588f70fda1661234761f9ed9ad79ebed3f2125d44be0862708b666e", + "sha256:673fdbbf668dd958eff750e500495ef3f611e2ecc209464f661bc82e9838991e", + "sha256:6afd80f6c79893cfc0574956f78a0add8c76e3696f2d6a15bca2c66c415cf2d4", + "sha256:6b5ff7e1d63a8281654b5e2896d7f08799378e594f09cf3674e832ecaf396ce8", + "sha256:6c4c4c3f878df21faf5fac86eda32671c27889e13570645a9eea0a1abdd50922", + "sha256:6cd8098517c64a85e790657e7b1e509b9fe07487fd358e19431cb120f7d96338", + "sha256:6d1e42d2735d437e7e80bab4d78eb2e459af48c0a46e686ea35f690b93db792d", + "sha256:6e30ac5e329098903262dc5bdd7e2086e0256aa762cc8b744f9e7bf2a427d3f8", + "sha256:70a838f7754483bcdc830444952fd89645569e7452e3226de4a613a4c1793fb2", + "sha256:720edcb916df872d80f80a1cc5ea9058300b97721efda8651efcd938a9c70a72", + "sha256:732672fbc449bab754e0b15356c077cc31566df874964d4801ab14f71951ea80", + "sha256:740884bc62a5e2bbb31e584f5d23b32320fd75d79f916f15a788d527a5e83644", + "sha256:7700936ef9d006b7ef605dc53aa364da2de5a3aa65516a1f3ce73bf82ecfc7ae", + "sha256:7732770412bab81c5a9f6d20aeb60ae943a9b36dcd990d876a773526468e7163", + "sha256:7750569d9526199c5b97e5a9f8d96a13300950d910cf04a861d96f4273d5b104", + "sha256:7f1944ce16401aad1e3f7d312247b3d5de7981f634dc9dfe90da72b87d37887d", + "sha256:81c5196a790032e0fc2464c0b4ab95f8610f96f1f2fa3d4deacce6a79852da60", + "sha256:8352f48d511de5f973e4f2f9412736d7dea76c69faa6d36bcf885b50c758ab9a", + "sha256:8927638a4d4137a289e41d0fd631551e89fa346d6dbcfc31ad627557d03ceb6d", + "sha256:8c7672e9fba7425f79019db9945b16e308ed8bc89348c23d955c8c0540da0a07", + "sha256:8d2e182c9ee01135e11e9676e9a62dfad791a7a467738f06726872374a83db49", + "sha256:910e71711d1055b2768181efa0a17537b2622afeb0424116619817007f8a2b10", + "sha256:942695a206a58d2575033ff1e42b12b2aece98d6003c6bc739fbf33d1773b12f", + "sha256:9437ca26784120a279f3137ee080b0e717012c42921eb07861b412340f85bae2", + "sha256:967342e045564cef76dfcf1edb700b1e20838d83b1aa02ab313e6a497cf923b8", + "sha256:998125738de0158f088aef3cb264a34251908dd2e5d9966774fdab7402edfab7", + "sha256:9e6934d70dc50f9f8ea47081ceafdec09245fd9f6032669c3b45705dea096b88", + "sha256:a3d456ff2a6a4d2adcdf3c1c960a36f4fd2fec6e3b4902a42a384d17cf4e7a65", + "sha256:a7b28c5b066bca9a4eb4e2f2663012debe680f097979d880657f00e1c30875a0", + "sha256:a888e8bdb45916234b99da2d859566f1e8a1d2275a801bb8e4a9644e3c7e7909", + "sha256:aa3679e751408d75a0b4d8d26d6647b6d9326f5e35c00a7ccd82b78ef64f65f8", + "sha256:aaa71ee43a703c321906813bb252f69524f02aa05bf4eec85f0c41d5d62d0f4c", + "sha256:b646bf655b135ccf4522ed43d6902af37d3f5dbcf0da66c769a2b3938b9d8184", + "sha256:b906b5f58892813e5ba5c6056d6a5ad08f358ba49f046d910ad992196ea61397", + "sha256:b9bb1f182a97880f6078283b3505a707057c42bf55d8fca604f70dedfdc0772a", + "sha256:bd1105b50ede37461c1d51b9698c4f4be6e13e69a908ab7751e3807985fc0346", + "sha256:bf18932d0003c8c4d51a39f244231986ab23ee057d235a12b2684ea26a353590", + "sha256:c273e795e7a0f1fddd46e1e3cb8be15634c29ae8ff31c196debb620e1edb9333", + "sha256:c69882964516dc143083d3795cb508e806b09fc3800fd0d4cddc1df6c36e76bb", + "sha256:c827576e2fa017a081346dce87d532a5310241648eb3700af9a571a6e9fc7e74", + "sha256:cbfbea39ba64f5e53ae2915de36f130588bba71245b418060ec3330ebf85678e", + "sha256:ce0bb20e3a11bd04461324a6a798af34d503f8d6f1aa3d2aa8901ceaf039176d", + "sha256:d0cee71bc618cd93716f3c1bf56653740d2d13ddbd47673efa8bf41435a60daa", + "sha256:d21be4770ff4e08698e1e8e0bce06edb6ea0626e7c8f560bc08222880aca6a6f", + "sha256:d31dea506d718693b6b2cffc0648a8929bdc51c70a311b2770f09611caa10d53", + "sha256:d44607f98caa2961bab4fa3c4309724b185b464cdc3ba6f3d7340bac3ec97cc1", + "sha256:d58ad6317d188c43750cb76e9deacf6051d0f884d87dc6518e0280438648a9ac", + "sha256:d70129cef4a8d979caa37e7fe957202e7eee8ea02c5e16455bc9808a59c6b2f0", + "sha256:d85164315bd68c0806768dc6bb0429c6f95c354f87485ee3593c4f6b14def2bd", + "sha256:d960de62227635d2e61068f42a6cb6aae91a7fe00fca0e3aeed17667c8a34611", + "sha256:dc48b479d540770c811fbd1eb9ba2bb66951863e448efec2e2c102625328e92f", + "sha256:e1735502458621921cee039c47318cb90b51d532c2766593be6207eec53e5c4c", + "sha256:e2be6e9dd4111d5b31ba3b74d17da54a8319d8168890fbaea4b9e5c3de630ae5", + "sha256:e4c39ad2f512b4041343ea3c7894339e4ca7839ac38ca83d68a832fc8b3748ab", + "sha256:ed402d6153c5d519a0faf1bb69898e97fb31613b49da27a84a13935ea9164dfc", + "sha256:ee17cd26b97d537af8f33635ef38be873073d516fd425e80559f4585a7b90c43", + "sha256:f3027be483868c99b4985fda802a57a67fdf30c5d9a50338d9db646d590198da", + "sha256:f5bab211605d91db0e2995a17b5c6ee5edec1270e46223e513eaa20da20076ac", + "sha256:f6f8e3fecca256fefc91bb6765a693d96692459d7d4c644660a9fff32e517843", + "sha256:f7afbfee1157e0f9376c00bb232e80a60e59ed716e3211a80cb8506550671e6e", + "sha256:fa242ac1ff583e4ec7771141606aafc92b361cd90a05c30d93e343a0c2d82a89", + "sha256:fab6ce90574645a0d6c58890e9bcaac8d94dff54fb51c69e5522a7358b80ab64" + ], + "markers": "python_version >= '3.8'", + "version": "==0.18.1" }, "s3transfer": { "hashes": [ @@ -1243,7 +1243,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "sqlparse": { @@ -1261,21 +1261,37 @@ ], "version": "==1.3" }, + "types-cffi": { + "hashes": [ + "sha256:a363e5ea54a4eb6a4a105d800685fde596bc318089b025b27dee09849fe41ff0", + "sha256:b8b20d23a2b89cfed5f8c5bc53b0cb8677c3aac6d970dbc771e28b9c698f5dee" + ], + "markers": "python_version >= '3.8'", + "version": "==1.16.0.20240331" + }, "types-pyopenssl": { "hashes": [ - "sha256:6e8e8bfad34924067333232c93f7fc4b369856d8bea0d5c9d1808cb290ab1972", - "sha256:7bca00cfc4e7ef9c5d2663c6a1c068c35798e59670595439f6296e7ba3d58083" + "sha256:0a7e82626c1983dc8dc59292bf20654a51c3c3881bcbb9b337c1da6e32f0204e", + "sha256:f51a156835555dd2a1f025621e8c4fbe7493470331afeef96884d1d29bf3a473" ], "markers": "python_version >= '3.8'", - "version": "==24.0.0.20240311" + "version": "==24.1.0.20240425" }, "types-redis": { "hashes": [ - "sha256:a3b92760c49a034827a0c3825206728df4e61e981c1324099d4414335af4f52f", - "sha256:ce217c279581d769df992c5b76d61c65425b0a679626048e633e643868eb881b" + "sha256:9402a10ee931d241fdfcc04592ebf7a661d7bb92a8dea631279f0d8acbcf3a22", + "sha256:ac5bc19e8f5997b9e76ad5d9cf15d0392d9f28cf5fc7746ea4a64b989c45c6a8" + ], + "markers": "python_version >= '3.8'", + "version": "==4.6.0.20240425" + }, + "types-setuptools": { + "hashes": [ + "sha256:a4381e041510755a6c9210e26ad55b1629bc10237aeb9cb8b6bd24996b73db48", + "sha256:a7ba908f1746c4337d13f027fa0f4a5bcad6d1d92048219ba792b3295c58586d" ], "markers": "python_version >= '3.8'", - "version": "==4.6.0.20240409" + "version": "==69.5.0.20240423" }, "typing-extensions": { "hashes": [ @@ -1332,12 +1348,12 @@ }, "whitenoise": { "hashes": [ - "sha256:15fe60546ac975b58e357ccaeb165a4ca2d0ab697e48450b8f0307ca368195a8", - "sha256:16468e9ad2189f09f4a8c635a9031cc9bb2cdbc8e5e53365407acf99f7ade9ec" + "sha256:8998f7370973447fac1e8ef6e8ded2c5209a7b1f67c1012866dbcd09681c3251", + "sha256:b1f9db9bf67dc183484d760b99f4080185633136a273a03f6436034a41064146" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==6.5.0" + "markers": "python_version >= '3.8'", + "version": "==6.6.0" } }, "develop": { @@ -1364,50 +1380,43 @@ "markers": "python_version >= '3.8'", "version": "==2.1.0" }, - "backcall": { - "hashes": [ - "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", - "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255" - ], - "version": "==0.2.0" - }, "black": { "hashes": [ - "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", - "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", - "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", - "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0", - "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9", - "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", - "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213", - "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d", - "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7", - "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837", - "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f", - "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395", - "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995", - "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", - "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597", - "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959", - "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5", - "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb", - "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", - "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7", - "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd", - "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==24.3.0" + "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474", + "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1", + "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0", + "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8", + "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96", + "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1", + "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04", + "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021", + "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94", + "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d", + "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c", + "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7", + "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c", + "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc", + "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7", + "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d", + "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c", + "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741", + "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce", + "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb", + "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063", + "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==24.4.2" }, "boto3": { "hashes": [ - "sha256:ba5d2104bba4370766036d64ad9021eb6289d154265852a2a821ec6a5e816faa", - "sha256:eaec72fda124084105a31bcd67eafa1355b34df6da70cadae0c0f262d8a4294f" + "sha256:7a02f44af32095946587d748ebeb39c3fa15b9d7275307ff612a6760ead47e04", + "sha256:91e6343474173e9b82f603076856e1d5b7b68f44247bdd556250857a3f16b37b" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.75" + "version": "==1.34.84" }, "boto3-stubs": { "extras": [ @@ -1418,25 +1427,24 @@ "sha256:73bbb509a69c4ac8cce038afb1510686b88398cbd46d5df1e3238fce66df9af5", "sha256:dd8b6147297b5aefd52212645179c96c4b5bcb4e514667dca6170485c1d4954a" ], - "index": "pypi", "markers": "python_version >= '3.8'", "version": "==1.34.84" }, "botocore": { "hashes": [ - "sha256:a2b309bf5594f0eb6f63f355ade79ba575ce8bf672e52e91da1a7933caa245e6", - "sha256:da1ae0a912e69e10daee2a34dafd6c6c106450d20b8623665feceb2d96c173eb" + "sha256:0330d139f18f78d38127e65361859e24ebd6a8bcba184f903c01bb999a3fa431", + "sha256:5f07e2c7302c0a9f469dcd08b4ddac152e9f5888b12220242c20056255010939" ], "markers": "python_version >= '3.8'", - "version": "==1.34.84" + "version": "==1.34.103" }, "botocore-stubs": { "hashes": [ - "sha256:0c3835c775db1387246c1ba8063b197604462fba8603d9b36b5dc60297197b2f", - "sha256:463248fd1d6e7b68a0c57bdd758d04c6bd0c5c2c3bfa81afdf9d64f0930b59bc" + "sha256:64d80a3467e3b19939e9c2750af33328b3087f8f524998dbdf7ed168227f507d", + "sha256:b0345f55babd8b901c53804fc5c326a4a0bd2e23e3b71f9ea5d9f7663466e6ba" ], "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==1.34.69" + "version": "==1.34.94" }, "certifi": { "hashes": [ @@ -1716,11 +1724,11 @@ }, "django-stubs-ext": { "hashes": [ - "sha256:45a5d102417a412e3606e3c358adb4744988a92b7b58ccf3fd64bddd5d04d14c", - "sha256:519342ac0849cda1559746c9a563f03ff99f636b0ebe7c14b75e816a00dfddc3" + "sha256:5bacfbb498a206d5938454222b843d81da79ea8b6fcd1a59003f529e775bc115", + "sha256:8e1334fdf0c8bff87e25d593b33d4247487338aaed943037826244ff788b56a8" ], "markers": "python_version >= '3.8'", - "version": "==4.2.7" + "version": "==5.0.0" }, "djangorestframework-stubs": { "hashes": [ @@ -1750,19 +1758,19 @@ }, "faker": { "hashes": [ - "sha256:73b1e7967b0ceeac42fc99a8c973bb49e4499cc4044d20d17ab661d5cb7eda1d", - "sha256:97c7874665e8eb7b517f97bf3b59f03bf3f07513fe2c159e98b6b9ea6b9f2b3d" + "sha256:2107618cf306bb188dcfea3e5cfd94aa92d65c7293a2437c1e96a99c83274755", + "sha256:24e28dce0b89683bb9e017e042b971c8c4909cff551b6d46f1e207674c7c2526" ], "markers": "python_version >= '3.8'", - "version": "==24.9.0" + "version": "==25.1.0" }, "filelock": { "hashes": [ - "sha256:404e5e9253aa60ad457cae1be07c0f0ca90a63931200a47d9b6a6af84fd7b45f", - "sha256:d13f466618bfde72bd2c18255e269f72542c6e70e7bac83a0232d6b1cc5c8cf4" + "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f", + "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a" ], "markers": "python_version >= '3.8'", - "version": "==3.13.4" + "version": "==3.14.0" }, "flake8": { "hashes": [ @@ -1792,11 +1800,11 @@ }, "identify": { "hashes": [ - "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791", - "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e" + "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa", + "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d" ], "markers": "python_version >= '3.8'", - "version": "==2.5.35" + "version": "==2.5.36" }, "idna": { "hashes": [ @@ -1808,12 +1816,12 @@ }, "ipython": { "hashes": [ - "sha256:2baeb5be6949eeebf532150f81746f8333e2ccce02de1c7eedde3f23ed5e9f1e", - "sha256:45a2c3a529296870a97b7de34eda4a31bee16bc7bf954e07d39abe49caf8f887" + "sha256:010db3f8a728a578bb641fdd06c063b9fb8e96a9464c63aec6310fbcb5e80501", + "sha256:d7bf2f6c4314984e3e02393213bab8703cf163ede39672ce5918c51fe253a2a3" ], "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==8.15.0" + "markers": "python_version >= '3.10'", + "version": "==8.24.0" }, "isort": { "hashes": [ @@ -1908,11 +1916,11 @@ }, "matplotlib-inline": { "hashes": [ - "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311", - "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304" + "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", + "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca" ], - "markers": "python_version >= '3.5'", - "version": "==0.1.6" + "markers": "python_version >= '3.8'", + "version": "==0.1.7" }, "mccabe": { "hashes": [ @@ -1958,10 +1966,10 @@ }, "mypy-boto3-s3": { "hashes": [ - "sha256:2aecfbe1c00654bc21f839068218d60123366954bf43a708baa50f9543e3f205", - "sha256:2fcdf412ce2924b2f0b34db59abf06a9c0bbe4cd3361f14f0d2c1e211c0f7ddd" + "sha256:0d37161fd0cd7ebf194cf9ccadb9101bf5c9b2426c2d00677b7e644d6f2298e4", + "sha256:70c8bad00db70704fb7ac0ee1440c7eb0587578ae9a2b00997f29f17f60f45e7" ], - "version": "==1.34.65" + "version": "==1.34.91" }, "mypy-extensions": { "hashes": [ @@ -2008,23 +2016,16 @@ "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f" ], - "markers": "sys_platform != 'win32'", + "markers": "sys_platform != 'win32' and sys_platform != 'emscripten'", "version": "==4.9.0" }, - "pickleshare": { - "hashes": [ - "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", - "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56" - ], - "version": "==0.7.5" - }, "platformdirs": { "hashes": [ - "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", - "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768" + "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf", + "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1" ], "markers": "python_version >= '3.8'", - "version": "==4.2.0" + "version": "==4.2.1" }, "pre-commit": { "hashes": [ @@ -2075,18 +2076,18 @@ }, "pygments": { "hashes": [ - "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", - "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" + "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", + "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" ], - "markers": "python_version >= '3.7'", - "version": "==2.17.2" + "markers": "python_version >= '3.8'", + "version": "==2.18.0" }, "python-dateutil": { "hashes": [ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "pyyaml": { @@ -2185,7 +2186,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "sqlparse": { @@ -2214,27 +2215,27 @@ }, "traitlets": { "hashes": [ - "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9", - "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80" + "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", + "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f" ], "markers": "python_version >= '3.8'", - "version": "==5.14.2" + "version": "==5.14.3" }, "types-awscrt": { "hashes": [ - "sha256:61811bbf4de95248939f9276a434be93d2b95f6ccfe8aa94e56999e9778cfcc2", - "sha256:79d5bfb01f64701b6cf442e89a37d9c4dc6dbb79a46f2f611739b2418d30ecfd" + "sha256:3ae374b553e7228ba41a528cf42bd0b2ad7303d806c73eff4aaaac1515e3ea4e", + "sha256:64898a2f4a2468f66233cb8c29c5f66de907cf80ba1ef5bb1359aef2f81bb521" ], "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==0.20.5" + "version": "==0.20.9" }, "types-pytz": { "hashes": [ - "sha256:9679eef0365db3af91ef7722c199dbb75ee5c1b67e3c4dd7bfbeb1b8a71c21a3", - "sha256:c93751ee20dfc6e054a0148f8f5227b9a00b79c90a4d3c9f464711a73179c89e" + "sha256:6810c8a1f68f21fdf0f4f374a432487c77645a0ac0b31de4bf4690cf21ad3981", + "sha256:8335d443310e2db7b74e007414e74c4f53b67452c0cb0d228ca359ccfba59659" ], "markers": "python_version >= '3.8'", - "version": "==2024.1.0.20240203" + "version": "==2024.1.0.20240417" }, "types-pyyaml": { "hashes": [ @@ -2254,11 +2255,11 @@ }, "types-s3transfer": { "hashes": [ - "sha256:35e4998c25df7f8985ad69dedc8e4860e8af3b43b7615e940d53c00d413bdc69", - "sha256:44fcdf0097b924a9aab1ee4baa1179081a9559ca62a88c807e2b256893ce688f" + "sha256:02154cce46528287ad76ad1a0153840e0492239a0887e8833466eccf84b98da0", + "sha256:49a7c81fa609ac1532f8de3756e64b58afcecad8767933310228002ec7adff74" ], - "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==0.10.0" + "markers": "python_version >= '3.8' and python_version < '4.0'", + "version": "==0.10.1" }, "typing-extensions": { "hashes": [ @@ -2273,16 +2274,16 @@ "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" ], - "markers": "python_version >= '3.6'", + "markers": "python_version >= '3.8'", "version": "==2.2.1" }, "virtualenv": { "hashes": [ - "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a", - "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197" + "sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b", + "sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75" ], "markers": "python_version >= '3.7'", - "version": "==20.25.1" + "version": "==20.26.1" }, "watchdog": { "hashes": [ @@ -2327,12 +2328,12 @@ }, "werkzeug": { "hashes": [ - "sha256:554b257c74bbeb7a0d254160a4f8ffe185243f52a52035060b761ca62d977f03", - "sha256:bba1f19f8ec89d4d607a3bd62f1904bd2e609472d93cd85e9d4e178f472c3748" + "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18", + "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2.3.8" + "version": "==3.0.3" } }, "docs": { @@ -2346,11 +2347,11 @@ }, "babel": { "hashes": [ - "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363", - "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287" + "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb", + "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413" ], - "markers": "python_version >= '3.7'", - "version": "==2.14.0" + "markers": "python_version >= '3.8'", + "version": "==2.15.0" }, "beautifulsoup4": { "hashes": [ @@ -2499,11 +2500,11 @@ }, "jinja2": { "hashes": [ - "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", - "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90" + "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", + "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" ], "markers": "python_version >= '3.7'", - "version": "==3.1.3" + "version": "==3.1.4" }, "markdown-it-py": { "hashes": [ @@ -2614,11 +2615,11 @@ }, "pygments": { "hashes": [ - "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c", - "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367" + "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", + "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" ], - "markers": "python_version >= '3.7'", - "version": "==2.17.2" + "markers": "python_version >= '3.8'", + "version": "==2.18.0" }, "pyyaml": { "hashes": [ @@ -2771,7 +2772,7 @@ "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" ], - "markers": "python_version >= '3.6'", + "markers": "python_version >= '3.8'", "version": "==2.2.1" } } diff --git a/aws/backend.json b/aws/backend.json index 594e0fe214..3d419d67c7 100644 --- a/aws/backend.json +++ b/aws/backend.json @@ -41,6 +41,10 @@ "name": "CURRENT_DOMAIN", "value": "https://care.ohc.network" }, + { + "name": "BACKEND_DOMAIN", + "value": "https://careapi.ohc.network" + }, { "name": "DJANGO_ADMIN_URL", "value": "w8BYTTYRkxqAsbS2iU9Yd2ZgQy6D3uws" diff --git a/aws/celery.json b/aws/celery.json index 51c2fd0c56..524a548144 100644 --- a/aws/celery.json +++ b/aws/celery.json @@ -32,6 +32,10 @@ "name": "CURRENT_DOMAIN", "value": "https://care.ohc.network" }, + { + "name": "BACKEND_DOMAIN", + "value": "https://careapi.ohc.network" + }, { "name": "DJANGO_ADMIN_URL", "value": "w8BYTTYRkxqAsbS2iU9Yd2ZgQy6D3uws" @@ -307,6 +311,10 @@ "name": "CURRENT_DOMAIN", "value": "https://care.ohc.network" }, + { + "name": "BACKEND_DOMAIN", + "value": "https://careapi.ohc.network" + }, { "name": "DJANGO_ADMIN_URL", "value": "w8BYTTYRkxqAsbS2iU9Yd2ZgQy6D3uws" diff --git a/care/abdm/api/serializers/consent.py b/care/abdm/api/serializers/consent.py new file mode 100644 index 0000000000..a6bb34b6c7 --- /dev/null +++ b/care/abdm/api/serializers/consent.py @@ -0,0 +1,33 @@ +from rest_framework import serializers + +from care.abdm.api.serializers.abhanumber import AbhaNumberSerializer +from care.abdm.models.consent import ConsentArtefact, ConsentRequest +from care.users.api.serializers.user import UserBaseMinimumSerializer + + +class ConsentArtefactSerializer(serializers.ModelSerializer): + id = serializers.CharField(source="external_id", read_only=True) + + class Meta: + model = ConsentArtefact + exclude = ( + "deleted", + "external_id", + "key_material_private_key", + "key_material_public_key", + "key_material_nonce", + "key_material_algorithm", + "key_material_curve", + "signature", + ) + + +class ConsentRequestSerializer(serializers.ModelSerializer): + id = serializers.CharField(source="external_id", read_only=True) + patient_abha_object = AbhaNumberSerializer(source="patient_abha", read_only=True) + requester = UserBaseMinimumSerializer(read_only=True) + consent_artefacts = ConsentArtefactSerializer(many=True, read_only=True) + + class Meta: + model = ConsentRequest + exclude = ("deleted", "external_id") diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 5802a493db..8cf290be31 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -290,7 +290,7 @@ def post(self, request, *args, **kwargs): data["hiRequest"]["keyMaterial"]["nonce"], ) - AbdmGateway().data_transfer( + data_transfer_response = AbdmGateway().data_transfer( { "transaction_id": data["transactionId"], "data_push_url": data["hiRequest"]["dataPushUrl"], @@ -306,11 +306,11 @@ def post(self, request, *args, **kwargs): ], "data": cipher.encrypt( Fhir( - PatientConsultation.objects.get( + PatientConsultation.objects.filter( external_id=context[ "careContextReference" ] - ) + ).first() ).create_record(record) )["data"], }, @@ -332,7 +332,7 @@ def post(self, request, *args, **kwargs): "parameters": "Curve25519/32byte random key", "keyValue": cipher.key_to_share, }, - "nonce": cipher.sender_nonce, + "nonce": cipher.internal_nonce, }, } ) @@ -345,6 +345,10 @@ def post(self, request, *args, **kwargs): ], "consent_id": data["hiRequest"]["consent"]["id"], "transaction_id": data["transactionId"], + "session_status": "TRANSFERRED" + if data_transfer_response + and data_transfer_response.status_code == 202 + else "FAILED", "care_contexts": list( map( lambda context: {"id": context["careContextReference"]}, diff --git a/care/abdm/api/viewsets/consent.py b/care/abdm/api/viewsets/consent.py new file mode 100644 index 0000000000..4a695ada2a --- /dev/null +++ b/care/abdm/api/viewsets/consent.py @@ -0,0 +1,284 @@ +import logging + +from django_filters import rest_framework as filters +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.mixins import ListModelMixin, RetrieveModelMixin +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + +from care.abdm.api.serializers.consent import ConsentRequestSerializer +from care.abdm.api.viewsets.health_information import HealthInformationViewSet +from care.abdm.models.base import Status +from care.abdm.models.consent import ConsentArtefact, ConsentRequest +from care.abdm.service.gateway import Gateway +from care.utils.queryset.facility import get_facility_queryset +from config.auth_views import CaptchaRequiredException +from config.authentication import ABDMAuthentication +from config.ratelimit import ratelimit + +logger = logging.getLogger(__name__) + + +class ConsentRequestFilter(filters.FilterSet): + patient = filters.UUIDFilter( + field_name="patient_abha__patientregistration__external_id" + ) + health_id = filters.CharFilter(field_name="patient_abha__health_id") + ordering = filters.OrderingFilter( + fields=( + "created_date", + "updated_date", + ) + ) + facility = filters.UUIDFilter( + field_name="patient_abha__patientregistration__facility__external_id" + ) + + class Meta: + model = ConsentRequest + fields = ["patient", "health_id", "purpose"] + + +class ConsentViewSet(GenericViewSet, ListModelMixin, RetrieveModelMixin): + serializer_class = ConsentRequestSerializer + model = ConsentRequest + queryset = ConsentRequest.objects.all() + permission_classes = (IsAuthenticated,) + filter_backends = (filters.DjangoFilterBackend,) + filterset_class = ConsentRequestFilter + + def get_queryset(self): + queryset = self.queryset + facilities = get_facility_queryset(self.request.user) + return queryset.filter(requester__facility__in=facilities).distinct() + + def create(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + if ratelimit( + request, "consent__create", [serializer.validated_data["patient_abha"]] + ): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + + consent = ConsentRequest(**serializer.validated_data, requester=request.user) + + try: + response = Gateway().consent_requests__init(consent) + if response.status_code != 202: + return Response(response.json(), status=response.status_code) + except Exception as e: + logger.warning( + f"Error: ConsentViewSet::create failed to notify (consent_requests__init). Reason: {e}", + exc_info=True, + ) + return Response( + {"detail": "Failed to initialize consent request"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + consent.save() + return Response( + ConsentRequestSerializer(consent).data, status=status.HTTP_201_CREATED + ) + + @action(detail=True, methods=["GET"]) + def status(self, request, pk): + if ratelimit(request, "consent__status", [pk]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + + consent = self.queryset.filter(external_id=pk).first() + + if not consent: + return Response(status=status.HTTP_404_NOT_FOUND) + + response = Gateway().consent_requests__status(str(consent.consent_id)) + if response.status_code != 202: + return Response(response.json(), status=response.status_code) + + return Response( + ConsentRequestSerializer(consent).data, status=status.HTTP_200_OK + ) + + @action(detail=True, methods=["GET"]) + def fetch(self, request, pk): + if ratelimit(request, "consent__fetch", [pk]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + + consent = self.queryset.filter(external_id=pk).first() + + if not consent: + return Response(status=status.HTTP_404_NOT_FOUND) + + for artefact in consent.consent_artefacts.all(): + response = Gateway().consents__fetch(str(artefact.artefact_id)) + + if response.status_code != 202: + return Response(response.json(), status=response.status_code) + + return Response( + ConsentRequestSerializer(consent).data, status=status.HTTP_200_OK + ) + + def list(self, request, *args, **kwargs): + if ratelimit(request, "consent__list", [request.user.username]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + + return super().list(request, *args, **kwargs) + + def retrieve(self, request, *args, **kwargs): + if ratelimit(request, "consent__retrieve", [kwargs["pk"]]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + + return super().retrieve(request, *args, **kwargs) + + +class ConsentCallbackViewSet(GenericViewSet): + permission_classes = (IsAuthenticated,) + authentication_classes = [ABDMAuthentication] + + def consent_request__on_init(self, request): + data = request.data + consent = ConsentRequest.objects.filter( + external_id=data["resp"]["requestId"] + ).first() + + if not consent: + return Response(status=status.HTTP_404_NOT_FOUND) + + consent.consent_id = data["consentRequest"]["id"] + consent.save() + + return Response(status=status.HTTP_202_ACCEPTED) + + def consent_request__on_status(self, request): + data = request.data + consent = ConsentRequest.objects.filter( + consent_id=data["consentRequest"]["id"] + ).first() + + if not consent: + return Response(status=status.HTTP_404_NOT_FOUND) + + if "notification" not in data: + return Response(status=status.HTTP_202_ACCEPTED) + + if data["notification"]["status"] != Status.DENIED: + consent_artefacts = data["notification"]["consentArtefacts"] or [] + for artefact in consent_artefacts: + consent_artefact = ConsentArtefact.objects.filter( + external_id=artefact["id"] + ).first() + if not consent_artefact: + consent_artefact = ConsentArtefact( + external_id=artefact["id"], + consent_request=consent, + **consent.consent_details_dict(), + ) + + consent_artefact.status = data["notification"]["status"] + consent_artefact.save() + consent.status = data["notification"]["status"] + consent.save() + + return Response(status=status.HTTP_202_ACCEPTED) + + def consents__hiu__notify(self, request): + data = request.data + + if not data["notification"]["consentRequestId"]: + for artefact in data["notification"]["consentArtefacts"]: + consent_artefact = ConsentArtefact.objects.filter( + external_id=artefact["id"] + ).first() + + consent_artefact.status = Status.REVOKED + consent_artefact.save() + return Response(status=status.HTTP_202_ACCEPTED) + + consent = ConsentRequest.objects.filter( + consent_id=data["notification"]["consentRequestId"] + ).first() + + if not consent: + return Response(status=status.HTTP_404_NOT_FOUND) + + if data["notification"]["status"] != Status.DENIED: + consent_artefacts = data["notification"]["consentArtefacts"] or [] + for artefact in consent_artefacts: + consent_artefact = ConsentArtefact.objects.filter( + external_id=artefact["id"] + ).first() + if not consent_artefact: + consent_artefact = ConsentArtefact( + external_id=artefact["id"], + consent_request=consent, + **consent.consent_details_dict(), + ) + + consent_artefact.status = data["notification"]["status"] + consent_artefact.save() + consent.status = data["notification"]["status"] + consent.save() + + Gateway().consents__hiu__on_notify(consent, data["requestId"]) + + if data["notification"]["status"] == Status.GRANTED: + ConsentViewSet().fetch(request, consent.external_id) + + return Response(status=status.HTTP_202_ACCEPTED) + + def consents__on_fetch(self, request): + data = request.data["consent"] + artefact = ConsentArtefact.objects.filter( + external_id=data["consentDetail"]["consentId"] + ).first() + + if not artefact: + return Response(status=status.HTTP_404_NOT_FOUND) + + artefact.hip = data["consentDetail"]["hip"]["id"] + artefact.hiu = data["consentDetail"]["hiu"]["id"] + artefact.cm = data["consentDetail"]["consentManager"]["id"] + + artefact.care_contexts = data["consentDetail"]["careContexts"] + artefact.hi_types = data["consentDetail"]["hiTypes"] + + artefact.access_mode = data["consentDetail"]["permission"]["accessMode"] + artefact.from_time = data["consentDetail"]["permission"]["dateRange"]["from"] + artefact.to_time = data["consentDetail"]["permission"]["dateRange"]["to"] + artefact.expiry = data["consentDetail"]["permission"]["dataEraseAt"] + + artefact.frequency_unit = data["consentDetail"]["permission"]["frequency"][ + "unit" + ] + artefact.frequency_value = data["consentDetail"]["permission"]["frequency"][ + "value" + ] + artefact.frequency_repeats = data["consentDetail"]["permission"]["frequency"][ + "repeats" + ] + + artefact.signature = data["signature"] + artefact.save() + + HealthInformationViewSet().request(request, artefact.external_id) + + return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/health_information.py b/care/abdm/api/viewsets/health_information.py new file mode 100644 index 0000000000..a1b33b4476 --- /dev/null +++ b/care/abdm/api/viewsets/health_information.py @@ -0,0 +1,160 @@ +import json +import logging + +from django.db.models import Q +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + +from care.abdm.models.consent import ConsentArtefact +from care.abdm.service.gateway import Gateway +from care.abdm.utils.cipher import Cipher +from care.facility.models.file_upload import FileUpload +from config.auth_views import CaptchaRequiredException +from config.authentication import ABDMAuthentication +from config.ratelimit import ratelimit + +logger = logging.getLogger(__name__) + + +class HealthInformationViewSet(GenericViewSet): + permission_classes = (IsAuthenticated,) + + def retrieve(self, request, pk): + if ratelimit(request, "health_information__retrieve", [pk]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + + files = FileUpload.objects.filter( + Q(internal_name=f"{pk}.json") | Q(associating_id=pk), + file_type=FileUpload.FileType.ABDM_HEALTH_INFORMATION.value, + ) + + if files.count() == 0 or all([not file.upload_completed for file in files]): + return Response( + {"detail": "No Health Information found with the given id"}, + status=status.HTTP_404_NOT_FOUND, + ) + + if files.count() == 1: + file = files.first() + + if file.is_archived: + return Response( + { + "is_archived": True, + "archived_reason": file.archive_reason, + "archived_time": file.archived_datetime, + "detail": f"This file has been archived as {file.archive_reason} at {file.archived_datetime}", + }, + status=status.HTTP_404_NOT_FOUND, + ) + + contents = [] + for file in files: + if file.upload_completed: + content_type, content = file.file_contents() + contents.extend(content) + + return Response({"data": json.loads(content)}, status=status.HTTP_200_OK) + + @action(detail=True, methods=["POST"]) + def request(self, request, pk): + if ratelimit(request, "health_information__request", [pk]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + + artefact = ConsentArtefact.objects.filter(external_id=pk).first() + + if not artefact: + return Response( + {"detail": "No Consent artefact found with the given id"}, + status=status.HTTP_404_NOT_FOUND, + ) + + response = Gateway().health_information__cm__request(artefact) + if response.status_code != 202: + return Response(response.json(), status=response.status_code) + + return Response(status=status.HTTP_200_OK) + + +class HealthInformationCallbackViewSet(GenericViewSet): + permission_classes = (IsAuthenticated,) + authentication_classes = [ABDMAuthentication] + + def health_information__hiu__on_request(self, request): + data = request.data + + artefact = ConsentArtefact.objects.filter( + consent_id=data["resp"]["requestId"] + ).first() + + if not artefact: + return Response(status=status.HTTP_404_NOT_FOUND) + + if "hiRequest" in data: + artefact.consent_id = data["hiRequest"]["transactionId"] + artefact.save() + + return Response(status=status.HTTP_202_ACCEPTED) + + def health_information__transfer(self, request): + data = request.data + + artefact = ConsentArtefact.objects.filter( + consent_id=data["transactionId"] + ).first() + + if not artefact: + return Response(status=status.HTTP_404_NOT_FOUND) + + cipher = Cipher( + data["keyMaterial"]["dhPublicKey"]["keyValue"], + data["keyMaterial"]["nonce"], + artefact.key_material_private_key, + artefact.key_material_public_key, + artefact.key_material_nonce, + ) + entries = [] + for entry in data["entries"]: + if "content" in entry: + entries.append( + { + "content": cipher.decrypt(entry["content"]), + "care_context_reference": entry["careContextReference"], + } + ) + + if "link" in entry: + # TODO: handle link + pass + + file = FileUpload( + internal_name=f"{artefact.external_id}.json", + file_type=FileUpload.FileType.ABDM_HEALTH_INFORMATION.value, + associating_id=artefact.consent_request.external_id, + ) + file.put_object(json.dumps(entries), ContentType="application/json") + file.upload_completed = True + file.save() + + try: + Gateway().health_information__notify(artefact) + except Exception as e: + logger.warning( + f"Error: health_information__transfer::post failed to notify (health-information/notify). Reason: {e}", + exc_info=True, + ) + return Response( + {"detail": "Failed to notify (health-information/notify)"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/patients.py b/care/abdm/api/viewsets/patients.py new file mode 100644 index 0000000000..2bed1f63ee --- /dev/null +++ b/care/abdm/api/viewsets/patients.py @@ -0,0 +1,77 @@ +import json + +from django.core.cache import cache +from django.db.models import Q +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + +from care.abdm.models.abha_number import AbhaNumber +from care.abdm.service.gateway import Gateway +from care.utils.notification_handler import send_webpush +from config.auth_views import CaptchaRequiredException +from config.authentication import ABDMAuthentication +from config.ratelimit import ratelimit + + +class PatientsViewSet(GenericViewSet): + permission_classes = (IsAuthenticated,) + + @action(detail=False, methods=["POST"]) + def find(self, request): + identifier = request.data["id"] + + if ratelimit(request, "patients__find", [identifier]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + + abha_object = AbhaNumber.objects.filter( + Q(abha_number=identifier) | Q(health_id=identifier) + ).first() + + if not abha_object: + return Response( + {"error": "Patient with given id not found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + response = Gateway().patients__find(abha_object) + if response.status_code != 202: + return Response(response.text, status=status.HTTP_400_BAD_REQUEST) + + cache.set( + f"abdm__patients__find__{json.loads(response.request.body)['requestId']}", + request.user.username, + timeout=60 * 60, + ) + return Response( + {"detail": "Requested ABDM for patient details"}, status=status.HTTP_200_OK + ) + + +class PatientsCallbackViewSet(GenericViewSet): + permission_classes = (IsAuthenticated,) + authentication_classes = [ABDMAuthentication] + + def patients__on_find(self, request): + username = cache.get( + f"abdm__patients__find__{request.data['resp']['requestId']}" + ) + + if username: + send_webpush( + username=username, + message=json.dumps( + { + "type": "MESSAGE", + "from": "patients/on_find", + "message": request.data, + } + ), + ) + + return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/migrations/0011_alter_abhanumber_abha_number_and_more.py b/care/abdm/migrations/0011_alter_abhanumber_abha_number_and_more.py new file mode 100644 index 0000000000..905cd09719 --- /dev/null +++ b/care/abdm/migrations/0011_alter_abhanumber_abha_number_and_more.py @@ -0,0 +1,429 @@ +# Generated by Django 4.2.2 on 2023-10-01 16:44 + +import uuid + +import django.contrib.postgres.fields +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + +import care.abdm.models.consent +import care.utils.models.validators + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("abdm", "0010_healthfacility_registered"), + ] + + operations = [ + migrations.AlterField( + model_name="abhanumber", + name="abha_number", + field=models.TextField(blank=True, null=True, unique=True), + ), + migrations.AlterField( + model_name="abhanumber", + name="health_id", + field=models.TextField(blank=True, null=True, unique=True), + ), + migrations.CreateModel( + name="ConsentRequest", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "external_id", + models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + ( + "created_date", + models.DateTimeField(auto_now_add=True, db_index=True, null=True), + ), + ( + "modified_date", + models.DateTimeField(auto_now=True, db_index=True, null=True), + ), + ("deleted", models.BooleanField(db_index=True, default=False)), + ("consent_id", models.UUIDField(blank=True, null=True, unique=True)), + ( + "care_contexts", + models.JSONField( + default=list, + validators=[ + care.utils.models.validators.JSONFieldSchemaValidator( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "content": [ + { + "additionalProperties": False, + "properties": { + "careContextReference": { + "type": "string" + }, + "patientReference": {"type": "string"}, + }, + "required": [ + "patientReference", + "careContextReference", + ], + "type": "object", + } + ], + "type": "array", + } + ) + ], + ), + ), + ( + "purpose", + models.CharField( + choices=[ + ("CAREMGT", "Care Management"), + ("BTG", "Break The Glass"), + ("PUBHLTH", "Public Health"), + ("HPAYMT", "Healthcare Payment"), + ("DSRCH", "Disease Specific Healthcare Research"), + ("PATRQT", "Self Requested"), + ], + default="CAREMGT", + max_length=20, + ), + ), + ( + "hi_types", + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[ + ("Prescription", "Prescription"), + ("DiagnosticReport", "Diagnostic Report"), + ("OPConsultation", "Op Consultation"), + ("DischargeSummary", "Discharge Summary"), + ("ImmunizationRecord", "Immunization Record"), + ("HealthDocumentRecord", "Record Artifact"), + ("WellnessRecord", "Wellness Record"), + ], + max_length=20, + ), + default=list, + size=None, + ), + ), + ("hip", models.CharField(blank=True, max_length=50, null=True)), + ("hiu", models.CharField(blank=True, max_length=50, null=True)), + ( + "access_mode", + models.CharField( + choices=[ + ("VIEW", "View"), + ("STORE", "Store"), + ("QUERY", "Query"), + ("STREAM", "Stream"), + ], + default="VIEW", + max_length=20, + ), + ), + ( + "from_time", + models.DateTimeField( + blank=True, + default=care.abdm.models.consent.Consent.default_from_time, + null=True, + ), + ), + ( + "to_time", + models.DateTimeField( + blank=True, + default=care.abdm.models.consent.Consent.default_to_time, + null=True, + ), + ), + ( + "expiry", + models.DateTimeField( + blank=True, + default=care.abdm.models.consent.Consent.default_expiry, + null=True, + ), + ), + ( + "frequency_unit", + models.CharField( + choices=[ + ("HOUR", "Hour"), + ("DAY", "Day"), + ("WEEK", "Week"), + ("MONTH", "Month"), + ("YEAR", "Year"), + ], + default="HOUR", + max_length=20, + ), + ), + ( + "frequency_value", + models.PositiveSmallIntegerField( + default=1, + validators=[django.core.validators.MinValueValidator(1)], + ), + ), + ("frequency_repeats", models.PositiveSmallIntegerField(default=0)), + ( + "patient_abha", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="abdm.abhanumber", + to_field="health_id", + ), + ), + ( + "requester", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="ConsentArtefact", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "external_id", + models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + ( + "created_date", + models.DateTimeField(auto_now_add=True, db_index=True, null=True), + ), + ( + "modified_date", + models.DateTimeField(auto_now=True, db_index=True, null=True), + ), + ("deleted", models.BooleanField(db_index=True, default=False)), + ("consent_id", models.UUIDField(blank=True, null=True, unique=True)), + ( + "care_contexts", + models.JSONField( + default=list, + validators=[ + care.utils.models.validators.JSONFieldSchemaValidator( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "content": [ + { + "additionalProperties": False, + "properties": { + "careContextReference": { + "type": "string" + }, + "patientReference": {"type": "string"}, + }, + "required": [ + "patientReference", + "careContextReference", + ], + "type": "object", + } + ], + "type": "array", + } + ) + ], + ), + ), + ( + "purpose", + models.CharField( + choices=[ + ("CAREMGT", "Care Management"), + ("BTG", "Break The Glass"), + ("PUBHLTH", "Public Health"), + ("HPAYMT", "Healthcare Payment"), + ("DSRCH", "Disease Specific Healthcare Research"), + ("PATRQT", "Self Requested"), + ], + default="CAREMGT", + max_length=20, + ), + ), + ( + "hi_types", + django.contrib.postgres.fields.ArrayField( + base_field=models.CharField( + choices=[ + ("Prescription", "Prescription"), + ("DiagnosticReport", "Diagnostic Report"), + ("OPConsultation", "Op Consultation"), + ("DischargeSummary", "Discharge Summary"), + ("ImmunizationRecord", "Immunization Record"), + ("HealthDocumentRecord", "Record Artifact"), + ("WellnessRecord", "Wellness Record"), + ], + max_length=20, + ), + default=list, + size=None, + ), + ), + ("hip", models.CharField(blank=True, max_length=50, null=True)), + ("hiu", models.CharField(blank=True, max_length=50, null=True)), + ( + "access_mode", + models.CharField( + choices=[ + ("VIEW", "View"), + ("STORE", "Store"), + ("QUERY", "Query"), + ("STREAM", "Stream"), + ], + default="VIEW", + max_length=20, + ), + ), + ( + "from_time", + models.DateTimeField( + blank=True, + default=care.abdm.models.consent.Consent.default_from_time, + null=True, + ), + ), + ( + "to_time", + models.DateTimeField( + blank=True, + default=care.abdm.models.consent.Consent.default_to_time, + null=True, + ), + ), + ( + "expiry", + models.DateTimeField( + blank=True, + default=care.abdm.models.consent.Consent.default_expiry, + null=True, + ), + ), + ( + "frequency_unit", + models.CharField( + choices=[ + ("HOUR", "Hour"), + ("DAY", "Day"), + ("WEEK", "Week"), + ("MONTH", "Month"), + ("YEAR", "Year"), + ], + default="HOUR", + max_length=20, + ), + ), + ( + "frequency_value", + models.PositiveSmallIntegerField( + default=1, + validators=[django.core.validators.MinValueValidator(1)], + ), + ), + ("frequency_repeats", models.PositiveSmallIntegerField(default=0)), + ( + "status", + models.CharField( + choices=[ + ("REQUESTED", "Requested"), + ("GRANTED", "Granted"), + ("DENIED", "Denied"), + ("EXPIRED", "Expired"), + ("REVOKED", "Revoked"), + ], + default="REQUESTED", + max_length=20, + ), + ), + ("cm", models.CharField(blank=True, max_length=50, null=True)), + ( + "key_material_algorithm", + models.CharField( + blank=True, default="ECDH", max_length=20, null=True + ), + ), + ( + "key_material_curve", + models.CharField( + blank=True, default="Curve25519", max_length=20, null=True + ), + ), + ( + "key_material_public_key", + models.CharField(blank=True, max_length=100, null=True), + ), + ( + "key_material_private_key", + models.CharField(blank=True, max_length=200, null=True), + ), + ( + "key_material_nonce", + models.CharField(blank=True, max_length=100, null=True), + ), + ("signature", models.TextField(blank=True, null=True)), + ( + "consent_request", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="consent_artefacts", + to="abdm.consentrequest", + to_field="consent_id", + ), + ), + ( + "patient_abha", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="abdm.abhanumber", + to_field="health_id", + ), + ), + ( + "requester", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/care/abdm/migrations/0012_consentrequest_status.py b/care/abdm/migrations/0012_consentrequest_status.py new file mode 100644 index 0000000000..43bf1ecb62 --- /dev/null +++ b/care/abdm/migrations/0012_consentrequest_status.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.2 on 2023-12-02 04:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("abdm", "0011_alter_abhanumber_abha_number_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="consentrequest", + name="status", + field=models.CharField( + choices=[ + ("REQUESTED", "Requested"), + ("GRANTED", "Granted"), + ("DENIED", "Denied"), + ("EXPIRED", "Expired"), + ("REVOKED", "Revoked"), + ], + default="REQUESTED", + max_length=20, + ), + ), + ] diff --git a/care/abdm/models/__init__.py b/care/abdm/models/__init__.py new file mode 100644 index 0000000000..5b7edbb6fb --- /dev/null +++ b/care/abdm/models/__init__.py @@ -0,0 +1,3 @@ +from .abha_number import * # noqa +from .consent import * # noqa +from .health_facility import * # noqa diff --git a/care/abdm/models.py b/care/abdm/models/abha_number.py similarity index 63% rename from care/abdm/models.py rename to care/abdm/models/abha_number.py index a0fdf7e647..30579dea3d 100644 --- a/care/abdm/models.py +++ b/care/abdm/models/abha_number.py @@ -1,16 +1,11 @@ -# from django.db import models - -# Create your models here. - from django.db import models -from care.abdm.permissions import HealthFacilityPermissions from care.utils.models.base import BaseModel class AbhaNumber(BaseModel): - abha_number = models.TextField(null=True, blank=True) - health_id = models.TextField(null=True, blank=True) + abha_number = models.TextField(null=True, blank=True, unique=True) + health_id = models.TextField(null=True, blank=True, unique=True) name = models.TextField(null=True, blank=True) first_name = models.TextField(null=True, blank=True) @@ -36,14 +31,3 @@ class AbhaNumber(BaseModel): def __str__(self): return f"{self.pk} {self.abha_number}" - - -class HealthFacility(BaseModel, HealthFacilityPermissions): - hf_id = models.CharField(max_length=50, unique=True) - registered = models.BooleanField(default=False) - facility = models.OneToOneField( - "facility.Facility", on_delete=models.PROTECT, to_field="external_id" - ) - - def __str__(self): - return self.hf_id + " " + self.facility.name diff --git a/care/abdm/models/base.py b/care/abdm/models/base.py new file mode 100644 index 0000000000..82ff16ca85 --- /dev/null +++ b/care/abdm/models/base.py @@ -0,0 +1,43 @@ +from django.db import models + + +class Status(models.TextChoices): + REQUESTED = "REQUESTED" + GRANTED = "GRANTED" + DENIED = "DENIED" + EXPIRED = "EXPIRED" + REVOKED = "REVOKED" + + +class Purpose(models.TextChoices): + CARE_MANAGEMENT = "CAREMGT" + BREAK_THE_GLASS = "BTG" + PUBLIC_HEALTH = "PUBHLTH" + HEALTHCARE_PAYMENT = "HPAYMT" + DISEASE_SPECIFIC_HEALTHCARE_RESEARCH = "DSRCH" + SELF_REQUESTED = "PATRQT" + + +class HealthInformationTypes(models.TextChoices): + PRESCRIPTION = "Prescription" + DIAGNOSTIC_REPORT = "DiagnosticReport" + OP_CONSULTATION = "OPConsultation" + DISCHARGE_SUMMARY = "DischargeSummary" + IMMUNIZATION_RECORD = "ImmunizationRecord" + RECORD_ARTIFACT = "HealthDocumentRecord" + WELLNESS_RECORD = "WellnessRecord" + + +class AccessMode(models.TextChoices): + VIEW = "VIEW" + STORE = "STORE" + QUERY = "QUERY" + STREAM = "STREAM" + + +class FrequencyUnit(models.TextChoices): + HOUR = "HOUR" + DAY = "DAY" + WEEK = "WEEK" + MONTH = "MONTH" + YEAR = "YEAR" diff --git a/care/abdm/models/consent.py b/care/abdm/models/consent.py new file mode 100644 index 0000000000..aafe01d726 --- /dev/null +++ b/care/abdm/models/consent.py @@ -0,0 +1,162 @@ +from django.contrib.postgres.fields import ArrayField +from django.core.validators import MinValueValidator +from django.db import models +from django.utils import timezone + +from care.abdm.models import AbhaNumber +from care.abdm.models.base import ( + AccessMode, + FrequencyUnit, + HealthInformationTypes, + Purpose, + Status, +) +from care.abdm.models.json_schema import CARE_CONTEXTS +from care.abdm.utils.cipher import Cipher +from care.facility.models.file_upload import FileUpload +from care.users.models import User +from care.utils.models.base import BaseModel +from care.utils.models.validators import JSONFieldSchemaValidator + + +class Consent(BaseModel): + class Meta: + abstract = True + + def default_expiry(): + return timezone.now() + timezone.timedelta(days=30) + + def default_from_time(): + return timezone.now() - timezone.timedelta(days=30) + + def default_to_time(): + return timezone.now() + + consent_id = models.UUIDField(null=True, blank=True, unique=True) + + patient_abha = models.ForeignKey( + AbhaNumber, on_delete=models.PROTECT, to_field="health_id" + ) + + care_contexts = models.JSONField( + default=list, validators=[JSONFieldSchemaValidator(CARE_CONTEXTS)] + ) + + status = models.CharField( + choices=Status.choices, max_length=20, default=Status.REQUESTED.value + ) + purpose = models.CharField( + choices=Purpose.choices, max_length=20, default=Purpose.CARE_MANAGEMENT.value + ) + hi_types = ArrayField( + models.CharField(choices=HealthInformationTypes.choices, max_length=20), + default=list, + ) + + hip = models.CharField(max_length=50, null=True, blank=True) + hiu = models.CharField(max_length=50, null=True, blank=True) + + requester = models.ForeignKey( + User, on_delete=models.SET_NULL, null=True, blank=True + ) + + access_mode = models.CharField( + choices=AccessMode.choices, max_length=20, default=AccessMode.VIEW.value + ) + from_time = models.DateTimeField(null=True, blank=True, default=default_from_time) + to_time = models.DateTimeField(null=True, blank=True, default=default_to_time) + expiry = models.DateTimeField(null=True, blank=True, default=default_expiry) + + frequency_unit = models.CharField( + choices=FrequencyUnit.choices, max_length=20, default=FrequencyUnit.HOUR.value + ) + frequency_value = models.PositiveSmallIntegerField( + default=1, validators=[MinValueValidator(1)] + ) + frequency_repeats = models.PositiveSmallIntegerField(default=0) + + def consent_details_dict(self): + return { + "patient_abha": self.patient_abha, + "care_contexts": self.care_contexts, + "status": self.status, + "purpose": self.purpose, + "hi_types": self.hi_types, + "hip": self.hip, + "hiu": self.hiu, + "requester": self.requester, + "access_mode": self.access_mode, + "from_time": self.from_time, + "to_time": self.to_time, + "expiry": self.expiry, + "frequency_unit": self.frequency_unit, + "frequency_value": self.frequency_value, + "frequency_repeats": self.frequency_repeats, + } + + +class ConsentRequest(Consent): + @property + def request_id(self): + return self.consent_id + + +class ConsentArtefact(Consent): + @property + def artefact_id(self): + return self.external_id + + @property + def transaction_id(self): + return self.consent_id + + def save(self, *args, **kwargs): + if self.key_material_private_key is None: + cipher = Cipher("", "") + key_material = cipher.generate_key_pair() + + self.key_material_algorithm = "ECDH" + self.key_material_curve = "Curve25519" + self.key_material_public_key = key_material["publicKey"] + self.key_material_private_key = key_material["privateKey"] + self.key_material_nonce = key_material["nonce"] + + if self.status in [Status.REVOKED.value, Status.EXPIRED.value]: + file = FileUpload.objects.filter( + internal_name=f"{self.external_id}.json", + file_type=FileUpload.FileType.ABDM_HEALTH_INFORMATION.value, + ).first() + + if file: + file.is_archived = True + file.archived_datetime = timezone.now() + file.archive_reason = self.status + file.save() + + return super().save(*args, **kwargs) + + consent_request = models.ForeignKey( + ConsentRequest, + on_delete=models.PROTECT, + to_field="consent_id", + null=True, + blank=True, + related_name="consent_artefacts", + ) + + cm = models.CharField(max_length=50, null=True, blank=True) + + key_material_algorithm = models.CharField( + max_length=20, + null=True, + blank=True, + default="ECDH", + ) + key_material_curve = models.CharField( + max_length=20, null=True, blank=True, default="Curve25519" + ) + key_material_public_key = models.CharField(max_length=100, null=True, blank=True) + key_material_private_key = models.CharField(max_length=200, null=True, blank=True) + key_material_nonce = models.CharField(max_length=100, null=True, blank=True) + + signature = models.TextField(null=True, blank=True) diff --git a/care/abdm/models/health_facility.py b/care/abdm/models/health_facility.py new file mode 100644 index 0000000000..a38bf1367c --- /dev/null +++ b/care/abdm/models/health_facility.py @@ -0,0 +1,15 @@ +from django.db import models + +from care.abdm.models.permissions.health_facility import HealthFacilityPermissions +from care.utils.models.base import BaseModel + + +class HealthFacility(BaseModel, HealthFacilityPermissions): + hf_id = models.CharField(max_length=50, unique=True) + registered = models.BooleanField(default=False) + facility = models.OneToOneField( + "facility.Facility", on_delete=models.PROTECT, to_field="external_id" + ) + + def __str__(self): + return f"{self.hf_id} {self.facility}" diff --git a/care/abdm/models/json_schema.py b/care/abdm/models/json_schema.py new file mode 100644 index 0000000000..081b65cc7c --- /dev/null +++ b/care/abdm/models/json_schema.py @@ -0,0 +1,15 @@ +CARE_CONTEXTS = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "content": [ + { + "type": "object", + "properties": { + "patientReference": {"type": "string"}, + "careContextReference": {"type": "string"}, + }, + "additionalProperties": False, + "required": ["patientReference", "careContextReference"], + } + ], +} diff --git a/care/abdm/permissions.py b/care/abdm/models/permissions/health_facility.py similarity index 100% rename from care/abdm/permissions.py rename to care/abdm/models/permissions/health_facility.py diff --git a/care/abdm/service/gateway.py b/care/abdm/service/gateway.py new file mode 100644 index 0000000000..6c817bdb7f --- /dev/null +++ b/care/abdm/service/gateway.py @@ -0,0 +1,214 @@ +import uuid +from datetime import datetime, timezone + +from django.conf import settings +from django.db.models import Q + +from care.abdm.models.abha_number import AbhaNumber +from care.abdm.models.base import Purpose +from care.abdm.models.consent import ConsentArtefact, ConsentRequest +from care.abdm.service.request import Request + + +class Gateway: + def __init__(self): + self.request = Request(settings.ABDM_URL + "/gateway") + + def consent_requests__init(self, consent: ConsentRequest): + data = { + "requestId": str(consent.external_id), + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ), + "consent": { + "purpose": { + "text": Purpose(consent.purpose).label, + "code": Purpose(consent.purpose).value, + }, + "patient": {"id": consent.patient_abha.health_id}, + "hiu": { + "id": self.get_hf_id_by_health_id(consent.patient_abha.health_id) + }, + "requester": { + "name": f"{consent.requester.REVERSE_TYPE_MAP[consent.requester.user_type]}, {consent.requester.first_name} {consent.requester.last_name}", + "identifier": { + "type": "Care Username", + "value": consent.requester.username, + "system": settings.CURRENT_DOMAIN, + }, + }, + "hiTypes": consent.hi_types, + "permission": { + "accessMode": consent.access_mode, + "dateRange": { + "from": consent.from_time.astimezone(timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ), + "to": consent.to_time.astimezone(timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ), + }, + "dataEraseAt": consent.expiry.astimezone(timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ), + "frequency": { + "unit": consent.frequency_unit, + "value": consent.frequency_value, + "repeats": consent.frequency_repeats, + }, + }, + }, + } + + path = "/v0.5/consent-requests/init" + return self.request.post(path, data, headers={"X-CM-ID": settings.X_CM_ID}) + + def consent_requests__status(self, consent_request_id: str): + data = { + "requestId": str(uuid.uuid4()), + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ), + "consentRequestId": consent_request_id, + } + + return self.request.post( + "/v0.5/consent-requests/status", data, headers={"X-CM-ID": settings.X_CM_ID} + ) + + def consents__hiu__on_notify(self, consent: ConsentRequest, request_id: str): + data = { + "requestId": str(uuid.uuid4()), + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ), + "resp": {"requestId": request_id}, + } + + if len(consent.consent_artefacts.all()): + data["acknowledgement"] = [] + + for aretefact in consent.consent_artefacts.all(): + data["acknowledgement"].append( + { + "consentId": str(aretefact.artefact_id), + "status": "OK", + } + ) + + return self.request.post( + "/v0.5/consents/hiu/on-notify", data, headers={"X-CM-ID": settings.X_CM_ID} + ) + + def consents__fetch(self, consent_artefact_id: str): + data = { + "requestId": str(uuid.uuid4()), + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ), + "consentId": consent_artefact_id, + } + + return self.request.post( + "/v0.5/consents/fetch", data, headers={"X-CM-ID": settings.X_CM_ID} + ) + + def health_information__cm__request(self, artefact: ConsentArtefact): + request_id = str(uuid.uuid4()) + artefact.consent_id = request_id + artefact.save() + + data = { + "requestId": request_id, + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ), + "hiRequest": { + "consent": {"id": str(artefact.artefact_id)}, + "dataPushUrl": settings.BACKEND_DOMAIN + + "/v0.5/health-information/transfer", + "keyMaterial": { + "cryptoAlg": artefact.key_material_algorithm, + "curve": artefact.key_material_curve, + "dhPublicKey": { + "expiry": artefact.expiry.astimezone(timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ), + "parameters": f"{artefact.key_material_curve}/{artefact.key_material_algorithm}", + "keyValue": artefact.key_material_public_key, + }, + "nonce": artefact.key_material_nonce, + }, + "dateRange": { + "from": artefact.from_time.strftime("%Y-%m-%dT%H:%M:%S.000Z"), + "to": artefact.to_time.strftime("%Y-%m-%dT%H:%M:%S.000Z"), + }, + }, + } + + return self.request.post( + "/v0.5/health-information/cm/request", + data, + headers={"X-CM-ID": settings.X_CM_ID}, + ) + + def get_hf_id_by_health_id(self, health_id): + abha_number = AbhaNumber.objects.filter( + Q(abha_number=health_id) | Q(health_id=health_id) + ).first() + if not abha_number: + raise Exception("No ABHA Number found") + + patient_facility = abha_number.patientregistration.last_consultation.facility + if not hasattr(patient_facility, "healthfacility"): + raise Exception("Health Facility not linked") + + return patient_facility.healthfacility.hf_id + + def health_information__notify(self, artefact: ConsentArtefact): + data = { + "requestId": str(uuid.uuid4()), + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ), + "notification": { + "consentId": str(artefact.artefact_id), + "transactionId": str(artefact.transaction_id), + "doneAt": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ), + "notifier": { + "type": "HIU", + "id": self.get_hf_id_by_health_id(artefact.patient_abha.health_id), + }, + "statusNotification": { + "sessionStatus": "TRANSFERRED", + "hipId": artefact.hip, + }, + }, + } + + return self.request.post( + "/v0.5/health-information/notify", + data, + headers={"X-CM-ID": settings.X_CM_ID}, + ) + + def patients__find(self, abha_number: AbhaNumber): + data = { + "requestId": str(uuid.uuid4()), + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ), + "query": { + "patient": {"id": abha_number.health_id}, + "requester": { + "type": "HIU", + "id": abha_number.patientregistration.facility.healthfacility.hf_id, + }, + }, + } + + return self.request.post( + "/v0.5/patients/find", data, headers={"X-CM-ID": settings.X_CM_ID} + ) diff --git a/care/abdm/service/request.py b/care/abdm/service/request.py new file mode 100644 index 0000000000..18c362d61c --- /dev/null +++ b/care/abdm/service/request.py @@ -0,0 +1,92 @@ +import json +import logging + +import requests +from django.conf import settings +from django.core.cache import cache + +ABDM_GATEWAY_URL = settings.ABDM_URL + "/gateway" +ABDM_TOKEN_URL = ABDM_GATEWAY_URL + "/v0.5/sessions" +ABDM_TOKEN_CACHE_KEY = "abdm_token" + +logger = logging.getLogger(__name__) + + +class Request: + def __init__(self, base_url): + self.url = base_url + + def user_header(self, user_token): + if not user_token: + return {} + return {"X-Token": "Bearer " + user_token} + + def auth_header(self): + token = cache.get(ABDM_TOKEN_CACHE_KEY) + if not token: + data = json.dumps( + { + "clientId": settings.ABDM_CLIENT_ID, + "clientSecret": settings.ABDM_CLIENT_SECRET, + } + ) + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + logger.info("No Token in Cache") + response = requests.post(ABDM_TOKEN_URL, data=data, headers=headers) + + if response.status_code < 300: + if response.headers["Content-Type"] != "application/json": + logger.info( + f"Unsupported Content-Type: {response.headers['Content-Type']}" + ) + logger.info(f"Response: {response.text}") + + return None + else: + data = response.json() + token = data["accessToken"] + expires_in = data["expiresIn"] + + logger.info(f"New Token: {token}") + logger.info(f"Expires in: {expires_in}") + + cache.set(ABDM_TOKEN_CACHE_KEY, token, expires_in) + else: + logger.info(f"Bad Response: {response.text}") + return None + + return {"Authorization": f"Bearer {token}"} + + def headers(self, additional_headers=None, auth=None): + return { + "Content-Type": "application/json", + "Accept": "*/*", + **(additional_headers or {}), + **(self.user_header(auth) or {}), + **(self.auth_header() or {}), + } + + def get(self, path, params=None, headers=None, auth=None): + url = self.url + path + headers = self.headers(headers, auth) + + logger.info(f"GET: {url}") + response = requests.get(url, headers=headers, params=params) + logger.info(f"{response.status_code} Response: {response.text}") + + return response + + def post(self, path, data=None, headers=None, auth=None): + url = self.url + path + payload = json.dumps(data) + headers = self.headers(headers, auth) + + logger.info(f"POST: {url}, {headers}, {data}") + response = requests.post(url, data=payload, headers=headers) + logger.info(f"{response.status_code} Response: {response.text}") + + return response diff --git a/care/abdm/urls.py b/care/abdm/urls.py index a6efbe58ea..bf4eb8a5d9 100644 --- a/care/abdm/urls.py +++ b/care/abdm/urls.py @@ -13,8 +13,11 @@ OnInitView, RequestDataView, ) +from care.abdm.api.viewsets.consent import ConsentCallbackViewSet +from care.abdm.api.viewsets.health_information import HealthInformationCallbackViewSet from care.abdm.api.viewsets.hip import HipViewSet from care.abdm.api.viewsets.monitoring import HeartbeatView +from care.abdm.api.viewsets.patients import PatientsCallbackViewSet from care.abdm.api.viewsets.status import NotifyView as PatientStatusNotifyView from care.abdm.api.viewsets.status import SMSOnNotifyView @@ -31,6 +34,45 @@ def __init__(self): abdm_urlpatterns = [ *abdm_router.urls, + path( + "v0.5/consent-requests/on-init", + ConsentCallbackViewSet.as_view({"post": "consent_request__on_init"}), + name="abdm__consent_request__on_init", + ), + path( + "v0.5/consent-requests/on-status", + ConsentCallbackViewSet.as_view({"post": "consent_request__on_status"}), + name="abdm__consent_request__on_status", + ), + path( + "v0.5/consents/hiu/notify", + ConsentCallbackViewSet.as_view({"post": "consents__hiu__notify"}), + name="abdm__consents__hiu__notify", + ), + path( + "v0.5/consents/on-fetch", + ConsentCallbackViewSet.as_view({"post": "consents__on_fetch"}), + name="abdm__consents__on_fetch", + ), + path( + "v0.5/health-information/hiu/on-request", + HealthInformationCallbackViewSet.as_view( + {"post": "health_information__hiu__on_request"} + ), + name="abdm__health_information__hiu__on_request", + ), + path( + "v0.5/health-information/transfer", + HealthInformationCallbackViewSet.as_view( + {"post": "health_information__transfer"} + ), + name="abdm__health_information__transfer", + ), + path( + "v0.5/patients/on-find", + PatientsCallbackViewSet.as_view({"post": "patients__on_find"}), + name="abdm__patients__on_find", + ), path( "v0.5/users/auth/on-fetch-modes", OnFetchView.as_view(), diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 6c060d1835..f49e2d6027 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -12,6 +12,7 @@ from django.db.models import Q from care.abdm.models import AbhaNumber +from care.abdm.service.request import Request from care.facility.models.patient_consultation import PatientConsultation GATEWAY_API_URL = settings.ABDM_URL @@ -676,7 +677,15 @@ def on_data_request(self, data): return response def data_transfer(self, data): - headers = {"Content-Type": "application/json"} + auth_header = Request("").auth_header() + + if not auth_header: + return None + + headers = { + "Content-Type": "application/json", + **auth_header, + } payload = { "pageNumber": 1, @@ -718,8 +727,8 @@ def data_notify(self, data): datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") ), "statusNotification": { - "sessionStatus": "TRANSFERRED", - "hipId": self.get_hip_id_by_health_id(data["healthId"]), + "sessionStatus": data["session_status"], + "hipId": self.get_hip_id_by_health_id(data["health_id"]), "statusResponses": list( map( lambda context: { diff --git a/care/abdm/utils/cipher.py b/care/abdm/utils/cipher.py index 2401d1e1ed..5a48430956 100644 --- a/care/abdm/utils/cipher.py +++ b/care/abdm/utils/cipher.py @@ -7,13 +7,20 @@ class Cipher: server_url = settings.FIDELIUS_URL - def __init__(self, reciever_public_key, reciever_nonce): - self.reciever_public_key = reciever_public_key - self.reciever_nonce = reciever_nonce - - self.sender_private_key = None - self.sender_public_key = None - self.sender_nonce = None + def __init__( + self, + external_public_key, + external_nonce, + internal_private_key=None, + internal_public_key=None, + internal_nonce=None, + ): + self.external_public_key = external_public_key + self.external_nonce = external_nonce + + self.internal_private_key = internal_private_key + self.internal_public_key = internal_public_key + self.internal_nonce = internal_nonce self.key_to_share = None @@ -23,16 +30,16 @@ def generate_key_pair(self): if response.status_code == 200: key_material = response.json() - self.sender_private_key = key_material["privateKey"] - self.sender_public_key = key_material["publicKey"] - self.sender_nonce = key_material["nonce"] + self.internal_private_key = key_material["privateKey"] + self.internal_public_key = key_material["publicKey"] + self.internal_nonce = key_material["nonce"] return key_material return None def encrypt(self, paylaod): - if not self.sender_private_key: + if not self.internal_private_key: key_material = self.generate_key_pair() if not key_material: @@ -43,11 +50,11 @@ def encrypt(self, paylaod): headers={"Content-Type": "application/json"}, data=json.dumps( { - "receiverPublicKey": self.reciever_public_key, - "receiverNonce": self.reciever_nonce, - "senderPrivateKey": self.sender_private_key, - "senderPublicKey": self.sender_public_key, - "senderNonce": self.sender_nonce, + "receiverPublicKey": self.external_public_key, + "receiverNonce": self.external_nonce, + "senderPrivateKey": self.internal_private_key, + "senderPublicKey": self.internal_public_key, + "senderNonce": self.internal_nonce, "plainTextData": paylaod, } ), @@ -60,7 +67,29 @@ def encrypt(self, paylaod): return { "public_key": self.key_to_share, "data": data["encryptedData"], - "nonce": self.sender_nonce, + "nonce": self.internal_nonce, } return None + + def decrypt(self, paylaod): + response = requests.post( + f"{self.server_url}/decrypt", + headers={"Content-Type": "application/json"}, + data=json.dumps( + { + "receiverPrivateKey": self.internal_private_key, + "receiverNonce": self.internal_nonce, + "senderPublicKey": self.external_public_key, + "senderNonce": self.external_nonce, + "encryptedData": paylaod, + } + ), + ) + + if response.status_code == 200: + data = response.json() + + return data["decryptedData"] + + return None diff --git a/care/facility/api/serializers/daily_round.py b/care/facility/api/serializers/daily_round.py index 5b9a005f0d..148cbdf94d 100644 --- a/care/facility/api/serializers/daily_round.py +++ b/care/facility/api/serializers/daily_round.py @@ -1,15 +1,11 @@ from datetime import timedelta -from uuid import uuid4 from django.db import transaction -from django.utils import timezone from django.utils.timezone import localtime, now from rest_framework import serializers from rest_framework.exceptions import ValidationError from care.facility.events.handler import create_consultation_events - -# from care.facility.api.serializers.bed import BedSerializer from care.facility.models import ( CATEGORY_CHOICES, COVID_CATEGORY_CHOICES, @@ -18,11 +14,7 @@ from care.facility.models.bed import Bed from care.facility.models.daily_round import DailyRound from care.facility.models.notification import Notification -from care.facility.models.patient_base import ( - CURRENT_HEALTH_CHOICES, - SYMPTOM_CHOICES, - SuggestionChoices, -) +from care.facility.models.patient_base import SYMPTOM_CHOICES, SuggestionChoices from care.facility.models.patient_consultation import PatientConsultation from care.users.api.serializers.user import UserBaseMinimumSerializer from care.utils.notification_handler import NotificationGenerator @@ -39,7 +31,6 @@ class DailyRoundSerializer(serializers.ModelSerializer): choices=COVID_CATEGORY_CHOICES, required=False ) # Deprecated patient_category = ChoiceField(choices=CATEGORY_CHOICES, required=False) - current_health = ChoiceField(choices=CURRENT_HEALTH_CHOICES, required=False) action = ChoiceField( choices=PatientRegistration.ActionChoices, write_only=True, required=False @@ -89,10 +80,6 @@ class DailyRoundSerializer(serializers.ModelSerializer): choices=DailyRound.InsulinIntakeFrequencyChoice, required=False ) - clone_last = serializers.BooleanField( - write_only=True, default=False, required=False - ) - last_edited_by = UserBaseMinimumSerializer(read_only=True) created_by = UserBaseMinimumSerializer(read_only=True) @@ -201,58 +188,6 @@ def create(self, validated_data): "rounds_type": "Telemedicine Rounds are only allowed for Domiciliary Care patients" } ) - if "clone_last" in validated_data: - should_clone = validated_data.pop("clone_last") - if should_clone: - last_objects = DailyRound.objects.filter( - consultation=consultation - ).order_by("-created_date") - if not last_objects.exists(): - raise ValidationError( - {"daily_round": "No Daily Round record available to copy"} - ) - - if "rounds_type" not in validated_data: - raise ValidationError( - {"daily_round": "Rounds type is required to clone"} - ) - - rounds_type = validated_data.get("rounds_type") - if rounds_type == DailyRound.RoundsType.NORMAL.value: - fields_to_clone = [ - "consultation_id", - "patient_category", - "taken_at", - "additional_symptoms", - "other_symptoms", - "physical_examination_info", - "other_details", - "bp", - "pulse", - "resp", - "temperature", - "rhythm", - "rhythm_detail", - "ventilator_spo2", - "consciousness_level", - ] - cloned_daily_round_obj = DailyRound() - for field in fields_to_clone: - value = getattr(last_objects[0], field) - setattr(cloned_daily_round_obj, field, value) - else: - cloned_daily_round_obj = last_objects[0] - - cloned_daily_round_obj.pk = None - cloned_daily_round_obj.rounds_type = rounds_type - cloned_daily_round_obj.created_by = self.context["request"].user - cloned_daily_round_obj.last_edited_by = self.context["request"].user - cloned_daily_round_obj.created_date = timezone.now() - cloned_daily_round_obj.modified_date = timezone.now() - cloned_daily_round_obj.external_id = uuid4() - cloned_daily_round_obj.save() - self.update_last_daily_round(cloned_daily_round_obj) - return self.update(cloned_daily_round_obj, validated_data) if ( "action" in validated_data diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index f526db1b73..680022c7d0 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -25,6 +25,7 @@ PatientContactDetails, PatientMetaInfo, PatientNotes, + PatientNoteThreadChoices, PatientRegistration, ) from care.facility.models.bed import ConsultationBed @@ -514,6 +515,9 @@ class PatientNotesSerializer(serializers.ModelSerializer): allow_null=True, read_only=True, ) + thread = serializers.ChoiceField( + choices=PatientNoteThreadChoices, required=False, allow_null=False + ) def validate_empty_values(self, data): if not data.get("note", "").strip(): @@ -521,6 +525,8 @@ def validate_empty_values(self, data): return super().validate_empty_values(data) def create(self, validated_data): + if "thread" not in validated_data: + raise serializers.ValidationError({"thread": "This field is required"}) user_type = User.REVERSE_TYPE_MAP[validated_data["created_by"].user_type] # If the user is a doctor and the note is being created in the home facility # then the user type is doctor else it is a remote specialist @@ -548,6 +554,8 @@ def create(self, validated_data): return instance def update(self, instance, validated_data): + validated_data.pop("thread", None) # Disallow changing thread of the note. + user = self.context["request"].user note = validated_data.get("note") @@ -572,6 +580,7 @@ class Meta: "note", "facility", "consultation", + "thread", "created_by_object", "user_type", "created_date", @@ -583,6 +592,7 @@ class Meta: "id", "created_date", "modified_date", + "user_type", "last_edited_by", "last_edited_date", ) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 02ff220a44..0b8e9000a8 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -62,6 +62,7 @@ Facility, FacilityPatientStatsHistory, PatientNotes, + PatientNoteThreadChoices, PatientRegistration, ShiftingRequest, ) @@ -602,7 +603,52 @@ def transfer(self, request, *args, **kwargs): class FacilityDischargedPatientFilterSet(filters.FilterSet): + disease_status = CareChoiceFilter(choice_dict=DISEASE_STATUS_DICT) + phone_number = filters.CharFilter(field_name="phone_number") + emergency_phone_number = filters.CharFilter(field_name="emergency_phone_number") name = filters.CharFilter(field_name="name", lookup_expr="icontains") + 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") + created_date = filters.DateFromToRangeFilter(field_name="created_date") + modified_date = filters.DateFromToRangeFilter(field_name="modified_date") + srf_id = filters.CharFilter(field_name="srf_id") + is_declared_positive = filters.BooleanFilter(field_name="is_declared_positive") + date_declared_positive = filters.DateFromToRangeFilter( + field_name="date_declared_positive" + ) + date_of_result = filters.DateFromToRangeFilter(field_name="date_of_result") + last_vaccinated_date = filters.DateFromToRangeFilter( + field_name="last_vaccinated_date" + ) + is_antenatal = filters.BooleanFilter(field_name="is_antenatal") + last_menstruation_start_date = filters.DateFromToRangeFilter( + field_name="last_menstruation_start_date" + ) + date_of_delivery = filters.DateFromToRangeFilter(field_name="date_of_delivery") + # Location Based Filtering + district = filters.NumberFilter(field_name="district__id") + district_name = filters.CharFilter( + field_name="district__name", lookup_expr="icontains" + ) + local_body = filters.NumberFilter(field_name="local_body__id") + local_body_name = filters.CharFilter( + field_name="local_body__name", lookup_expr="icontains" + ) + state = filters.NumberFilter(field_name="state__id") + state_name = filters.CharFilter(field_name="state__name", lookup_expr="icontains") + # Vaccination Filters + covin_id = filters.CharFilter(field_name="covin_id") + is_vaccinated = filters.BooleanFilter(field_name="is_vaccinated") + number_of_doses = filters.NumberFilter(field_name="number_of_doses") + last_consultation__new_discharge_reason = filters.ChoiceFilter( + field_name="last_consultation__new_discharge_reason", + choices=NewDischargeReasonEnum.choices, + ) + last_consultation_discharge_date = filters.DateFromToRangeFilter( + field_name="last_consultation__discharge_date" + ) @extend_schema_view(tags=["patient"]) @@ -616,21 +662,56 @@ class FacilityDischargedPatientViewSet(GenericViewSet, mixins.ListModelMixin): PatientCustomOrderingFilter, ) filterset_class = FacilityDischargedPatientFilterSet - queryset = PatientRegistration.objects.select_related( - "local_body", - "district", - "state", - "ward", - "assigned_to", - "facility", - "facility__ward", - "facility__local_body", - "facility__district", - "facility__state", - "last_consultation", - "last_consultation__assigned_to", - "last_edited", - "created_by", + queryset = ( + PatientRegistration.objects.select_related( + "local_body", + "district", + "state", + "ward", + "assigned_to", + "facility", + "facility__ward", + "facility__local_body", + "facility__district", + "facility__state", + "last_consultation", + "last_consultation__assigned_to", + "last_edited", + "created_by", + ) + .annotate( + coalesced_dob=Coalesce( + "date_of_birth", + Func( + F("year_of_birth"), + Value(1), + Value(1), + function="MAKE_DATE", + output_field=models.DateField(), + ), + output_field=models.DateField(), + ), + age_end=Case( + When(death_datetime__isnull=True, then=Now()), + default=F("death_datetime__date"), + ), + ) + .annotate( + age=Func( + Value("year"), + Func( + F("age_end"), + F("coalesced_dob"), + function="age", + ), + function="date_part", + output_field=models.IntegerField(), + ), + age_days=ExpressionWrapper( + ExtractDay(F("age_end") - F("coalesced_dob")), + output_field=models.IntegerField(), + ), + ) ) ordering_fields = [ @@ -813,6 +894,7 @@ def list(self, request, *args, **kwargs): class PatientNotesFilterSet(filters.FilterSet): + thread = filters.ChoiceFilter(choices=PatientNoteThreadChoices.choices) consultation = filters.CharFilter(field_name="consultation__external_id") diff --git a/care/facility/events/handler.py b/care/facility/events/handler.py index a78513d185..4fac4efe03 100644 --- a/care/facility/events/handler.py +++ b/care/facility/events/handler.py @@ -58,6 +58,15 @@ def create_consultation_event_entry( ).values_list("id", "fields") for group_id, group_fields in groups: if set(group_fields) & fields_to_store: + value = {} + for field in group_fields: + try: + value[field] = data[field] + except KeyError: + value[field] = getattr(object_instance, field, None) + # if all values in the group are Falsy, skip creating the event for this group + if all(not v for v in value.values()): + continue PatientConsultationEvent.objects.select_for_update().filter( consultation_id=consultation_id, event_type=group_id, @@ -66,12 +75,6 @@ def create_consultation_event_entry( object_id=object_instance.id, created_date__lt=created_date, ).update(is_latest=False) - value = {} - for field in group_fields: - try: - value[field] = data[field] - except KeyError: - value[field] = getattr(object_instance, field, None) batch.append( PatientConsultationEvent( consultation_id=consultation_id, @@ -99,7 +102,7 @@ def create_consultation_events( objects: list | QuerySet | Model, caused_by: int, created_date: datetime = None, - old: Model = None, + old: Model | None = None, ): if created_date is None: created_date = now() diff --git a/care/facility/management/commands/load_event_types.py b/care/facility/management/commands/load_event_types.py index 98b4330ec1..5d3e034295 100644 --- a/care/facility/management/commands/load_event_types.py +++ b/care/facility/management/commands/load_event_types.py @@ -105,7 +105,6 @@ class Command(BaseCommand): "fields": ("physical_examination_info",), }, {"name": "PATIENT_CATEGORY", "fields": ("patient_category",)}, - {"name": "CURRENT_HEALTH", "fields": ("current_health",)}, ), }, { diff --git a/care/facility/migrations/0430_alter_asset_support_phone.py b/care/facility/migrations/0430_alter_asset_support_phone.py new file mode 100644 index 0000000000..2364f2315e --- /dev/null +++ b/care/facility/migrations/0430_alter_asset_support_phone.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.8 on 2024-05-09 14:11 + +from django.db import migrations, models + +import care.utils.models.validators + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0429_double_pain_scale"), + ] + + operations = [ + migrations.AlterField( + model_name="asset", + name="support_phone", + field=models.CharField( + default="", + max_length=15, + validators=[ + care.utils.models.validators.PhoneNumberValidator( + types=("mobile", "landline", "support") + ) + ], + ), + ), + ] diff --git a/care/facility/migrations/0431_patientnotes_thread.py b/care/facility/migrations/0431_patientnotes_thread.py new file mode 100644 index 0000000000..f243c24cd1 --- /dev/null +++ b/care/facility/migrations/0431_patientnotes_thread.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.10 on 2024-05-12 07:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0430_alter_asset_support_phone"), + ] + + operations = [ + migrations.AddField( + model_name="patientnotes", + name="thread", + field=models.SmallIntegerField( + choices=[(10, "DOCTORS"), (20, "NURSES")], db_index=True, default=10 + ), + ), + ] diff --git a/care/facility/migrations/0432_alter_fileupload_file_type.py b/care/facility/migrations/0432_alter_fileupload_file_type.py new file mode 100644 index 0000000000..edffd8fcf9 --- /dev/null +++ b/care/facility/migrations/0432_alter_fileupload_file_type.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.10 on 2024-05-13 03:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0431_patientnotes_thread"), + ] + + operations = [ + migrations.AlterField( + model_name="fileupload", + name="file_type", + field=models.IntegerField( + choices=[ + (0, "OTHER"), + (1, "PATIENT"), + (2, "CONSULTATION"), + (3, "SAMPLE_MANAGEMENT"), + (4, "CLAIM"), + (5, "DISCHARGE_SUMMARY"), + (6, "COMMUNICATION"), + (7, "CONSENT_RECORD"), + (8, "ABDM_HEALTH_INFORMATION"), + ], + default=1, + ), + ), + ] diff --git a/care/facility/migrations/0433_alter_hospitaldoctors_area.py b/care/facility/migrations/0433_alter_hospitaldoctors_area.py new file mode 100644 index 0000000000..7dd4a4bbdb --- /dev/null +++ b/care/facility/migrations/0433_alter_hospitaldoctors_area.py @@ -0,0 +1,68 @@ +# Generated by Django 4.2.10 on 2024-05-14 15:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0432_alter_fileupload_file_type"), + ] + + operations = [ + migrations.AlterField( + model_name="hospitaldoctors", + name="area", + field=models.IntegerField( + choices=[ + (1, "General Medicine"), + (2, "Pulmonology"), + (3, "Intensivist"), + (4, "Pediatrician"), + (5, "Others"), + (6, "Anesthesiologist"), + (7, "Cardiac Surgeon"), + (8, "Cardiologist"), + (9, "Dentist"), + (10, "Dermatologist"), + (11, "Diabetologist"), + (12, "Emergency Medicine Physician"), + (13, "Endocrinologist"), + (14, "Family Physician"), + (15, "Gastroenterologist"), + (16, "General Surgeon"), + (17, "Geriatrician"), + (18, "Hematologist"), + (29, "Immunologist"), + (20, "Infectious Disease Specialist"), + (21, "MBBS doctor"), + (22, "Medical Officer"), + (23, "Nephrologist"), + (24, "Neuro Surgeon"), + (25, "Neurologist"), + (26, "Obstetrician and Gynecologist"), + (27, "Oncologist"), + (28, "Oncology Surgeon"), + (29, "Ophthalmologist"), + (30, "Oral and Maxillofacial Surgeon"), + (31, "Orthopedic"), + (32, "Orthopedic Surgeon"), + (33, "Otolaryngologist (ENT)"), + (34, "Palliative care Physician"), + (35, "Pathologist"), + (36, "Pediatric Surgeon"), + (37, "Physician"), + (38, "Plastic Surgeon"), + (39, "Psychiatrist"), + (40, "Pulmonologist"), + (41, "Radio technician"), + (42, "Radiologist"), + (43, "Rheumatologist"), + (44, "Sports Medicine Specialist"), + (45, "Thoraco-Vascular Surgeon"), + (46, "Transfusion Medicine Specialist"), + (47, "Urologist"), + (48, "Nurse"), + ] + ), + ), + ] diff --git a/care/facility/migrations/0433_remove_dailyround_current_health.py b/care/facility/migrations/0433_remove_dailyround_current_health.py new file mode 100644 index 0000000000..e56e865d90 --- /dev/null +++ b/care/facility/migrations/0433_remove_dailyround_current_health.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.8 on 2024-05-14 12:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0432_alter_fileupload_file_type"), + ] + + operations = [ + migrations.RemoveField( + model_name="dailyround", + name="current_health", + ), + ] diff --git a/care/facility/migrations/0434_unique_investigations.py b/care/facility/migrations/0434_unique_investigations.py new file mode 100644 index 0000000000..adee9af156 --- /dev/null +++ b/care/facility/migrations/0434_unique_investigations.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.10 on 2024-03-30 09:46 + +from django.db import migrations, models +from django.db.models import F, Window +from django.db.models.functions import RowNumber + + +def fix_duplicate_investigation_names(apps, schema_editor): + PatientInvestigation = apps.get_model("facility", "PatientInvestigation") + + window = Window( + expression=RowNumber(), + partition_by=[F("name")], + order_by=F("id").asc(), + ) + + investigations = PatientInvestigation.objects.annotate(row_number=window).filter( + row_number__gt=1 + ) + + for investigation in investigations: + investigation.name = f"{investigation.name}_{investigation.row_number - 1}" + + PatientInvestigation.objects.bulk_update(investigations, ["name"], batch_size=2000) + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0433_alter_hospitaldoctors_area"), + ] + + operations = [ + migrations.RunPython(fix_duplicate_investigation_names), + migrations.AlterField( + model_name="patientinvestigation", + name="name", + field=models.CharField(max_length=500, unique=True), + ), + ] diff --git a/care/facility/migrations/0435_merge_20240515_1301.py b/care/facility/migrations/0435_merge_20240515_1301.py new file mode 100644 index 0000000000..84b019c10f --- /dev/null +++ b/care/facility/migrations/0435_merge_20240515_1301.py @@ -0,0 +1,12 @@ +# Generated by Django 4.2.8 on 2024-05-15 07:31 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0433_remove_dailyround_current_health"), + ("facility", "0434_unique_investigations"), + ] + + operations = [] diff --git a/care/facility/models/asset.py b/care/facility/models/asset.py index af560e8e9c..c85a206e89 100644 --- a/care/facility/models/asset.py +++ b/care/facility/models/asset.py @@ -100,7 +100,7 @@ class Asset(BaseModel): vendor_name = models.CharField(max_length=1024, blank=True, null=True) support_name = models.CharField(max_length=1024, blank=True, null=True) support_phone = models.CharField( - max_length=14, + max_length=15, validators=[PhoneNumberValidator(types=("mobile", "landline", "support"))], default="", ) diff --git a/care/facility/models/daily_round.py b/care/facility/models/daily_round.py index 0b5b78ec3c..4d7a00e4d7 100644 --- a/care/facility/models/daily_round.py +++ b/care/facility/models/daily_round.py @@ -25,7 +25,7 @@ PAIN_SCALE_ENHANCED, PRESSURE_SORE, ) -from care.facility.models.patient_base import CURRENT_HEALTH_CHOICES, SYMPTOM_CHOICES +from care.facility.models.patient_base import SYMPTOM_CHOICES from care.facility.models.patient_consultation import PatientConsultation from care.users.models import User from care.utils.models.validators import JSONFieldSchemaValidator @@ -159,9 +159,6 @@ class InsulinIntakeFrequencyType(enum.Enum): patient_category = models.CharField( choices=CATEGORY_CHOICES, max_length=8, blank=False, null=True ) - current_health = models.IntegerField( - default=0, choices=CURRENT_HEALTH_CHOICES, blank=True - ) other_details = models.TextField(null=True, blank=True) medication_given = JSONField(default=dict) # To be Used Later on diff --git a/care/facility/models/facility.py b/care/facility/models/facility.py index f417092853..f5a0013540 100644 --- a/care/facility/models/facility.py +++ b/care/facility/models/facility.py @@ -101,9 +101,52 @@ DOCTOR_TYPES = [ (1, "General Medicine"), (2, "Pulmonology"), - (3, "Critical Care"), - (4, "Paediatrics"), - (5, "Other Speciality"), + (3, "Intensivist"), + (4, "Pediatrician"), + (5, "Others"), + (6, "Anesthesiologist"), + (7, "Cardiac Surgeon"), + (8, "Cardiologist"), + (9, "Dentist"), + (10, "Dermatologist"), + (11, "Diabetologist"), + (12, "Emergency Medicine Physician"), + (13, "Endocrinologist"), + (14, "Family Physician"), + (15, "Gastroenterologist"), + (16, "General Surgeon"), + (17, "Geriatrician"), + (18, "Hematologist"), + (29, "Immunologist"), + (20, "Infectious Disease Specialist"), + (21, "MBBS doctor"), + (22, "Medical Officer"), + (23, "Nephrologist"), + (24, "Neuro Surgeon"), + (25, "Neurologist"), + (26, "Obstetrician and Gynecologist"), + (27, "Oncologist"), + (28, "Oncology Surgeon"), + (29, "Ophthalmologist"), + (30, "Oral and Maxillofacial Surgeon"), + (31, "Orthopedic"), + (32, "Orthopedic Surgeon"), + (33, "Otolaryngologist (ENT)"), + (34, "Palliative care Physician"), + (35, "Pathologist"), + (36, "Pediatric Surgeon"), + (37, "Physician"), + (38, "Plastic Surgeon"), + (39, "Psychiatrist"), + (40, "Pulmonologist"), + (41, "Radio technician"), + (42, "Radiologist"), + (43, "Rheumatologist"), + (44, "Sports Medicine Specialist"), + (45, "Thoraco-Vascular Surgeon"), + (46, "Transfusion Medicine Specialist"), + (47, "Urologist"), + (48, "Nurse"), ] REVERSE_DOCTOR_TYPES = reverse_choices(DOCTOR_TYPES) diff --git a/care/facility/models/file_upload.py b/care/facility/models/file_upload.py index 56a140fd5d..51bce92d96 100644 --- a/care/facility/models/file_upload.py +++ b/care/facility/models/file_upload.py @@ -138,6 +138,7 @@ class FileType(models.IntegerChoices): DISCHARGE_SUMMARY = 5, "DISCHARGE_SUMMARY" COMMUNICATION = 6, "COMMUNICATION" CONSENT_RECORD = 7, "CONSENT_RECORD" + ABDM_HEALTH_INFORMATION = 8, "ABDM_HEALTH_INFORMATION" file_type = models.IntegerField(choices=FileType.choices, default=FileType.PATIENT) is_archived = models.BooleanField(default=False) diff --git a/care/facility/models/icd11_diagnosis.py b/care/facility/models/icd11_diagnosis.py index 35ba3a0a4a..c4747f59e6 100644 --- a/care/facility/models/icd11_diagnosis.py +++ b/care/facility/models/icd11_diagnosis.py @@ -64,7 +64,9 @@ class ConditionVerificationStatus(models.TextChoices): if status not in INACTIVE_CONDITION_VERIFICATION_STATUSES ] # These statuses are allowed to be selected during create and these diagnosis can only be a principal diagnosis -REVERSE_CONDITION_VERIFICATION_STATUSES = reverse_choices(ConditionVerificationStatus) +REVERSE_CONDITION_VERIFICATION_STATUSES = reverse_choices( + [(x.value, str(x.label)) for x in ConditionVerificationStatus] +) class ConsultationDiagnosis(BaseModel, ConsultationRelatedPermissionMixin): diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index 786ff584b7..94a06f0f5f 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -732,6 +732,11 @@ class PatientMobileOTP(BaseModel): otp = models.CharField(max_length=10) +class PatientNoteThreadChoices(models.IntegerChoices): + DOCTORS = 10, "DOCTORS" + NURSES = 20, "NURSES" + + class PatientNotes(FacilityBaseModel, ConsultationRelatedPermissionMixin): patient = models.ForeignKey( PatientRegistration, on_delete=models.PROTECT, null=False, blank=False @@ -748,6 +753,11 @@ class PatientNotes(FacilityBaseModel, ConsultationRelatedPermissionMixin): on_delete=models.SET_NULL, null=True, ) + thread = models.SmallIntegerField( + choices=PatientNoteThreadChoices.choices, + db_index=True, + default=PatientNoteThreadChoices.DOCTORS, + ) note = models.TextField(default="", blank=True) def get_related_consultation(self): diff --git a/care/facility/models/patient_base.py b/care/facility/models/patient_base.py index 63e11e50b1..73bbcfaacb 100644 --- a/care/facility/models/patient_base.py +++ b/care/facility/models/patient_base.py @@ -12,14 +12,6 @@ def reverse_choices(choices): return output -CURRENT_HEALTH_CHOICES = [ - (0, "NO DATA"), - (1, "REQUIRES VENTILATOR"), - (2, "WORSE"), - (3, "STATUS QUO"), - (4, "BETTER"), -] - SYMPTOM_CHOICES = [ (1, "ASYMPTOMATIC"), (2, "FEVER"), diff --git a/care/facility/models/patient_investigation.py b/care/facility/models/patient_investigation.py index 9e94e23153..e9fec6b3af 100644 --- a/care/facility/models/patient_investigation.py +++ b/care/facility/models/patient_investigation.py @@ -17,7 +17,7 @@ def __str__(self) -> str: class PatientInvestigation(BaseModel): - name = models.CharField(max_length=500, blank=False, null=False) + name = models.CharField(max_length=500, blank=False, null=False, unique=True) groups = models.ManyToManyField(PatientInvestigationGroup) unit = models.TextField(null=True, blank=True) ideal_value = models.TextField(blank=True, null=True) diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index d86f7fdd20..bf0af8838d 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -4,6 +4,7 @@ from rest_framework import status from rest_framework.test import APITestCase +from care.facility.models import PatientNoteThreadChoices from care.facility.models.icd11_diagnosis import ( ConditionVerificationStatus, ICD11Diagnosis, @@ -22,6 +23,7 @@ class ExpectedPatientNoteKeys(Enum): MODIFIED_DATE = "modified_date" LAST_EDITED_BY = "last_edited_by" LAST_EDITED_DATE = "last_edited_date" + THREAD = "thread" USER_TYPE = "user_type" @@ -131,6 +133,7 @@ def create_patient_note( data = { "facility": patient.facility or self.facility, "note": note, + "thread": PatientNoteThreadChoices.DOCTORS, } data.update(kwargs) self.client.force_authenticate(user=created_by) @@ -140,7 +143,11 @@ def test_patient_notes(self): self.client.force_authenticate(user=self.state_admin) patientId = self.patient.external_id response = self.client.get( - f"/api/v1/patient/{patientId}/notes/?consultation={self.consultation.external_id}" + f"/api/v1/patient/{patientId}/notes/", + { + "consultation": self.consultation.external_id, + "thread": PatientNoteThreadChoices.DOCTORS, + }, ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertIsInstance(response.json()["results"], list) diff --git a/care/facility/tests/test_patient_daily_rounds_api.py b/care/facility/tests/test_patient_daily_rounds_api.py index 07ccb87df6..7c5686275a 100644 --- a/care/facility/tests/test_patient_daily_rounds_api.py +++ b/care/facility/tests/test_patient_daily_rounds_api.py @@ -40,7 +40,6 @@ def setUpTestData(cls) -> None: cls.consultation_with_bed.save() cls.log_update = { - "clone_last": False, "rounds_type": "NORMAL", "patient_category": "Comfort", "action": "DISCHARGE_RECOMMENDED", diff --git a/care/facility/tests/test_unlink_district_admins.py b/care/facility/tests/test_unlink_district_admins.py index 419e5f83a7..673ae4e95e 100644 --- a/care/facility/tests/test_unlink_district_admins.py +++ b/care/facility/tests/test_unlink_district_admins.py @@ -49,11 +49,8 @@ def test_unlink_home_facility_admin_different_district(self): response = self.client.delete( "/api/v1/users/" + username + "/clear_home_facility/" ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual( - response.json()["facility"], - "Cannot unlink User's Home Facility from other district", - ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.json()["detail"], "Not found.") def test_unlink_faciltity_admin_same_district(self): self.client.force_login(self.admin1) @@ -80,5 +77,5 @@ def test_unlink_faciltity_admin_different_district(self): "/api/v1/users/" + username + "/delete_facility/", {"facility": self.facility2.external_id}, ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["facility"], "Facility Access not Present") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.json()["detail"], "Not found.") diff --git a/care/users/api/viewsets/users.py b/care/users/api/viewsets/users.py index af2d91e4c5..7916d08418 100644 --- a/care/users/api/viewsets/users.py +++ b/care/users/api/viewsets/users.py @@ -1,5 +1,5 @@ from django.core.cache import cache -from django.db.models import F +from django.db.models import F, Q, Subquery from django_filters import rest_framework as filters from drf_spectacular.utils import extend_schema from dry_rest_permissions.generics import DRYPermissions @@ -21,6 +21,7 @@ UserSerializer, ) from care.users.models import User +from care.utils.cache.cache_allowed_facilities import get_accessible_facilities def remove_facility_user_cache(user_id): @@ -119,6 +120,36 @@ class UserViewSet( # DRYPermissions(), # ] + def get_queryset(self): + if self.request.user.is_superuser: + return self.queryset + query = Q(id=self.request.user.id) + if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateReadOnlyAdmin"]: + query |= Q( + state=self.request.user.state, + user_type__lt=User.TYPE_VALUE_MAP["StateAdmin"], + is_superuser=False, + ) + elif ( + self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] + ): + query |= Q( + district=self.request.user.district, + user_type__lt=User.TYPE_VALUE_MAP["DistrictAdmin"], + is_superuser=False, + ) + else: + query |= Q( + id__in=Subquery( + FacilityUser.objects.filter( + facility_id__in=get_accessible_facilities(self.request.user) + ).values("user_id") + ), + user_type__lt=User.TYPE_VALUE_MAP["DistrictAdmin"], + is_superuser=False, + ) + return self.queryset.filter(query) + def get_serializer_class(self): if self.action == "list": return UserListSerializer diff --git a/care/users/management/commands/populate_investigations.py b/care/users/management/commands/populate_investigations.py index 1603041f3b..21847ee190 100644 --- a/care/users/management/commands/populate_investigations.py +++ b/care/users/management/commands/populate_investigations.py @@ -1,156 +1,18 @@ +import json + from django.core.management import BaseCommand +from django.db import transaction from care.facility.models.patient_investigation import ( PatientInvestigation, PatientInvestigationGroup, ) -# TODO Move the Investigations and Investigation Groups into a proper JSON like python dict structure to allow easy updates and additions. -investigations = """name unit ideal min max type (Float/String/Choice) choices category_id -Blood Group Choice A-,A+,B+,B-,O+,O-,AB-,AB+ 1 -Total Count cell/cumm 4500-11000 cells/cumm 4500 11000 Float 1 -Neutrophil count cell/cumm 4500-11000 cells/cumm 4500 11000 Float 1 -Lymphocyte count Eosinophil count cell/cumm 4500-11000 cells/cumm 4500 11000 Float 1 -Eosinophil count cell/cumm 4500-11000 cells/cumm 4500 11000 Float 1 -Basophil count cell/cumm 4500-11000 cells/cumm 4500 11000 Float 1 -Monocyte count cell/cumm 4500-11000 cells/cumm 4500 11000 Float 1 -neutrophil % 4500-11000 cells/cumm 4500 11000 Float 1 -lymphocyte % 4500-11000 cells/cumm 4500 11000 Float 1 -eosinophil % 4500-11000 cells/cumm 4500 11000 Float 1 -basophile % 4500-11000 cells/cumm 4500 11000 Float 1 -monocyte % 4500-11000 cells/cumm 4500 11000 Float 1 -Hb gm% men 14-17 gm% , woman 12-16 gm% ,children 12-14 gm% 12 17 Float 1 -PCV % Men 38-51 gm% , Woman 36-47% 36 51 Float 1 -RBC count million/cumm 4.5-6.0 million/cumm 4.5 6 Float 1 -RDW % 11.8 - 16.1% 11.8 16.1 Float 1 -Platelets lakhs/cumm 1.5-4.5 lakhs/cumm 1.5 4.5 Float 1 -MCV Fl 80-96 Fl 80 96 Float 1 -MCH pg 27-33 pg 27 33 Float 1 -MCHC g/dl 33.4-35.5 g/dl 33.4 35.5 Float 1 -ESR mm/hr 0-20 mm/hr 0 20 Float 1 -Peripheral blood smear String 1 -Reticulocyte count % adults 0.5-1.5%, newborns 3-6% 0.5 6 Float 1 -M P smear String 1 -FBS mg/dl 70-110 mg/dl 70 110 Float 2 -PPBS mg/dl < 140 mg/dl 0 140 Float 2 -RBS mg/dl 80-120 mg/dl 80 120 Float 2 -T. Cholestrol mg/dl 150-220 mg/dl 150 220 Float 2 -LDL mg/dl < 130 mg/dl 0 130 Float 2 -HDL mg/dl male 35-80 mg/dl, female 40-88 mg/dl 35 88 Float 2 -Triglycerides mg/dl male 60-165 mg/dl, female 40-140 mg/dl 40 165 Float 2 -VLDL mg/dl 2-30 mg/dl 2 30 Float 2 -Urea mg/dl 10-50 mg/dl 10 50 Float 2 -Uric Acid mg/dl male 3.5-7.2 mg/dl, female 2.6-6 mg/dl 2.6 7.2 Float 2 -Creatinine mg/dl male 0.7-1.4 mg/dl, female 0.6-1.2 mg/dl 0.6 1.4 Float 2 -CRP mg/l upto 6 mg/l 0 6 Float 2 -Serum Sodium (Na+) mmol/l 135-155 mmol/l 135 155 Float 2 -Serum Potassium (K+) mmol/l 3.5 - 5.5 mmol/l 3.5 5.5 Float 2 -Serum Calcium mg/dl 8.8-10.2 mg/dl 8.8 10.2 Float 2 -Serum Phosphorus mg/dl children 4-7 mg/dl, adult 2.5-4.5 mg/dl 2.5 7 Float 2 -Serum Chloride mmol/l 96-109 mmol/l 96 109 Float 2 -Serum Megnesium mg/dl 1.6-2.6 mg/dl 1.6 2.6 Float 2 -Total Bilirubin mg/dl adult upto 1.2 mg/dl, infant 0.2-8 mg/dl 0.2 8 Float 2 -Direct Bilirubin mg/dl upto 0.4 mg/dl 0 0.4 Float 2 -SGOT IU/L upto 46 IU/L 0 46 Float 2 -SGPT IU/L upto 49 IU/L 0 49 Float 2 -ALP IU/L male 80-306 IU/L, female 64-306 IU/L 64 306 Float 2 -Total Protein g/dl 6-8 g/dl 6 8 Float 2 -Albumin g/dl 3.5-5.2 g/dl 3.5 5.2 Float 2 -Globulin g/dl 1.5-2.5 g/dl 1.5 2.5 Float 2 -PT sec 9.1-12.1 seconds 9.1 12.1 Float 2 -INR sec 0.8-1.1 seconds 0.8 1.1 Float 2 -APTT sec 25.4-38.4 seconds 25.4 38.4 Float 2 -D-Dimer ug/l < 0.5 ug/l 0 0.5 Float 2 -Fibrinogen mg/dl 200-400 mg/dl 200 400 Float 2 -GCT mg/dl < 140 mg/dl 0 140 Float 2 -GTT mg/dl 140-200 mg/dl 140 200 Float 2 -GGT U/L 11-50 U/L 11 50 Float 2 -HbA1C % 4-5.6 % 4 5.6 Float 2 -Serum Copper mcg/dl 85-180 mcg/dl 85 180 Float 2 -Serum Lead mcg/dl upto 10 mcg/dl 0 10 Float 2 -Iron mcg/dl 60-170 mcg/dl 60 170 Float 2 -TIBC mcg/dl 250-450 mcg/dl 250 450 Float 2 -Transferin Saturation % 15-50 % 15 50 Float 2 -IL6 pg/ml 0-16.4 pg/ml 0 16.4 Float 2 -Lactate mmol/l 0.5-1 mmol/l 0.5 1 Float 2 -Ceruloplasmin mg/dl 14-40 mg/dl 14 40 Float 2 -ACP U/L 0.13-0.63 U/L 0.13 0.63 Float 2 -Protein C IU dl 1 65-135 IU dl 1 65 135 Float 2 -Protein S % 70-140 % 70 140 Float 2 -G6PD U/g Hb neonate 10.15-14.71 U/g Hb, adult 6.75-11.95 U/g Hb Float 2 -ACCP EU/ml < 20 EU/ml 0 20 Float 2 -Ferritin ng/ml 20-250 ng/ml 20 250 Float 2 -LDH U/L 140-280 U/L 140 280 Float 2 -Amylase U/L 60-180 U/L 60 180 Float 2 -Lipase U/L 0-160 U/L 0 160 Float 2 -Ammonia ug/dL 15-45 ug/dL 15 45 Float 2 -CKMB IU/L 5-25 IU/L 5 25 Float 2 -CK NAC U/L male < 171 U/L, female < 145 U/L Float 2 -24hrs Urine Protein mg/dl <10 mg/dl 0 10 Float 2 -24hrs Urine Uric Acid mg/24hr 250-750 mg/24hr 250 750 Float 2 -24 hrs Urine Oxalate mg/L <15 mg/L 0 15 Float 2 -Urine Microalbumin mg < 30 mg 0 30 Float 2 -Urine Sodium mEq/day 40-220 mEq/day 40 220 Float 2 -PCT ng/ml < 0.15 ng/ml 0 0.15 Float 2 -T3 ng/dl 80-220 ng/dl 80 220 Float 2 -T4 ug/L 5-12 ug/L 5 12 Float 2 -TSH mIU/L 0.5-5 mIU/L 0.5 5 Float 2 -FT3 ng/dl 60-180 ng/dl 60 180 Float 2 -FT4 ng/dl 0.7-1.9 ng/dl 0.7 1.9 Float 2 -Estradiol pg/ml premenopausal women 30-400 pg/ml, post-menopausal women 0-30 pg/ml, men 10-50 pg/ml Float 2 -Growth Hormone ng/ml male 0.4-10 ng/ml, female 1-14 ng/ml 0.4 14 Float 2 -Cortisol mcg/dl 5-25 ng/ml 5 25 Float 2 -PTH pg/ml 14-65 pg/ml 14 65 Float 2 -Prolactine ng/ml male <20 ng/ml, female <25 ng/ml, pregnant women <300 ng/ml Float 2 -Pro BNP pg/ml < 300 pg/ml 0 300 Float 2 -Vitamine D3 ng/ml 20-40 ng/ml 20 40 Float 2 -Vitamine B12 pg/ml 160-950 pg/ml 160 950 Float 2 -FSH IU/L before puberty 0-4 IU/L, during puberty 0.36-10 IU/L, 0 10 Float 2 -LH IU/L before menopause 5-25 IU/L, after menopause 14.2-52.3 IU/L 5 52.3 Float 2 -PSA ng/ml <4 ng/ml 0 4 Float 2 -ACTH pg/ml 10-60 pg/ml 10 60 Float 2 -CEA ng/ml 0-2.5 ng/ml 0 2.5 Float 2 -AFP ng/ml 10-20 ng/ml 10 20 Float 2 -CA125 U/ml < 46 U/ml 0 46 Float 2 -CA19.9 U/ml 0-37 U/ml 0 37 Float 2 -Testosterone ng/dl 270-1070 ng/dl 270 1070 Float 2 -Progestrone ng/ml female pre ovulation, menupausal women, men <1 ng/ml, mid-cycle 5-20 ng/ml Float 2 -Serum IgG g/L 6-16 g/L 6 16 Float 2 -Serum IgE UL/ml 150-1000 UL/ml 150 1000 Float 2 -Serum IgM g/L 0.4-2.5 g/L 0.4 2.5 Float 2 -Serum IgA G/L 0.8-3 g/L 0.8 3 Float 2 -Colour String 3 -Appearence String 3 -Ph String 3 -Specific Gravity String 3 -Nitrite String 3 -Urobilinogen String 3 -Bile Salt String 3 -Bile Pigment String 3 -Acetone String 3 -Albumin String 3 -Sugar String 3 -Puscells String 3 -Epithetical Cells String 3 -RBC String 3 -Cast String 3 -Crystal String 3 -others String 3 -UPT String 3 -Stool OB String 3 -Stool Microscopy String 3""" - - -investigation_groups = """Id Name -1 Haematology -2 Biochemistry test -3 Urine Test""" - - -def none_or_float(val): - if len(val.strip()) != 0: - return float(val) - return None +with open("data/investigations.json", "r") as investigations_data: + investigations = json.load(investigations_data) + +with open("data/investigation_groups.json") as investigation_groups_data: + investigation_groups = json.load(investigation_groups_data) class Command(BaseCommand): @@ -161,34 +23,69 @@ class Command(BaseCommand): help = "Seed Data for Investigations" def handle(self, *args, **options): - investigation_group_data = investigation_groups.split("\n")[1:] investigation_group_dict = {} - for investigation_group in investigation_group_data: - current_investigation_group = investigation_group.split("\t") - current_obj = PatientInvestigationGroup.objects.filter( - name=current_investigation_group[1] - ).first() - if not current_obj: - current_obj = PatientInvestigationGroup( - name=current_investigation_group[1] - ) - current_obj.save() - investigation_group_dict[current_investigation_group[0]] = current_obj - investigation_data = investigations.split("\n")[1:] - for investigation in investigation_data: - current_investigation = investigation.split("\t") + + investigation_groups_to_create = [ + PatientInvestigationGroup(name=group.get("name")) + for group in investigation_groups + if group.get("id") not in investigation_group_dict + ] + created_groups = PatientInvestigationGroup.objects.bulk_create( + investigation_groups_to_create + ) + investigation_group_dict.update({group.id: group for group in created_groups}) + + existing_objs = PatientInvestigation.objects.filter( + name__in=[investigation["name"] for investigation in investigations] + ) + + bulk_create_data = [] + bulk_update_data = [] + + for investigation in investigations: data = { - "name": current_investigation[0], - "unit": current_investigation[1], - "ideal_value": current_investigation[2], - "min_value": none_or_float(current_investigation[3]), - "max_value": none_or_float(current_investigation[4]), - "investigation_type": current_investigation[5], - "choices": current_investigation[6], + "name": investigation["name"], + "unit": investigation.get("unit", ""), + "ideal_value": investigation.get("ideal_value", ""), + "min_value": None + if investigation.get("min_value") is None + else float(investigation.get("min_value")), + "max_value": None + if investigation.get("max_value") is None + else float(investigation.get("max_value")), + "investigation_type": investigation["type"], + "choices": investigation.get("choices", ""), } - current_obj = PatientInvestigation.objects.filter(**data).first() - if not current_obj: - current_obj = PatientInvestigation(**data) - current_obj.save() - current_obj.groups.add(investigation_group_dict[current_investigation[7]]) - current_obj.save() + + existing_obj = existing_objs.filter(name=data["name"]).first() + if existing_obj: + bulk_update_data.append(existing_obj) + else: + new_obj = PatientInvestigation(**data) + bulk_create_data.append(new_obj) + + group_ids = investigation.get("category_ids", []) + groups_to_add = [ + investigation_group_dict[category_id] for category_id in group_ids + ] + if existing_obj: + existing_obj.groups.set(groups_to_add) + else: + data["groups"] = groups_to_add + + with transaction.atomic(): + if bulk_create_data: + PatientInvestigation.objects.bulk_create(bulk_create_data) + + if bulk_update_data: + PatientInvestigation.objects.bulk_update( + bulk_update_data, + fields=[ + "unit", + "ideal_value", + "min_value", + "max_value", + "investigation_type", + "choices", + ], + ) diff --git a/care/users/tests/test_api.py b/care/users/tests/test_api.py index 18512436db..90ca8b45e4 100644 --- a/care/users/tests/test_api.py +++ b/care/users/tests/test_api.py @@ -123,29 +123,33 @@ def setUpTestData(cls) -> None: 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("staff1", cls.district, home_facility=cls.facility) + cls.data_2 = cls.get_user_data(cls.district) cls.data_2.update({"username": "user_2", "password": "password"}) cls.user_2 = cls.create_user(**cls.data_2) + cls.data_3 = cls.get_user_data(cls.district) + cls.data_3.update({"username": "user_3", "password": "password"}) + cls.user_3 = cls.create_user(**cls.data_3) + cls.link_user_with_facility(cls.user_3, cls.facility, cls.super_user) + def test_user_can_access_url(self): """Test user can access the url by location""" username = self.user.username response = self.client.get(f"/api/v1/users/{username}/") self.assertEqual(response.status_code, status.HTTP_200_OK) - def test_user_can_read_all(self): - """Test user can read all""" + def test_user_can_read_all_users_within_accessible_facility(self): + """Test user can read all users within the accessible facility""" response = self.client.get("/api/v1/users/") - # test response code self.assertEqual(response.status_code, status.HTTP_200_OK) res_data_json = response.json() - # test total user count self.assertEqual(res_data_json["count"], 2) results = res_data_json["results"] - # test presence of usernames self.assertIn(self.user.id, {r["id"] for r in results}) - self.assertIn(self.user_2.id, {r["id"] for r in results}) + self.assertIn(self.user_3.id, {r["id"] for r in results}) def test_user_can_modify_themselves(self): """Test user can modify the attributes for themselves""" @@ -170,7 +174,8 @@ def test_user_cannot_read_others(self): """Test 1 user can read the attributes of the other user""" username = self.data_2["username"] response = self.client.get(f"/api/v1/users/{username}/") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.json()["detail"], "Not found.") def test_user_cannot_modify_others(self): """Test a user can't modify others""" @@ -183,15 +188,14 @@ def test_user_cannot_modify_others(self): "password": password, }, ) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.json()["detail"], "Not found.") def test_user_cannot_delete_others(self): """Test a user can't delete others""" field = "username" response = self.client.delete(f"/api/v1/users/{self.data_2[field]}/") - # test response code self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - # test backend response(user_2 still exists) self.assertEqual( self.data_2[field], User.objects.get(username=self.data_2[field]).username, diff --git a/config/api_router.py b/config/api_router.py index 82b330cc1c..70512b97c0 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -4,8 +4,11 @@ from rest_framework_nested.routers import NestedSimpleRouter from care.abdm.api.viewsets.abha import AbhaViewSet +from care.abdm.api.viewsets.consent import ConsentViewSet from care.abdm.api.viewsets.health_facility import HealthFacilityViewSet +from care.abdm.api.viewsets.health_information import HealthInformationViewSet from care.abdm.api.viewsets.healthid import ABDMHealthIDViewSet +from care.abdm.api.viewsets.patients import PatientsViewSet from care.facility.api.viewsets.ambulance import ( AmbulanceCreateViewSet, AmbulanceViewSet, @@ -248,10 +251,19 @@ # ABDM endpoints if settings.ENABLE_ABDM: router.register("abdm/healthid", ABDMHealthIDViewSet, basename="abdm-healthid") + router.register("abdm/consent", ConsentViewSet, basename="abdm-consent") + router.register( + "abdm/health_information", + HealthInformationViewSet, + basename="abdm-healthinformation", + ) + router.register("abdm/patients", PatientsViewSet, basename="abdm-patients") + router.register( "abdm/health_facility", HealthFacilityViewSet, basename="abdm-healthfacility" ) + app_name = "api" urlpatterns = [ path("", include(router.urls)), diff --git a/config/settings/base.py b/config/settings/base.py index 9a6df3249b..03e764e8b3 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -543,6 +543,7 @@ "audio/midi", "audio/x-midi", "audio/webm", + "audio/mp4" # Documents "text/plain", "text/csv", @@ -584,6 +585,7 @@ # current hosted domain CURRENT_DOMAIN = env("CURRENT_DOMAIN", default="localhost:8000") +BACKEND_DOMAIN = env("BACKEND_DOMAIN", default="localhost:9000") # open id connect JWKS = JsonWebKey.import_key_set( diff --git a/data/investigation_groups.json b/data/investigation_groups.json new file mode 100644 index 0000000000..210fa3da3d --- /dev/null +++ b/data/investigation_groups.json @@ -0,0 +1,30 @@ +[ + { + "id": "1", + "name": "Haematology" + }, + { + "id": "2", + "name": "Biochemistry test" + }, + { + "id": "3", + "name": "Urine Test" + }, + { + "id": "4", + "name": "CBC" + }, + { + "id": "5", + "name": "Differential white blood cell count" + }, + { + "id": "6", + "name": "Liver Function Test" + }, + { + "id": "7", + "name": "Kidney Function test" + } +] diff --git a/data/investigations.json b/data/investigations.json new file mode 100644 index 0000000000..a33a48c6d5 --- /dev/null +++ b/data/investigations.json @@ -0,0 +1,1830 @@ +[ + { + "name": "Blood Group", + "type": "Choice", + "choices": "A-,A+,B+,B-,O+,O-,AB-,AB+", + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 1 + ] + }, + { + "name": "Total Count", + "type": "Float", + "choices": null, + "unit": "cell/cumm", + "ideal": "4500-11000 cells/cumm", + "min": 4500, + "max": 11000, + "category_id": [ + 1 + ] + }, + { + "name": "Neutrophil count", + "type": "Float", + "choices": null, + "unit": "cell/cumm", + "ideal": "4500-11000 cells/cumm", + "min": 4500, + "max": 11000, + "category_id": [ + 1 + ] + }, + { + "name": "Lymphocyte count Eosinophil count", + "type": "Float", + "choices": null, + "unit": "cell/cumm", + "ideal": "4500-11000 cells/cumm", + "min": 4500, + "max": 11000, + "category_id": [ + 1 + ] + }, + { + "name": "Eosinophil count", + "type": "Float", + "choices": null, + "unit": "cell/cumm", + "ideal": "4500-11000 cells/cumm", + "min": 4500, + "max": 11000, + "category_id": [ + 1 + ] + }, + { + "name": "Basophil count", + "type": "Float", + "choices": null, + "unit": "cell/cumm", + "ideal": "4500-11000 cells/cumm", + "min": 4500, + "max": 11000, + "category_id": [ + 1 + ] + }, + { + "name": "Monocyte count", + "type": "Float", + "choices": null, + "unit": "cell/cumm", + "ideal": "4500-11000 cells/cumm", + "min": 4500, + "max": 11000, + "category_id": [ + 1 + ] + }, + { + "name": "neutrophil", + "type": "Float", + "choices": null, + "unit": "%", + "ideal": "40-60 %", + "min": 40, + "max": 60, + "category_id": [ + 1 + ] + }, + { + "name": "lymphocyte", + "type": "Float", + "choices": null, + "unit": "%", + "ideal": "20-4 %", + "min": 20, + "max": 4, + "category_id": [ + 1 + ] + }, + { + "name": "eosinophil", + "type": "Float", + "choices": null, + "unit": "%", + "ideal": "1-3 %", + "min": 1, + "max": 3, + "category_id": [ + 1 + ] + }, + { + "name": "basophile", + "type": "Float", + "choices": null, + "unit": "%", + "ideal": "0-1 %", + "min": 0, + "max": 1, + "category_id": [ + 1 + ] + }, + { + "name": "monocyte", + "type": "Float", + "choices": null, + "unit": "%", + "ideal": "4-8 %", + "min": 4, + "max": 8, + "category_id": [ + 1 + ] + }, + { + "name": "Hb", + "type": "Float", + "choices": null, + "unit": "gm%", + "ideal": "men 14-17 gm% , woman 12-16 gm% ,children 12-14 gm%", + "min": 12, + "max": 17, + "category_id": [ + 4 + ] + }, + { + "name": "PCV", + "type": "Float", + "choices": null, + "unit": "%", + "ideal": "Men 38-51 gm% , Woman 36-47%", + "min": 36, + "max": 51, + "category_id": [ + 1 + ] + }, + { + "name": "RBC count", + "type": "Float", + "choices": null, + "unit": "million/cumm", + "ideal": "4.5-6.0 million/cumm", + "min": 4.5, + "max": 6, + "category_id": [ + 4 + ] + }, + { + "name": "RDW", + "type": "Float", + "choices": null, + "unit": "%", + "ideal": "11.8 - 16.1%", + "min": 11.8, + "max": 16.1, + "category_id": [ + 1 + ] + }, + { + "name": "Platelets", + "type": "Float", + "choices": null, + "unit": "lakhs/cumm", + "ideal": "1.5-4.5 lakhs/cumm", + "min": 1.5, + "max": 4.5, + "category_id": [ + 4 + ] + }, + { + "name": "MCV", + "type": "Float", + "choices": null, + "unit": "Fl", + "ideal": "80-96 Fl", + "min": 80, + "max": 96, + "category_id": [ + 4 + ] + }, + { + "name": "MCH", + "type": "Float", + "choices": null, + "unit": "pg", + "ideal": "27-33 pg", + "min": 27, + "max": 33, + "category_id": [ + 4 + ] + }, + { + "name": "MCHC", + "type": "Float", + "choices": null, + "unit": "g/dl", + "ideal": "33.4-35.5 g/dl", + "min": 33.4, + "max": 35.5, + "category_id": [ + 4 + ] + }, + { + "name": "ESR", + "type": "Float", + "choices": null, + "unit": "mm/hr", + "ideal": "0-20 mm/hr", + "min": 0, + "max": 20, + "category_id": [ + 1 + ] + }, + { + "name": "Peripheral blood smear", + "type": "String", + "choices": null, + "unit": "", + "ideal": "", + "min": null, + "max": null, + "category_id": [ + 1 + ] + }, + { + "name": "Reticulocyte count", + "type": "Float", + "choices": null, + "unit": "%", + "ideal": "adults 0.5-1.5%, newborns 3-6%", + "min": 0.5, + "max": 6, + "category_id": [ + 1 + ] + }, + { + "name": "M P smear", + "type": "String", + "choices": null, + "unit": "", + "ideal": "", + "min": null, + "max": null, + "category_id": [ + 1 + ] + }, + { + "name": "FBS", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "70-110 mg/dl", + "min": 70, + "max": 110, + "category_id": [ + 2 + ] + }, + { + "name": "PPBS", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "< 140 mg/dl", + "min": 0, + "max": 140, + "category_id": [ + 2 + ] + }, + { + "name": "RBS", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "80-120 mg/dl", + "min": 80, + "max": 120, + "category_id": [ + 2 + ] + }, + { + "name": "T. Cholestrol", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "150-220 mg/dl", + "min": 150, + "max": 220, + "category_id": [ + 2 + ] + }, + { + "name": "LDL", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "< 130 mg/dl", + "min": 0, + "max": 130, + "category_id": [ + 2 + ] + }, + { + "name": "HDL", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "male 35-80 mg/dl, female 40-88 mg/dl", + "min": 35, + "max": 88, + "category_id": [ + 2 + ] + }, + { + "name": "Triglycerides", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "male 60-165 mg/dl, female 40-140 mg/dl", + "min": 40, + "max": 165, + "category_id": [ + 2 + ] + }, + { + "name": "VLDL", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "2-30 mg/dl", + "min": 2, + "max": 30, + "category_id": [ + 2 + ] + }, + { + "name": "Urea", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "10-50 mg/dl", + "min": 10, + "max": 50, + "category_id": [ + 2 + ] + }, + { + "name": "Uric Acid", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "male 3.5-7.2 mg/dl, female 2.6-6 mg/dl", + "min": 2.6, + "max": 7.2, + "category_id": [ + 2 + ] + }, + { + "name": "Creatinine", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "male 0.7-1.4 mg/dl, female 0.6-1.2 mg/dl", + "min": 0.6, + "max": 1.4, + "category_id": [ + 2 + ] + }, + { + "name": "CRP", + "type": "Float", + "choices": null, + "unit": "mg/l", + "ideal": "upto 6 mg/l", + "min": 0, + "max": 6, + "category_id": [ + 2 + ] + }, + { + "name": "Serum Sodium (Na+)", + "type": "Float", + "choices": null, + "unit": "mmol/l", + "ideal": "135-155 mmol/l", + "min": 135, + "max": 155, + "category_id": [ + 2 + ] + }, + { + "name": "Serum Potassium (K+)", + "type": "Float", + "choices": null, + "unit": "mmol/l", + "ideal": "3.5 - 5.5 mmol/l", + "min": 3.5, + "max": 5.5, + "category_id": [ + 2 + ] + }, + { + "name": "Serum Calcium", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "8.8-10.2 mg/dl", + "min": 8.8, + "max": 10.2, + "category_id": [ + 2 + ] + }, + { + "name": "Serum Phosphorus", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "children 4-7 mg/dl, adult 2.5-4.5 mg/dl", + "min": 2.5, + "max": 7, + "category_id": [ + 2 + ] + }, + { + "name": "Serum Chloride", + "type": "Float", + "choices": null, + "unit": "mmol/l", + "ideal": "96-109 mmol/l", + "min": 96, + "max": 109, + "category_id": [ + 2 + ] + }, + { + "name": "Serum Megnesium", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "1.6-2.6 mg/dl", + "min": 1.6, + "max": 2.6, + "category_id": [ + 2 + ] + }, + { + "name": "Total Bilirubin", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "adult upto 1.2 mg/dl, infant 0.2-8 mg/dl", + "min": 0.2, + "max": 8, + "category_id": [ + 6 + ] + }, + { + "name": "Direct Bilirubin", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "upto 0.4 mg/dl", + "min": 0, + "max": 0.4, + "category_id": [ + 6 + ] + }, + { + "name": "SGOT", + "type": "Float", + "choices": null, + "unit": "IU/L", + "ideal": "upto 46 IU/L", + "min": 0, + "max": 46, + "category_id": [ + 2 + ] + }, + { + "name": "SGPT", + "type": "Float", + "choices": null, + "unit": "IU/L", + "ideal": "upto 49 IU/L", + "min": 0, + "max": 49, + "category_id": [ + 2 + ] + }, + { + "name": "ALP", + "type": "Float", + "choices": null, + "unit": "IU/L", + "ideal": "male 80-306 IU/L, female 64-306 IU/L", + "min": 64, + "max": 306, + "category_id": [ + 2 + ] + }, + { + "name": "Total Protein", + "type": "Float", + "choices": null, + "unit": "g/dl", + "ideal": "6-8 g/dl", + "min": 6, + "max": 8, + "category_id": [ + 6 + ] + }, + { + "name": "Albumin", + "type": "Float", + "choices": null, + "unit": "g/dl", + "ideal": "3.5-5.2 g/dl", + "min": 3.5, + "max": 5.2, + "category_id": [ + 6 + ] + }, + { + "name": "Globulin", + "type": "Float", + "choices": null, + "unit": "g/dl", + "ideal": "1.5-2.5 g/dl", + "min": 1.5, + "max": 2.5, + "category_id": [ + 2 + ] + }, + { + "name": "PT", + "type": "Float", + "choices": null, + "unit": "sec", + "ideal": "9.1-12.1 seconds", + "min": 9.1, + "max": 12.1, + "category_id": [ + 2 + ] + }, + { + "name": "INR", + "type": "Float", + "choices": null, + "unit": "sec", + "ideal": "0.8-1.1 seconds", + "min": 0.8, + "max": 1.1, + "category_id": [ + 2 + ] + }, + { + "name": "APTT", + "type": "Float", + "choices": null, + "unit": "sec", + "ideal": "25.4-38.4 seconds", + "min": 25.4, + "max": 38.4, + "category_id": [ + 2 + ] + }, + { + "name": "D-Dimer", + "type": "Float", + "choices": null, + "unit": "ug/l", + "ideal": "< 0.5 ug/l", + "min": 0, + "max": 0.5, + "category_id": [ + 2 + ] + }, + { + "name": "Fibrinogen", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "200-400 mg/dl", + "min": 200, + "max": 400, + "category_id": [ + 2 + ] + }, + { + "name": "GCT", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "< 140 mg/dl", + "min": 0, + "max": 140, + "category_id": [ + 2 + ] + }, + { + "name": "GTT", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "140-200 mg/dl", + "min": 140, + "max": 200, + "category_id": [ + 2 + ] + }, + { + "name": "GGT", + "type": "Float", + "choices": null, + "unit": "U/L", + "ideal": "8 - 61 U/L (male),5 - 36 U/L (female)", + "min": 3, + "max": 300, + "category_id": [ + 6 + ] + }, + { + "name": "HbA1C", + "type": "Float", + "choices": null, + "unit": "%", + "ideal": "4-5.6 %", + "min": 4, + "max": 5.6, + "category_id": [ + 2 + ] + }, + { + "name": "Serum Copper", + "type": "Float", + "choices": null, + "unit": "mcg/dl", + "ideal": "85-180 mcg/dl", + "min": 85, + "max": 180, + "category_id": [ + 2 + ] + }, + { + "name": "Serum Lead", + "type": "Float", + "choices": null, + "unit": "mcg/dl", + "ideal": "upto 10 mcg/dl", + "min": 0, + "max": 10, + "category_id": [ + 2 + ] + }, + { + "name": "Iron", + "type": "Float", + "choices": null, + "unit": "mcg/dl", + "ideal": "60-170 mcg/dl", + "min": 60, + "max": 170, + "category_id": [ + 2 + ] + }, + { + "name": "TIBC", + "type": "Float", + "choices": null, + "unit": "mcg/dl", + "ideal": "250-450 mcg/dl", + "min": 250, + "max": 450, + "category_id": [ + 2 + ] + }, + { + "name": "Transferin Saturation", + "type": "Float", + "choices": null, + "unit": "%", + "ideal": "15-50 %", + "min": 15, + "max": 50, + "category_id": [ + 2 + ] + }, + { + "name": "IL6", + "type": "Float", + "choices": null, + "unit": "pg/ml", + "ideal": "0-16.4 pg/ml", + "min": 0, + "max": 16.4, + "category_id": [ + 2 + ] + }, + { + "name": "Lactate", + "type": "Float", + "choices": null, + "unit": "mmol/l", + "ideal": "0.5-1 mmol/l", + "min": 0.5, + "max": 1, + "category_id": [ + 2 + ] + }, + { + "name": "Ceruloplasmin", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "14-40 mg/dl", + "min": 14, + "max": 40, + "category_id": [ + 2 + ] + }, + { + "name": "ACP", + "type": "Float", + "choices": null, + "unit": "U/L", + "ideal": "0.13-0.63 U/L", + "min": 0.13, + "max": 0.63, + "category_id": [ + 2 + ] + }, + { + "name": "Protein C", + "type": "Float", + "choices": null, + "unit": "IU dl 1", + "ideal": "65-135 IU dl 1", + "min": 65, + "max": 135, + "category_id": [ + 2 + ] + }, + { + "name": "Protein S", + "type": "Float", + "choices": null, + "unit": "%", + "ideal": "70-140 %", + "min": 70, + "max": 140, + "category_id": [ + 2 + ] + }, + { + "name": "G6PD", + "type": "Float", + "choices": null, + "unit": "U/g Hb", + "ideal": "neonate 10.15-14.71 U/g Hb, adult 6.75-11.95 U/g Hb", + "min": null, + "max": null, + "category_id": [ + 2 + ] + }, + { + "name": "ACCP", + "type": "Float", + "choices": null, + "unit": "EU/ml", + "ideal": "< 20 EU/ml", + "min": 0, + "max": 20, + "category_id": [ + 2 + ] + }, + { + "name": "Ferritin", + "type": "Float", + "choices": null, + "unit": "ng/ml", + "ideal": "20-250 ng/ml", + "min": 20, + "max": 250, + "category_id": [ + 2 + ] + }, + { + "name": "LDH", + "type": "Float", + "choices": null, + "unit": "U/L", + "ideal": "140-280 U/L", + "min": 140, + "max": 280, + "category_id": [ + 2 + ] + }, + { + "name": "Amylase", + "type": "Float", + "choices": null, + "unit": "U/L", + "ideal": "60-180 U/L", + "min": 60, + "max": 180, + "category_id": [ + 2 + ] + }, + { + "name": "Lipase", + "type": "Float", + "choices": null, + "unit": "U/L", + "ideal": "0-160 U/L", + "min": 0, + "max": 160, + "category_id": [ + 2 + ] + }, + { + "name": "Ammonia", + "type": "Float", + "choices": null, + "unit": "ug/dL", + "ideal": "15-45 ug/dL", + "min": 15, + "max": 45, + "category_id": [ + 2 + ] + }, + { + "name": "CKMB", + "type": "Float", + "choices": null, + "unit": "IU/L", + "ideal": "5-25 IU/L", + "min": 5, + "max": 25, + "category_id": [ + 2 + ] + }, + { + "name": "CK NAC", + "type": "Float", + "choices": null, + "unit": "U/L", + "ideal": "male < 171 U/L, female < 145 U/L", + "min": null, + "max": null, + "category_id": [ + 2 + ] + }, + { + "name": "24hrs Urine Protein", + "type": "Float", + "choices": null, + "unit": "mg/dl", + "ideal": "<10 mg/dl", + "min": 0, + "max": 10, + "category_id": [ + 7 + ] + }, + { + "name": "24hrs Urine Uric Acid", + "type": "Float", + "choices": null, + "unit": "mg/24hr", + "ideal": "250-750 mg/24hr", + "min": 250, + "max": 750, + "category_id": [ + 2 + ] + }, + { + "name": "24 hrs Urine Oxalate", + "type": "Float", + "choices": null, + "unit": "mg/L", + "ideal": "<15 mg/L", + "min": 0, + "max": 15, + "category_id": [ + 2 + ] + }, + { + "name": "Urine Microalbumin", + "type": "Float", + "choices": null, + "unit": "mg", + "ideal": "< 30 mg", + "min": 0, + "max": 30, + "category_id": [ + 2 + ] + }, + { + "name": "Urine Sodium", + "type": "Float", + "choices": null, + "unit": "mEq/day", + "ideal": "40-220 mEq/day", + "min": 40, + "max": 220, + "category_id": [ + 2 + ] + }, + { + "name": "PCT", + "type": "Float", + "choices": null, + "unit": "ng/ml", + "ideal": "< 0.15 ng/ml", + "min": 0, + "max": 0.15, + "category_id": [ + 2 + ] + }, + { + "name": "T3", + "type": "Float", + "choices": null, + "unit": "ng/dl", + "ideal": "80-220 ng/dl", + "min": 80, + "max": 220, + "category_id": [ + 2 + ] + }, + { + "name": "T4", + "type": "Float", + "choices": null, + "unit": "ug/L", + "ideal": "5-12 ug/L", + "min": 5, + "max": 12, + "category_id": [ + 2 + ] + }, + { + "name": "TSH", + "type": "Float", + "choices": null, + "unit": "mIU/L", + "ideal": "0.5-5 mIU/L", + "min": 0.5, + "max": 5, + "category_id": [ + 2 + ] + }, + { + "name": "FT3", + "type": "Float", + "choices": null, + "unit": "ng/dl", + "ideal": "60-180 ng/dl", + "min": 60, + "max": 180, + "category_id": [ + 2 + ] + }, + { + "name": "FT4", + "type": "Float", + "choices": null, + "unit": "ng/dl", + "ideal": "0.7-1.9 ng/dl", + "min": 0.7, + "max": 1.9, + "category_id": [ + 2 + ] + }, + { + "name": "Estradiol", + "type": "Float", + "choices": null, + "unit": "pg/ml", + "ideal": "premenopausal women 30-400 pg/ml, post-menopausal women 0-30 pg/ml, men 10-50 pg/ml", + "min": null, + "max": null, + "category_id": [ + 2 + ] + }, + { + "name": "Growth Hormone", + "type": "Float", + "choices": null, + "unit": "ng/ml", + "ideal": "male 0.4-10 ng/ml, female 1-14 ng/ml", + "min": 0.4, + "max": 14, + "category_id": [ + 2 + ] + }, + { + "name": "Cortisol", + "type": "Float", + "choices": null, + "unit": "mcg/dl", + "ideal": "5-25 ng/ml", + "min": 5, + "max": 25, + "category_id": [ + 2 + ] + }, + { + "name": "PTH", + "type": "Float", + "choices": null, + "unit": "pg/ml", + "ideal": "14-65 pg/ml", + "min": 14, + "max": 65, + "category_id": [ + 2 + ] + }, + { + "name": "Prolactine", + "type": "Float", + "choices": null, + "unit": "ng/ml", + "ideal": "male <20 ng/ml, female <25 ng/ml, pregnant women <300 ng/ml", + "min": null, + "max": null, + "category_id": [ + 2 + ] + }, + { + "name": "Pro BNP", + "type": "Float", + "choices": null, + "unit": "pg/ml", + "ideal": "< 300 pg/ml", + "min": 0, + "max": 300, + "category_id": [ + 2 + ] + }, + { + "name": "Vitamine D3", + "type": "Float", + "choices": null, + "unit": "ng/ml", + "ideal": "20-40 ng/ml", + "min": 20, + "max": 40, + "category_id": [ + 2 + ] + }, + { + "name": "Vitamine B12", + "type": "Float", + "choices": null, + "unit": "pg/ml", + "ideal": "160-950 pg/ml", + "min": 160, + "max": 950, + "category_id": [ + 2 + ] + }, + { + "name": "FSH", + "type": "Float", + "choices": null, + "unit": "IU/L", + "ideal": "before puberty 0-4 IU/L, during puberty 0.36-10 IU/L, ", + "min": 0, + "max": 10, + "category_id": [ + 2 + ] + }, + { + "name": "LH", + "type": "Float", + "choices": null, + "unit": "IU/L", + "ideal": "before menopause 5-25 IU/L, after menopause 14.2-52.3 IU/L", + "min": 5, + "max": 52.3, + "category_id": [ + 2 + ] + }, + { + "name": "PSA", + "type": "Float", + "choices": null, + "unit": "ng/ml", + "ideal": "<4 ng/ml", + "min": 0, + "max": 4, + "category_id": [ + 2 + ] + }, + { + "name": "ACTH", + "type": "Float", + "choices": null, + "unit": "pg/ml", + "ideal": "10-60 pg/ml", + "min": 10, + "max": 60, + "category_id": [ + 2 + ] + }, + { + "name": "CEA", + "type": "Float", + "choices": null, + "unit": "ng/ml", + "ideal": "0-2.5 ng/ml", + "min": 0, + "max": 2.5, + "category_id": [ + 2 + ] + }, + { + "name": "AFP", + "type": "Float", + "choices": null, + "unit": "ng/ml", + "ideal": "10-20 ng/ml", + "min": 10, + "max": 20, + "category_id": [ + 2 + ] + }, + { + "name": "CA125", + "type": "Float", + "choices": null, + "unit": "U/ml", + "ideal": "< 46 U/ml", + "min": 0, + "max": 46, + "category_id": [ + 2 + ] + }, + { + "name": "CA19.9", + "type": "Float", + "choices": null, + "unit": "U/ml", + "ideal": "0-37 U/ml", + "min": 0, + "max": 37, + "category_id": [ + 2 + ] + }, + { + "name": "Testosterone ", + "type": "Float", + "choices": null, + "unit": "ng/dl", + "ideal": "270-1070 ng/dl", + "min": 270, + "max": 1070, + "category_id": [ + 2 + ] + }, + { + "name": "Progestrone", + "type": "Float", + "choices": null, + "unit": "ng/ml", + "ideal": "female pre ovulation, menupausal women, men <1 ng/ml, mid-cycle 5-20 ng/ml", + "min": null, + "max": null, + "category_id": [ + 2 + ] + }, + { + "name": "Serum IgG", + "type": "Float", + "choices": null, + "unit": "g/L", + "ideal": "6-16 g/L", + "min": 6, + "max": 16, + "category_id": [ + 2 + ] + }, + { + "name": "Serum IgE", + "type": "Float", + "choices": null, + "unit": "UL/ml", + "ideal": "150-1000 UL/ml", + "min": 150, + "max": 1000, + "category_id": [ + 2 + ] + }, + { + "name": "Serum IgM", + "type": "Float", + "choices": null, + "unit": "g/L", + "ideal": "0.4-2.5 g/L", + "min": 0.4, + "max": 2.5, + "category_id": [ + 2 + ] + }, + { + "name": "Serum IgA", + "type": "Float", + "choices": null, + "unit": "G/L", + "ideal": "0.8-3 g/L", + "min": 0.8, + "max": 3, + "category_id": [ + 2 + ] + }, + { + "name": "Colour", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "Appearence", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "Ph", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "Specific Gravity", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "Nitrite", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "Urobilinogen", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "Bile Salt", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "Bile Pigment", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "Acetone", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "Sugar", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "Puscells", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "Epithetical Cells", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "RBC ", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "Cast", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "Crystal", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "others", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "UPT", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "Stool OB", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "Stool Microscopy", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3 + ] + }, + { + "name": "HBA1C", + "type": "Float", + "choices": "-", + "unit": "%", + "ideal": "4%- 5.6%", + "min": 0, + "max": 10, + "category_id": [ + 1 + ] + }, + { + "name": "Haemoglobin", + "type": "Float", + "choices": null, + "unit": "gm/dl", + "ideal": "Male 13-18,Female 11-16", + "min": 0, + "max": 25, + "category_id": [ + 4 + ] + }, + { + "name": "PCV/HCT", + "type": "Float", + "choices": null, + "unit": "%", + "ideal": "Male: 38.8% - 50.0%,Female: 34.9% - 44.5%", + "min": 0, + "max": 100, + "category_id": [ + 4 + ] + }, + { + "name": "White Blood Cell (WBC) Count", + "type": "Float", + "choices": null, + "unit": "thousands/\u03bcL ", + "ideal": "4.5 - 11.0", + "min": 0, + "max": 100, + "category_id": [ + 4 + ] + }, + { + "name": "Neutrophils", + "type": "Float", + "choices": null, + "unit": "%", + "ideal": "40-60", + "min": 0, + "max": 100, + "category_id": [ + 4, + 5 + ] + }, + { + "name": "Lymphocytes", + "type": "Float", + "choices": null, + "unit": "%", + "ideal": "20% - 40%", + "min": 0, + "max": 100, + "category_id": [ + 4, + 5 + ] + }, + { + "name": "Monocytes", + "type": "Float", + "choices": null, + "unit": "%", + "ideal": "2% - 8%", + "min": 0, + "max": 100, + "category_id": [ + 4, + 5 + ] + }, + { + "name": "Eosinophils", + "type": "Float", + "choices": null, + "unit": "%", + "ideal": "1% - 4%", + "min": 0, + "max": 100, + "category_id": [ + 4, + 5 + ] + }, + { + "name": "Basophils", + "type": "Float", + "choices": null, + "unit": "%", + "ideal": "0% - 1%", + "min": 0, + "max": 100, + "category_id": [ + 4, + 5 + ] + }, + { + "name": "Platelet Count", + "type": "Float", + "choices": null, + "unit": "thousands/\u03bcL ", + "ideal": "150,000 - 450,000 ", + "min": 0, + "max": 100000000, + "category_id": [ + 4 + ] + }, + { + "name": "Alanine Aminotransferase (ALT)", + "type": "Float", + "choices": null, + "unit": "u/l", + "ideal": "7 - 55 U/L (male),7 - 45 U/L (female)", + "min": 0, + "max": 1500, + "category_id": [ + 6 + ] + }, + { + "name": "Aspartate Aminotransferase (AST)", + "type": "Float", + "choices": null, + "unit": "u/l", + "ideal": "8 - 48 U/L ", + "min": 5, + "max": 300, + "category_id": [ + 6 + ] + }, + { + "name": "Alkaline Phosphatase (ALP)", + "type": "Float", + "choices": null, + "unit": "u/l", + "ideal": "Newborn (0-30 days) 150 - 420 U/L,Infants (1-11 months) 70 - 320 U/L,Children (1-3 years) 80 - 280 U/L,Children (4-6 years) 80 - 230 U/L,Children (7-9 years) 65 - 230 U/L,Children (10-13 years) 45 - 250 U/L,Children (14-17 years) 40 - 220 U/L,Adults (>18 years) 44 - 147 U/L", + "min": 30, + "max": 1200, + "category_id": [ + 6 + ] + }, + { + "name": "Indirect Bilirubin", + "type": "Float", + "choices": null, + "unit": "mg/dL ", + "ideal": "0.2 - 0.8 mg/dL", + "min": 0, + "max": 1, + "category_id": [ + 6 + ] + }, + { + "name": "Prothrombin Time (PT)", + "type": "Float", + "choices": null, + "unit": "Seconds", + "ideal": "Newborn (0-2 days)- 14.0 - 20.5 seconds,Infant (3 days - 1 month)- 14.0 - 19.2 seconds,Infant (1 - 6 months)- 13.3 - 18.7 seconds,Infant (6 - 12 months)- 13.3 - 17.8 seconds,Toddler (1 - 2 years) -12.5 - 16.5 seconds,Child (2 - 6 years) - 11.5 - 15.5 seconds,Child (7 - 12 years) - 11.8 - 14.5 seconds,Adolescent (13 - 15 years) - 12.0 - 14.6 seconds,Adolescent (16 - 17 years) - 11.7 - 14.2 seconds,Adult (> 18 years) -11.0 - 13.0 seconds", + "min": 5, + "max": 30, + "category_id": [ + 6 + ] + }, + { + "name": "Serum Creatinine", + "type": "Float", + "choices": null, + "unit": "mg/dL ", + "ideal": "Infants (0-11 months) 0.2 - 0.4 mg/dL,Children (1-17 years) 0.3 - 0.7 mg/dL (varies with age),Adult (18-60 years) 0.6 - 1.2 mg/dL,Adult (> 60 years) 0.6 - 1.3 mg/dL", + "min": 0, + "max": 2, + "category_id": [ + 7 + ] + }, + { + "name": "Blood Urea Nitrogen (BUN)", + "type": "Float", + "choices": null, + "unit": "mg/dL ", + "ideal": "Newborn (0-2 days) 3 - 17 mg/dL,Infant (3 days - 1 month) 3 - 19 mg/dL,Infant (1 - 6 months) 5 - 20 mg/dL,Infant (6 - 12 months) 5 - 13 mg/dL,Toddler (1 - 2 years) 7 - 20 mg/dL,Child (3 - 5 years) 8 - 18 mg/dL,Child (6 - 11 years) 8 - 21 mg/dL,Adolescent (12 - 17 years) 7 - 20 mg/dL,Adult (> 18 years) 7 - 20 mg/dL", + "min": 0, + "max": 25, + "category_id": [ + 7 + ] + }, + { + "name": "Estimated Glomerular Filtration Rate (eGFR)", + "type": "Float", + "choices": null, + "unit": "mL/min/1.73m\u00b2 (milliliters per minute per 1.73 square meters)", + "ideal": "> 90 mL/min/1.73m\u00b2 ", + "min": 0, + "max": 90, + "category_id": [ + 7 + ] + }, + { + "name": "Blood Uric Acid", + "type": "Float", + "choices": null, + "unit": "mg/dL ", + "ideal": "3.5 - 7.2 mg/dL (males),2.6 - 6.0 mg/dL (females)", + "min": 2, + "max": 10, + "category_id": [] + }, + { + "name": "Urinalysis", + "type": "String", + "choices": null, + "unit": null, + "ideal": null, + "min": null, + "max": null, + "category_id": [ + 3, + 7 + ] + }, + { + "name": "Serum Creatinine Clearance", + "type": "Float", + "choices": null, + "unit": "mL/min ", + "ideal": "110 to 150mL/min (males),100 to 130mL/min (females)", + "min": "Varies based on age, sex, and muscle mass", + "max": "55 - 105 mL/min (females)", + "category_id": [ + 7 + ] + } +]