diff --git a/.env.example b/.env.example index 091bea02fd..057f853951 100644 --- a/.env.example +++ b/.env.example @@ -7,8 +7,6 @@ DATABASE_URL=postgres://postgres:postgres@localhost:5433/care REDIS_URL=redis://localhost:6380 CELERY_BROKER_URL=redis://localhost:6380/0 -FIDELIUS_URL=http://localhost:8092 - DJANGO_DEBUG=False BUCKET_REGION=ap-south-1 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 04cb82a8da..e6d222c56c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1 @@ -* @ohcnetwork/care-developers -*.yml @tomahawk-pilot +* @ohcnetwork/care-backend-admins diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e2aa44c12f..34b73cff0a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -102,6 +102,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} build-args: | APP_VERSION=${{ github.sha }} + ADDITIONAL_PLUGS=${{ env.ADDITIONAL_PLUGS }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4bceff1ce5..019ec021aa 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: "3.13" cache: 'pipenv' - name: Install pipenv diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index cbf322bd10..37d7c67fad 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -14,7 +14,11 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-python@v3 + + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - uses: pre-commit/action@v3.0.1 with: extra_args: --color=always --from-ref ${{ github.event.pull_request.base.sha }} --to-ref ${{ github.event.pull_request.head.sha }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4cad6717e2..2969bbc4da 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ default_stages: [commit] repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: no-commit-to-branch args: [--branch, develop, --branch, staging, --branch, production] @@ -17,7 +17,7 @@ repos: - id: check-toml - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.6.7 + rev: v0.7.0 hooks: - id: ruff args: [ --fix ] diff --git a/Pipfile b/Pipfile index 16f5923139..b18cab9908 100644 --- a/Pipfile +++ b/Pipfile @@ -6,17 +6,17 @@ name = "pypi" [packages] argon2-cffi = "==23.1.0" authlib = "==1.3.2" -boto3 = "==1.35.29" +boto3 = "==1.35.49" celery = "==5.4.0" -django = "==5.1.1" +django = "==5.1.2" django-environ = "==0.11.2" -django-cors-headers = "==4.4.0" +django-cors-headers = "==4.5.0" django-filter = "==24.3" django-maintenance-mode = "==0.21.1" django-queryset-csv = "==1.1.0" django-ratelimit = "==4.1.0" django-redis = "==5.4.0" -django-rest-passwordreset = "==1.4.1" +django-rest-passwordreset = "==1.4.2" django-simple-history = "==3.7.0" djangoql = "==0.18.1" djangorestframework = "==3.15.2" @@ -28,39 +28,37 @@ drf-spectacular = "==0.27.2" gunicorn = "==23.0.0" healthy-django = "==0.1.0" jsonschema = "==4.23.0" -jwcrypto = "==1.5.6" -newrelic = "==10.0.0" -pillow = "==10.4.0" -psycopg = { extras = ["c"], version = "==3.2.2" } -pycryptodome = "==3.20.0" +newrelic = "==10.2.0" +pillow = "==11.0.0" +psycopg = { extras = ["c"], version = "==3.2.3" } pydantic = "==1.10.18" # fix for fhir.resources < 7.0.2 pyjwt = "==2.9.0" python-slugify = "==8.0.4" -pywebpush = "==2.0.0" +pywebpush = "==2.0.1" redis = { extras = ["hiredis"], version = "==5.0.8" } # constraint for redis-om redis-om = "==0.3.1" # > 0.3.1 broken with pydantic < 2 requests = "==2.32.3" -sentry-sdk = "==2.14.0" +sentry-sdk = "==2.17.0" whitenoise = "==6.7.0" [dev-packages] -boto3-stubs = { extras = ["s3", "boto3"], version = "==1.35.29" } -coverage = "==7.6.1" -debugpy = "==1.8.6" +boto3-stubs = { extras = ["s3", "boto3"], version = "==1.35.49" } +coverage = "==7.6.4" +debugpy = "==1.8.7" django-coverage-plugin = "==3.1.0" django-extensions = "==3.2.3" django-silk = "==5.2.0" djangorestframework-stubs = "==3.15.1" factory-boy = "==3.3.1" freezegun = "==1.5.1" -ipython = "==8.27.0" -mypy = "==1.11.2" -pre-commit = "==3.8.0" +ipython = "==8.28.0" +mypy = "==1.12.1" +pre-commit = "==4.0.1" requests-mock = "==1.12.1" tblib = "==3.0.0" watchdog = "==5.0.3" -werkzeug = "==3.0.4" -ruff = "==0.6.8" +werkzeug = "==3.0.6" +ruff = "==0.7.0" [docs] furo = "==2024.8.6" @@ -68,4 +66,4 @@ sphinx = "==8.0.2" myst-parser = "==4.0.0" [requires] -python_version = "3.12" +python_version = "3.13" diff --git a/Pipfile.lock b/Pipfile.lock index dda896d1fc..94a791a397 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "82b40d895920ac109f51b1e331c891ba2c9e3ebe5e7feded2cd8cc01bbd948d0" + "sha256": "e1007c202923fb8d82dc686ed4d9f3ff7e05afa8cbd6e81ea857baf3c0397a75" }, "pipfile-spec": 6, "requires": { - "python_version": "3.12" + "python_version": "3.13" }, "sources": [ { @@ -26,100 +26,100 @@ }, "aiohttp": { "hashes": [ - "sha256:10c7932337285a6bfa3a5fe1fd4da90b66ebfd9d0cbd1544402e1202eb9a8c3e", - "sha256:177126e971782769b34933e94fddd1089cef0fe6b82fee8a885e539f5b0f0c6a", - "sha256:1ce46dfb49cfbf9e92818be4b761d4042230b1f0e05ffec0aad15b3eb162b905", - "sha256:1e7a6af57091056a79a35104d6ec29d98ec7f1fb7270ad9c6fff871b678d1ff8", - "sha256:21a72f4a9c69a8567a0aca12042f12bba25d3139fd5dd8eeb9931f4d9e8599cd", - "sha256:21c1925541ca84f7b5e0df361c0a813a7d6a56d3b0030ebd4b220b8d232015f9", - "sha256:21f8225f7dc187018e8433c9326be01477fb2810721e048b33ac49091b19fb4a", - "sha256:22cdeb684d8552490dd2697a5138c4ecb46f844892df437aaf94f7eea99af879", - "sha256:270e653b5a4b557476a1ed40e6b6ce82f331aab669620d7c95c658ef976c9c5e", - "sha256:2df786c96c57cd6b87156ba4c5f166af7b88f3fc05f9d592252fdc83d8615a3c", - "sha256:32710d6b3b6c09c60c794d84ca887a3a2890131c0b02b3cefdcc6709a2260a7c", - "sha256:33a68011a38020ed4ff41ae0dbf4a96a202562ecf2024bdd8f65385f1d07f6ef", - "sha256:365783e1b7c40b59ed4ce2b5a7491bae48f41cd2c30d52647a5b1ee8604c68ad", - "sha256:3a95d2686bc4794d66bd8de654e41b5339fab542b2bca9238aa63ed5f4f2ce82", - "sha256:3b2036479b6b94afaaca7d07b8a68dc0e67b0caf5f6293bb6a5a1825f5923000", - "sha256:3c7f270f4ca92760f98a42c45a58674fff488e23b144ec80b1cc6fa2effed377", - "sha256:3f6d47e392c27206701565c8df4cac6ebed28fdf6dcaea5b1eea7a4631d8e6db", - "sha256:40d2d719c3c36a7a65ed26400e2b45b2d9ed7edf498f4df38b2ae130f25a0d01", - "sha256:4618f0d2bf523043866a9ff8458900d8eb0a6d4018f251dae98e5f1fb699f3a8", - "sha256:471a8c47344b9cc309558b3fcc469bd2c12b49322b4b31eb386c4a2b2d44e44a", - "sha256:4954e6b06dd0be97e1a5751fc606be1f9edbdc553c5d9b57d72406a8fbd17f9d", - "sha256:497a7d20caea8855c5429db3cdb829385467217d7feb86952a6107e033e031b9", - "sha256:4b91f4f62ad39a8a42d511d66269b46cb2fb7dea9564c21ab6c56a642d28bff5", - "sha256:4dbf252ac19860e0ab56cd480d2805498f47c5a2d04f5995d8d8a6effd04b48c", - "sha256:4e10b04542d27e21538e670156e88766543692a0a883f243ba8fad9ddea82e53", - "sha256:5284997e3d88d0dfb874c43e51ae8f4a6f4ca5b90dcf22995035187253d430db", - "sha256:57359785f27394a8bcab0da6dcd46706d087dfebf59a8d0ad2e64a4bc2f6f94f", - "sha256:597128cb7bc5f068181b49a732961f46cb89f85686206289d6ccb5e27cb5fbe2", - "sha256:5aa1a073514cf59c81ad49a4ed9b5d72b2433638cd53160fd2f3a9cfa94718db", - "sha256:680dbcff5adc7f696ccf8bf671d38366a1f620b5616a1d333d0cb33956065395", - "sha256:6984dda9d79064361ab58d03f6c1e793ea845c6cfa89ffe1a7b9bb400dfd56bd", - "sha256:69de056022e7abf69cb9fec795515973cc3eeaff51e3ea8d72a77aa933a91c52", - "sha256:6c7efa6616a95e3bd73b8a69691012d2ef1f95f9ea0189e42f338fae080c2fc6", - "sha256:6d1ad868624f6cea77341ef2877ad4e71f7116834a6cd7ec36ec5c32f94ee6ae", - "sha256:713dff3f87ceec3bde4f3f484861464e722cf7533f9fa6b824ec82bb5a9010a7", - "sha256:71462f8eeca477cbc0c9700a9464e3f75f59068aed5e9d4a521a103692da72dc", - "sha256:7c38cfd355fd86c39b2d54651bd6ed7d63d4fe3b5553f364bae3306e2445f847", - "sha256:8296edd99d0dd9d0eb8b9e25b3b3506eef55c1854e9cc230f0b3f885f680410b", - "sha256:85431c9131a9a0f65260dc7a65c800ca5eae78c4c9931618f18c8e0933a0e0c1", - "sha256:85e4d7bd05d18e4b348441e7584c681eff646e3bf38f68b2626807f3add21aa2", - "sha256:8885ca09d3a9317219c0831276bfe26984b17b2c37b7bf70dd478d17092a4772", - "sha256:8960fabc20bfe4fafb941067cda8e23c8c17c98c121aa31c7bf0cdab11b07842", - "sha256:9443d9ebc5167ce1fbb552faf2d666fb22ef5716a8750be67efd140a7733738c", - "sha256:9721554bfa9e15f6e462da304374c2f1baede3cb06008c36c47fa37ea32f1dc4", - "sha256:98a4eb60e27033dee9593814ca320ee8c199489fbc6b2699d0f710584db7feb7", - "sha256:98fae99d5c2146f254b7806001498e6f9ffb0e330de55a35e72feb7cb2fa399b", - "sha256:9a281cba03bdaa341c70b7551b2256a88d45eead149f48b75a96d41128c240b3", - "sha256:a087c84b4992160ffef7afd98ef24177c8bd4ad61c53607145a8377457385100", - "sha256:a1ba7bc139592339ddeb62c06486d0fa0f4ca61216e14137a40d626c81faf10c", - "sha256:a3081246bab4d419697ee45e555cef5cd1def7ac193dff6f50be761d2e44f194", - "sha256:a72f89aea712c619b2ca32c6f4335c77125ede27530ad9705f4f349357833695", - "sha256:a78ba86d5a08207d1d1ad10b97aed6ea48b374b3f6831d02d0b06545ac0f181e", - "sha256:a961ee6f2cdd1a2be4735333ab284691180d40bad48f97bb598841bfcbfb94ec", - "sha256:ab1546fc8e00676febc81c548a876c7bde32f881b8334b77f84719ab2c7d28dc", - "sha256:ab2d6523575fc98896c80f49ac99e849c0b0e69cc80bf864eed6af2ae728a52b", - "sha256:aff048793d05e1ce05b62e49dccf81fe52719a13f4861530706619506224992b", - "sha256:b1a012677b8e0a39e181e218de47d6741c5922202e3b0b65e412e2ce47c39337", - "sha256:b667e2a03407d79a76c618dc30cedebd48f082d85880d0c9c4ec2faa3e10f43e", - "sha256:b91557ee0893da52794b25660d4f57bb519bcad8b7df301acd3898f7197c5d81", - "sha256:badb51d851358cd7535b647bb67af4854b64f3c85f0d089c737f75504d5910ec", - "sha256:c36074b26f3263879ba8e4dbd33db2b79874a3392f403a70b772701363148b9f", - "sha256:c4916070e12ae140110aa598031876c1bf8676a36a750716ea0aa5bd694aa2e7", - "sha256:c6769d71bfb1ed60321363a9bc05e94dcf05e38295ef41d46ac08919e5b00d19", - "sha256:c887019dbcb4af58a091a45ccf376fffe800b5531b45c1efccda4bedf87747ea", - "sha256:cd9716ef0224fe0d0336997eb242f40619f9f8c5c57e66b525a1ebf9f1d8cebe", - "sha256:ceacea31f8a55cdba02bc72c93eb2e1b77160e91f8abd605969c168502fd71eb", - "sha256:d088ca05381fd409793571d8e34eca06daf41c8c50a05aeed358d2d340c7af81", - "sha256:d3a79200a9d5e621c4623081ddb25380b713c8cf5233cd11c1aabad990bb9381", - "sha256:d82404a0e7b10e0d7f022cf44031b78af8a4f99bd01561ac68f7c24772fed021", - "sha256:d95ae4420669c871667aad92ba8cce6251d61d79c1a38504621094143f94a8b4", - "sha256:da57af0c54a302b7c655fa1ccd5b1817a53739afa39924ef1816e7b7c8a07ccb", - "sha256:ddb9b9764cfb4459acf01c02d2a59d3e5066b06a846a364fd1749aa168efa2be", - "sha256:de23085cf90911600ace512e909114385026b16324fa203cc74c81f21fd3276a", - "sha256:e1f0f7b27171b2956a27bd8f899751d0866ddabdd05cbddf3520f945130a908c", - "sha256:e32148b4a745e70a255a1d44b5664de1f2e24fcefb98a75b60c83b9e260ddb5b", - "sha256:e45fdfcb2d5bcad83373e4808825b7512953146d147488114575780640665027", - "sha256:e56bb7e31c4bc79956b866163170bc89fd619e0581ce813330d4ea46921a4881", - "sha256:e860985f30f3a015979e63e7ba1a391526cdac1b22b7b332579df7867848e255", - "sha256:ee3587506898d4a404b33bd19689286ccf226c3d44d7a73670c8498cd688e42c", - "sha256:ee97c4e54f457c366e1f76fbbf3e8effee9de57dae671084a161c00f481106ce", - "sha256:ef9b484604af05ca745b6108ca1aaa22ae1919037ae4f93aaf9a37ba42e0b835", - "sha256:f21e8f2abed9a44afc3d15bba22e0dfc71e5fa859bea916e42354c16102b036f", - "sha256:f23a6c1d09de5de89a33c9e9b229106cb70dcfdd55e81a3a3580eaadaa32bc92", - "sha256:f5d5d5401744dda50b943d8764508d0e60cc2d3305ac1e6420935861a9d544bc", - "sha256:f78e2a78432c537ae876a93013b7bc0027ba5b93ad7b3463624c4b6906489332", - "sha256:f8179855a4e4f3b931cb1764ec87673d3fbdcca2af496c8d30567d7b034a13db", - "sha256:fc0e7f91705445d79beafba9bb3057dd50830e40fe5417017a76a214af54e122", - "sha256:fe285a697c851734285369614443451462ce78aac2b77db23567507484b1dc6f", - "sha256:fe3d79d6af839ffa46fdc5d2cf34295390894471e9875050eafa584cb781508d", - "sha256:fecd55e7418fabd297fd836e65cbd6371aa4035a264998a091bbf13f94d9c44d", - "sha256:ffef3d763e4c8fc97e740da5b4d0f080b78630a3914f4e772a122bbfa608c1db" - ], - "markers": "python_version >= '3.8'", - "version": "==3.10.8" + "sha256:007ec22fbc573e5eb2fb7dec4198ef8f6bf2fe4ce20020798b2eb5d0abda6138", + "sha256:00819de9e45d42584bed046314c40ea7e9aea95411b38971082cad449392b08c", + "sha256:01948b1d570f83ee7bbf5a60ea2375a89dfb09fd419170e7f5af029510033d24", + "sha256:038f514fe39e235e9fef6717fbf944057bfa24f9b3db9ee551a7ecf584b5b480", + "sha256:03a42ac7895406220124c88911ebee31ba8b2d24c98507f4a8bf826b2937c7f2", + "sha256:05646ebe6b94cc93407b3bf34b9eb26c20722384d068eb7339de802154d61bc5", + "sha256:0631dd7c9f0822cc61c88586ca76d5b5ada26538097d0f1df510b082bad3411a", + "sha256:0b00807e2605f16e1e198f33a53ce3c4523114059b0c09c337209ae55e3823a8", + "sha256:0e1b370d8007c4ae31ee6db7f9a2fe801a42b146cec80a86766e7ad5c4a259cf", + "sha256:15ecd889a709b0080f02721255b3f80bb261c2293d3c748151274dfea93ac871", + "sha256:1b66ccafef7336a1e1f0e389901f60c1d920102315a56df85e49552308fc0486", + "sha256:1bbb122c557a16fafc10354b9d99ebf2f2808a660d78202f10ba9d50786384b9", + "sha256:1eb89d3d29adaf533588f209768a9c02e44e4baf832b08118749c5fad191781d", + "sha256:258c5dd01afc10015866114e210fb7365f0d02d9d059c3c3415382ab633fcbcb", + "sha256:2609e9ab08474702cc67b7702dbb8a80e392c54613ebe80db7e8dbdb79837c68", + "sha256:274cfa632350225ce3fdeb318c23b4a10ec25c0e2c880eff951a3842cf358ac1", + "sha256:28529e08fde6f12eba8677f5a8608500ed33c086f974de68cc65ab218713a59d", + "sha256:2b606353da03edcc71130b52388d25f9a30a126e04caef1fd637e31683033abd", + "sha256:30ca7c3b94708a9d7ae76ff281b2f47d8eaf2579cd05971b5dc681db8caac6e1", + "sha256:333cf6cf8e65f6a1e06e9eb3e643a0c515bb850d470902274239fea02033e9a8", + "sha256:3455522392fb15ff549d92fbf4b73b559d5e43dc522588f7eb3e54c3f38beee7", + "sha256:362f641f9071e5f3ee6f8e7d37d5ed0d95aae656adf4ef578313ee585b585959", + "sha256:3bcd391d083f636c06a68715e69467963d1f9600f85ef556ea82e9ef25f043f7", + "sha256:3dffb610a30d643983aeb185ce134f97f290f8935f0abccdd32c77bed9388b42", + "sha256:3fe407bf93533a6fa82dece0e74dbcaaf5d684e5a51862887f9eaebe6372cd79", + "sha256:413251f6fcf552a33c981c4709a6bba37b12710982fec8e558ae944bfb2abd38", + "sha256:438cd072f75bb6612f2aca29f8bd7cdf6e35e8f160bc312e49fbecab77c99e3a", + "sha256:4470c73c12cd9109db8277287d11f9dd98f77fc54155fc71a7738a83ffcc8ea8", + "sha256:45c3b868724137f713a38376fef8120c166d1eadd50da1855c112fe97954aed8", + "sha256:486f7aabfa292719a2753c016cc3a8f8172965cabb3ea2e7f7436c7f5a22a151", + "sha256:4f05e9727ce409358baa615dbeb9b969db94324a79b5a5cea45d39bdb01d82e6", + "sha256:50aed5155f819873d23520919e16703fc8925e509abbb1a1491b0087d1cd969e", + "sha256:50edbcad60d8f0e3eccc68da67f37268b5144ecc34d59f27a02f9611c1d4eec7", + "sha256:54ca74df1be3c7ca1cf7f4c971c79c2daf48d9aa65dea1a662ae18926f5bc8ce", + "sha256:578a4b875af3e0daaf1ac6fa983d93e0bbfec3ead753b6d6f33d467100cdc67b", + "sha256:597a079284b7ee65ee102bc3a6ea226a37d2b96d0418cc9047490f231dc09fe8", + "sha256:59bb3c54aa420521dc4ce3cc2c3fe2ad82adf7b09403fa1f48ae45c0cbde6628", + "sha256:5c6a5b8c7926ba5d8545c7dd22961a107526562da31a7a32fa2456baf040939f", + "sha256:64f6c17757251e2b8d885d728b6433d9d970573586a78b78ba8929b0f41d045a", + "sha256:679abe5d3858b33c2cf74faec299fda60ea9de62916e8b67e625d65bf069a3b7", + "sha256:741a46d58677d8c733175d7e5aa618d277cd9d880301a380fd296975a9cdd7bc", + "sha256:7789050d9e5d0c309c706953e5e8876e38662d57d45f936902e176d19f1c58ab", + "sha256:77abf6665ae54000b98b3c742bc6ea1d1fb31c394bcabf8b5d2c1ac3ebfe7f3b", + "sha256:79019094f87c9fb44f8d769e41dbb664d6e8fcfd62f665ccce36762deaa0e911", + "sha256:7b06b7843929e41a94ea09eb1ce3927865387e3e23ebe108e0d0d09b08d25be9", + "sha256:7e338c0523d024fad378b376a79faff37fafb3c001872a618cde1d322400a572", + "sha256:7ea7ffc6d6d6f8a11e6f40091a1040995cdff02cfc9ba4c2f30a516cb2633554", + "sha256:8105fd8a890df77b76dd3054cddf01a879fc13e8af576805d667e0fa0224c35d", + "sha256:84afcdea18eda514c25bc68b9af2a2b1adea7c08899175a51fe7c4fb6d551257", + "sha256:9294bbb581f92770e6ed5c19559e1e99255e4ca604a22c5c6397b2f9dd3ee42c", + "sha256:93429602396f3383a797a2a70e5f1de5df8e35535d7806c9f91df06f297e109b", + "sha256:9627cc1a10c8c409b5822a92d57a77f383b554463d1884008e051c32ab1b3742", + "sha256:998f3bd3cfc95e9424a6acd7840cbdd39e45bc09ef87533c006f94ac47296090", + "sha256:9c72109213eb9d3874f7ac8c0c5fa90e072d678e117d9061c06e30c85b4cf0e6", + "sha256:9fc1500fd2a952c5c8e3b29aaf7e3cc6e27e9cfc0a8819b3bce48cc1b849e4cc", + "sha256:a3f00003de6eba42d6e94fabb4125600d6e484846dbf90ea8e48a800430cc142", + "sha256:a45d85cf20b5e0d0aa5a8dca27cce8eddef3292bc29d72dcad1641f4ed50aa16", + "sha256:a7d8d14fe962153fc681f6366bdec33d4356f98a3e3567782aac1b6e0e40109a", + "sha256:a8fa23fe62c436ccf23ff930149c047f060c7126eae3ccea005f0483f27b2e28", + "sha256:aa6658732517ddabe22c9036479eabce6036655ba87a0224c612e1ae6af2087e", + "sha256:aafc8ee9b742ce75044ae9a4d3e60e3d918d15a4c2e08a6c3c3e38fa59b92d94", + "sha256:ab5a5a0c7a7991d90446a198689c0535be89bbd6b410a1f9a66688f0880ec026", + "sha256:acd48d5b80ee80f9432a165c0ac8cbf9253eaddb6113269a5e18699b33958dbb", + "sha256:ad7593bb24b2ab09e65e8a1d385606f0f47c65b5a2ae6c551db67d6653e78c28", + "sha256:baa42524a82f75303f714108fea528ccacf0386af429b69fff141ffef1c534f9", + "sha256:bdfcf6443637c148c4e1a20c48c566aa694fa5e288d34b20fcdc58507882fed3", + "sha256:be7443669ae9c016b71f402e43208e13ddf00912f47f623ee5994e12fc7d4b3f", + "sha256:c02a30b904282777d872266b87b20ed8cc0d1501855e27f831320f471d54d983", + "sha256:c1277cd707c465cd09572a774559a3cc7c7a28802eb3a2a9472588f062097205", + "sha256:c30a0eafc89d28e7f959281b58198a9fa5e99405f716c0289b7892ca345fe45f", + "sha256:c5ce2ce7c997e1971b7184ee37deb6ea9922ef5163c6ee5aa3c274b05f9e12fa", + "sha256:c823bc3971c44ab93e611ab1a46b1eafeae474c0c844aff4b7474287b75fe49c", + "sha256:ce0cdc074d540265bfeb31336e678b4e37316849d13b308607efa527e981f5c2", + "sha256:d1720b4f14c78a3089562b8875b53e36b51c97c51adc53325a69b79b4b48ebcb", + "sha256:d183cf9c797a5291e8301790ed6d053480ed94070637bfaad914dd38b0981f67", + "sha256:d9010c31cd6fa59438da4e58a7f19e4753f7f264300cd152e7f90d4602449762", + "sha256:d9e5e4a85bdb56d224f412d9c98ae4cbd032cc4f3161818f692cd81766eee65a", + "sha256:da1dee8948d2137bb51fbb8a53cce6b1bcc86003c6b42565f008438b806cccd8", + "sha256:df9270660711670e68803107d55c2b5949c2e0f2e4896da176e1ecfc068b974a", + "sha256:e00e3505cd80440f6c98c6d69269dcc2a119f86ad0a9fd70bccc59504bebd68a", + "sha256:e48d5021a84d341bcaf95c8460b152cfbad770d28e5fe14a768988c461b821bc", + "sha256:e7f8b04d83483577fd9200461b057c9f14ced334dcb053090cea1da9c8321a91", + "sha256:edfe3341033a6b53a5c522c802deb2079eee5cbfbb0af032a55064bd65c73a23", + "sha256:ef9c33cc5cbca35808f6c74be11eb7f5f6b14d2311be84a15b594bd3e58b5527", + "sha256:f2d4324a98062be0525d16f768a03e0bbb3b9fe301ceee99611dc9a7953124e6", + "sha256:f3935f82f6f4a3820270842e90456ebad3af15810cf65932bd24da4463bc0a4c", + "sha256:f614ab0c76397661b90b6851a030004dac502e48260ea10f2441abd2207fbcc7", + "sha256:f7db54c7914cc99d901d93a34704833568d86c20925b2762f9fa779f9cd2e70f", + "sha256:fbc6264158392bad9df19537e872d476f7c57adf718944cc1e4495cbabf38e2a", + "sha256:fe2fb38c2ed905a2582948e2de560675e9dfbee94c6d5ccdb1301c6d0a5bf092", + "sha256:ffe595f10566f8276b76dc3a11ae4bb7eba1aac8ddd75811736a15b0d5311414" + ], + "markers": "python_version >= '3.8'", + "version": "==3.10.10" }, "aiosignal": { "hashes": [ @@ -208,20 +208,20 @@ }, "boto3": { "hashes": [ - "sha256:2244044cdfa8ac345d7400536dc15a4824835e7ec5c55bc267e118af66bb27db", - "sha256:7bbb1ee649e09e956952285782cfdebd7e81fc78384f48dfab3d66c6eaf3f63f" + "sha256:b660c649a27a6b47a34f6f858f5bd7c3b0a798a16dec8dda7cbebeee80fd1f60", + "sha256:ddecb27f5699ca9f97711c52b6c0652c2e63bf6c2bfbc13b819b4f523b4d30ff" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.29" + "version": "==1.35.49" }, "botocore": { "hashes": [ - "sha256:4cee814875bc78656aef4011d3d6b2231e96f53ea3661ee428201afb579d5c31", - "sha256:f7bfa910cf2cbcc8c2307c1cf7b93495d614c2d699883417893e0a337fe4eb63" + "sha256:07d0c1325fdbfa49a4a054413dbdeab0a6030449b2aa66099241af2dac48afd8", + "sha256:aed4d3643afd702920792b68fbe712a8c3847993820d1048cd238a6469354da1" ], "markers": "python_version >= '3.8'", - "version": "==1.35.31" + "version": "==1.35.49" }, "celery": { "hashes": [ @@ -315,99 +315,114 @@ }, "charset-normalizer": { "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", + "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", + "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", + "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", + "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", + "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", + "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", + "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", + "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", + "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", + "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", + "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", + "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", + "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", + "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", + "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", + "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", + "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", + "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", + "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", + "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", + "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", + "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", + "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", + "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", + "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", + "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", + "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", + "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", + "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", + "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", + "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", + "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", + "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", + "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", + "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", + "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", + "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", + "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", + "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", + "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", + "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", + "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", + "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", + "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", + "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", + "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", + "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", + "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", + "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", + "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", + "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", + "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", + "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", + "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", + "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", + "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", + "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", + "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", + "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", + "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", + "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", + "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", + "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", + "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", + "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", + "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", + "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", + "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", + "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", + "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", + "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", + "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", + "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", + "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", + "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", + "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", + "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", + "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", + "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", + "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", + "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", + "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", + "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", + "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", + "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", + "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", + "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", + "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", + "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", + "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" + "version": "==3.4.0" }, "click": { "hashes": [ @@ -442,54 +457,54 @@ }, "cryptography": { "hashes": [ - "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494", - "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806", - "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d", - "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062", - "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2", - "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4", - "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1", - "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85", - "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84", - "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042", - "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d", - "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962", - "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2", - "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa", - "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d", - "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365", - "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96", - "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47", - "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d", - "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d", - "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c", - "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb", - "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277", - "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172", - "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034", - "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", - "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289" + "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", + "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", + "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa", + "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83", + "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff", + "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", + "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", + "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664", + "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08", + "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", + "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", + "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", + "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", + "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", + "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", + "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", + "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3", + "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", + "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", + "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", + "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c", + "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", + "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", + "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", + "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7", + "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", + "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7" ], "markers": "python_version >= '3.7'", - "version": "==43.0.1" + "version": "==43.0.3" }, "django": { "hashes": [ - "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2", - "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f" + "sha256:bd7376f90c99f96b643722eee676498706c9fd7dc759f55ebfaf2c08ebcdf4f0", + "sha256:f11aa87ad8d5617171e3f77e1d5d16f004b79a2cf5d2e1d2b97a6a1f8e9ba5ed" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==5.1.1" + "version": "==5.1.2" }, "django-cors-headers": { "hashes": [ - "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6", - "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2" + "sha256:28c1ded847aa70208798de3e42422a782f427b8b720e8d7319d34b654b5978e6", + "sha256:6c01a85cf1ec779a7bde621db853aa3ce5c065a5ba8e27df7a9f9e8dac310f4f" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==4.4.0" + "markers": "python_version >= '3.9'", + "version": "==4.5.0" }, "django-environ": { "hashes": [ @@ -544,11 +559,11 @@ }, "django-rest-passwordreset": { "hashes": [ - "sha256:701f26804b6317f8ddbb1f9b2159176b65d8281e980b90db32dbd60407fb518a", - "sha256:94ea2aa717d2a6c50898541a1177dca4ae8b74bb67460aae3fd6ae02c992ce52" + "sha256:52e0a5ce0729102a9f691153ce5b554dc63660d9375932f7bc59e7ec242f2575", + "sha256:b81bd309bfdc3f01355e70c8e9c09f188f84590ad9ed3fd0bdcb075a76193006" ], "index": "pypi", - "version": "==1.4.1" + "version": "==1.4.2" }, "django-simple-history": { "hashes": [ @@ -586,11 +601,11 @@ }, "dnspython": { "hashes": [ - "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50", - "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc" + "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", + "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1" ], - "markers": "python_version >= '3.8'", - "version": "==2.6.1" + "markers": "python_version >= '3.9'", + "version": "==2.7.0" }, "drf-nested-routers": { "hashes": [ @@ -635,86 +650,101 @@ }, "frozenlist": { "hashes": [ - "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", - "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98", - "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad", - "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5", - "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae", - "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e", - "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a", - "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701", - "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d", - "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6", - "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6", - "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106", - "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75", - "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868", - "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a", - "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0", - "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1", - "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826", - "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec", - "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6", - "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950", - "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19", - "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0", - "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8", - "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a", - "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09", - "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86", - "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c", - "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5", - "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b", - "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b", - "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d", - "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0", - "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea", - "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776", - "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a", - "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897", - "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7", - "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09", - "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9", - "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe", - "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd", - "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742", - "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09", - "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0", - "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932", - "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1", - "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a", - "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49", - "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d", - "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7", - "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480", - "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89", - "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e", - "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", - "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82", - "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb", - "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068", - "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8", - "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b", - "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb", - "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2", - "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11", - "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b", - "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc", - "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0", - "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497", - "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17", - "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0", - "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2", - "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439", - "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5", - "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac", - "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825", - "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887", - "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced", - "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74" - ], - "markers": "python_version >= '3.8'", - "version": "==1.4.1" + "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e", + "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf", + "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6", + "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a", + "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d", + "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f", + "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", + "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", + "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", + "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", + "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec", + "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2", + "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c", + "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336", + "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4", + "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d", + "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b", + "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c", + "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10", + "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08", + "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942", + "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", + "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f", + "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10", + "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5", + "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", + "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", + "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", + "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d", + "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923", + "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", + "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", + "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17", + "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0", + "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", + "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", + "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c", + "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a", + "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0", + "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", + "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab", + "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", + "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3", + "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", + "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", + "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604", + "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", + "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5", + "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", + "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", + "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", + "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", + "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d", + "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", + "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3", + "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", + "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", + "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9", + "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf", + "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76", + "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba", + "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171", + "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb", + "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439", + "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", + "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972", + "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d", + "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869", + "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9", + "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411", + "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723", + "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2", + "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b", + "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99", + "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e", + "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", + "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3", + "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb", + "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", + "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", + "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca", + "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45", + "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", + "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f", + "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5", + "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307", + "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e", + "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2", + "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778", + "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a", + "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30", + "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a" + ], + "markers": "python_version >= '3.8'", + "version": "==1.5.0" }, "gunicorn": { "hashes": [ @@ -874,20 +904,11 @@ }, "jsonschema-specifications": { "hashes": [ - "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc", - "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c" + "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", + "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf" ], - "markers": "python_version >= '3.8'", - "version": "==2023.12.1" - }, - "jwcrypto": { - "hashes": [ - "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789", - "sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.5.6" + "markers": "python_version >= '3.9'", + "version": "==2024.10.1" }, "kombu": { "hashes": [ @@ -1005,35 +1026,39 @@ }, "newrelic": { "hashes": [ - "sha256:002e21527c77c0c9640402c152d40a114b4cc821e7de93cf445fffaef160f1aa", - "sha256:01e68cf6826a3d456aaa0a4c88a7b864403428369b855c3d9c5c27958ef48adb", - "sha256:03a5068d68f22d80797a048a4018d673b8cdd646bc5f9fb63328b53b08bc6de7", - "sha256:0d1f0c1c54a301ee8f7c4372a8905a18cd36d9a2f9b6550898dd7bac147480d3", - "sha256:14e675e0a73e52fde94df9de89201de945cc3a2a046b4fdfe5ba1717b15cad78", - "sha256:1f4cd5ca11f08badd4b1cdd746053cfb30a09d5d9b9c1f5d911718d2870b4493", - "sha256:27d2f34bf714ef9d7ff8a68265a2094b87a4bdc7b1bbbd0a1421cf5cf8f33311", - "sha256:34b60d16d6e8fbc3e65a7d5171718999ecf7bc369cf8baae1bee3c6317972e18", - "sha256:4d09af04f86d40c534d3753bdc1e45e9ca76ce85cea7ea87994c77b3d0677381", - "sha256:548b538c3e95b589a30565bff668285ca74bb64069eb1d6f643bde9768944f53", - "sha256:6257413c9e261e8256be5cadb488945dbb3830dcc6091805fa3a5c70992a03a6", - "sha256:78bc57206c7747f7096ed081d828719def7c0952ea7834c7769d383bd7ba0aa6", - "sha256:8716867245ebe97656017e7a6ef17ebccb730e59062531e3e7b9ce9ffc7b4e4b", - "sha256:94c94a0a05e2995dff812f4fb85113227bcc5a24635539031842af9c1ddc4368", - "sha256:a8a16dfac53914dd0b930a2c087df701585d4f372b2c138466418e78d067b50f", - "sha256:a8c1b480bbe5c3e2e156f8de86182aa207430dbb32e0e5dc523ba8c3731328fc", - "sha256:b1352b6d357e82899ff102acb6971fb9c2cebe70c783081f11c3e53fddfedc4e", - "sha256:b60407fdb9798eee54488130bf87dfe542c3f04475c0c6b8c14e84274db5b1eb", - "sha256:bd3c73bbfac0a48402583aada21bf026161df8b73c6552cb8654f4a93f409860", - "sha256:c633972d88d89a2a17471b834961a889709f4970016e9641e3ddc0234669aadf", - "sha256:d6e09a66088431356c6c1a75bd1cdac2e425d547b47026138b254ac51d5df23d", - "sha256:d8bfbbb50ccc39a51a3449cdfb61970d7e61be0eb93336e3725857b7c1d17ff7", - "sha256:d90d41d78bd72d7fab7ed1cf34bf3dc519ab2d8c6820554061b8708eb7951374", - "sha256:f1aac4a5fe1d0cbe2bb9e2c52152604fb872a6bce28e129febd29d1d307df1f4", - "sha256:f446bf0943220e114e861bbc96761733e0877684ee860cf61755abe2d9805367" + "sha256:00df1aa613294cb592a52157f789e75166dbf439cfa9e6cf59f6cf4a265dada9", + "sha256:03ab987eae0452aeb5aed8571c100d1735613a3a227387f99fe54ed38f1ae0e9", + "sha256:23400846dad2283693eade90b6d3c3462301a4b7735c7f76009b1fa445660aeb", + "sha256:32bd34e4cd73c2435472c0b67869fd2db914d6c99d3e1e404e09affe61a8551e", + "sha256:3415b1c7cab5e586e72cca467dd80cd0507f23a3139c02911cf75892fdbb48a6", + "sha256:3521d646c0032db53b7320fe6b6859eebd863f1b47d7c7dd480073727091e50e", + "sha256:36a2218c9e79897d9b5671cdeac30c467d7fbac10cda4f2d79062f2bd0fcaed8", + "sha256:3bee0b9ce1eccf6ac63e51113781743853b1b84c98ae48ed17d0410c352ccb4d", + "sha256:3cd5aeade6462519328fc42f4e98948a45571f3d22360a0559e19a6525c723a6", + "sha256:3d9c8297ba158ce4570fc48cfea7bdf3678b2054baaf0cad4debcbca33c2af3a", + "sha256:3f15a940b6794b4008ab983e7ac3b4d179efe609e040ee96ed5744723fc580c8", + "sha256:501cc575b3fd702a21542a0f5dac59b83f47e2806f5b7c0f4e4510b5474ed77f", + "sha256:60d01303807228718c4099d8550f72d21ee8b61a33555d8974800f6868f2144a", + "sha256:62d521a5d7269c8a5c5838c4ca3b757ef63a13257302c901223c75510cc6f9f5", + "sha256:69aa68cae47c595bdeb95f275d78693ec27a9fd9353bf81257e21f8607134db6", + "sha256:7b449546ebb89feaadfd36fda7735ce06023fc90979b838e244f98369aba5ccb", + "sha256:7f021eac4c2e3b14eab90c608d8bd25b4e3c6b0b0d40796ec1c1260cc47b5e83", + "sha256:a15df23effd09bb1d1f5c38866b75cc5f380b6aa953efbca9e95e79b72744db4", + "sha256:a6d4094d19db924c51ca35da603344907bcbca030822f7a78d5d9c6ad361d419", + "sha256:a6ff022c7556b61b067e8e6b729fe60f437a8356f319ae3b8342858792f3930d", + "sha256:b4220b97669d214e75d2039fea9e0505fde5bc450832210abbc76b9a635785ed", + "sha256:bc693e0db87ab4cf6623847c3949debbcc991554edfb4dd8c02c136e0770b367", + "sha256:c57e79d37ed87e2790c5e66253f9a5d91ed8cc218f160d5a4d062fc759791a78", + "sha256:c6aa9cf936b16d13b65c1b7aa6c722a76a0702469f91bcfc3c5b39f3293181eb", + "sha256:e5809b4111ef3b1d0b5fa66ad06a81de512842370707863d44888c9439f16c4c", + "sha256:e6f822e6a43151af13a748fb2de6ff298aeb6eee03bf6512afba6aaa79211172", + "sha256:ef5d27001d3b5ca53f19d150c60b570c1b0c774d082ab9bf8349f4430ed85b48", + "sha256:f6333aa7051544ddc7f8a85f344bc3f401ddd8635540878da34de7bfd91f5d95", + "sha256:fc3d34db12133b481636384663f45b9ccd7f0f41554a59c15ec37aeb4f77227d" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==10.0.0" + "version": "==10.2.0" }, "packaging": { "hashes": [ @@ -1045,90 +1070,85 @@ }, "pillow": { "hashes": [ - "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", - "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", - "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df", - "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", - "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", - "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d", - "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd", - "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", - "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908", - "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", - "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", - "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", - "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b", - "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", - "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a", - "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e", - "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", - "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", - "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b", - "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", - "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", - "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab", - "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", - "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", - "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", - "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", - "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", - "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", - "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", - "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", - "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", - "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", - "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", - "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0", - "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", - "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", - "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", - "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef", - "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680", - "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b", - "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", - "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", - "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", - "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", - "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8", - "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", - "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736", - "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", - "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126", - "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd", - "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5", - "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b", - "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", - "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b", - "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", - "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", - "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2", - "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c", - "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", - "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", - "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", - "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", - "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", - "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b", - "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", - "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3", - "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84", - "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1", - "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", - "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", - "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", - "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", - "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", - "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e", - "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", - "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", - "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", - "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27", - "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", - "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1" + "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", + "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5", + "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", + "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", + "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", + "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2", + "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9", + "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f", + "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc", + "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", + "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d", + "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2", + "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316", + "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a", + "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", + "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd", + "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba", + "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", + "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273", + "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa", + "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a", + "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", + "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", + "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae", + "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291", + "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97", + "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06", + "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", + "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b", + "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b", + "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", + "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", + "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947", + "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb", + "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", + "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5", + "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f", + "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", + "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944", + "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830", + "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", + "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", + "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", + "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", + "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7", + "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", + "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", + "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9", + "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", + "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4", + "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", + "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd", + "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50", + "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c", + "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086", + "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba", + "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306", + "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", + "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e", + "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488", + "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", + "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", + "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3", + "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9", + "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", + "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", + "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790", + "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734", + "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916", + "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1", + "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", + "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", + "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", + "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2", + "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==10.4.0" + "markers": "python_version >= '3.9'", + "version": "==11.0.0" }, "ply": { "hashes": [ @@ -1145,22 +1165,126 @@ "markers": "python_full_version >= '3.7.0'", "version": "==3.0.48" }, + "propcache": { + "hashes": [ + "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9", + "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763", + "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325", + "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb", + "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b", + "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09", + "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957", + "sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68", + "sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f", + "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798", + "sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418", + "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6", + "sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162", + "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f", + "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036", + "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8", + "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2", + "sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110", + "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23", + "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8", + "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638", + "sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a", + "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44", + "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2", + "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2", + "sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850", + "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136", + "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b", + "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887", + "sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89", + "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87", + "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348", + "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4", + "sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861", + "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e", + "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c", + "sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b", + "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb", + "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1", + "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de", + "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354", + "sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563", + "sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5", + "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf", + "sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9", + "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12", + "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4", + "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5", + "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71", + "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9", + "sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed", + "sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336", + "sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90", + "sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063", + "sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad", + "sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6", + "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8", + "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e", + "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2", + "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7", + "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d", + "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d", + "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df", + "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b", + "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178", + "sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2", + "sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630", + "sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48", + "sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61", + "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89", + "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb", + "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3", + "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6", + "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562", + "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b", + "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58", + "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db", + "sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99", + "sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37", + "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83", + "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a", + "sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d", + "sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04", + "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70", + "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544", + "sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394", + "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea", + "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7", + "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1", + "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793", + "sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577", + "sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7", + "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57", + "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d", + "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032", + "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d", + "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016", + "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504" + ], + "markers": "python_version >= '3.8'", + "version": "==0.2.0" + }, "psycopg": { "extras": [ "c" ], "hashes": [ - "sha256:8bad2e497ce22d556dac1464738cb948f8d6bab450d965cf1d8a8effd52412e0", - "sha256:babf565d459d8f72fb65da5e211dd0b58a52c51e4e1fa9cadecff42d6b7619b2" + "sha256:644d3973fe26908c73d4be746074f6e5224b03c1101d302d9a53bf565ad64907", + "sha256:a5764f67c27bec8bfac85764d23c534af2c27b893550377e37ce59c12aac47a2" ], "markers": "python_version >= '3.8'", - "version": "==3.2.2" + "version": "==3.2.3" }, "psycopg-c": { "hashes": [ - "sha256:de8cac75bc6640ef0f54ad9187b81e07c430206a83c566b73d4cca41ecccb7c8" + "sha256:06ae7db8eaec1a3845960fa7f997f4ccdb1a7a7ab8dc593a680bcc74e1359671" ], - "version": "==3.2.2" + "version": "==3.2.3" }, "py-vapid": { "hashes": [ @@ -1176,45 +1300,6 @@ "markers": "python_version >= '3.8'", "version": "==2.22" }, - "pycryptodome": { - "hashes": [ - "sha256:06d6de87c19f967f03b4cf9b34e538ef46e99a337e9a61a77dbe44b2cbcf0690", - "sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7", - "sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4", - "sha256:2a1250b7ea809f752b68e3e6f3fd946b5939a52eaeea18c73bdab53e9ba3c2dd", - "sha256:2ab6ab0cb755154ad14e507d1df72de9897e99fd2d4922851a276ccc14f4f1a5", - "sha256:3427d9e5310af6680678f4cce149f54e0bb4af60101c7f2c16fdf878b39ccccc", - "sha256:3cd3ef3aee1079ae44afaeee13393cf68b1058f70576b11439483e34f93cf818", - "sha256:405002eafad114a2f9a930f5db65feef7b53c4784495dd8758069b89baf68eab", - "sha256:417a276aaa9cb3be91f9014e9d18d10e840a7a9b9a9be64a42f553c5b50b4d1d", - "sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a", - "sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25", - "sha256:5601c934c498cd267640b57569e73793cb9a83506f7c73a8ec57a516f5b0b091", - "sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea", - "sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a", - "sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c", - "sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72", - "sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9", - "sha256:a60fedd2b37b4cb11ccb5d0399efe26db9e0dd149016c1cc6c8161974ceac2d6", - "sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044", - "sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04", - "sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c", - "sha256:acf6e43fa75aca2d33e93409f2dafe386fe051818ee79ee8a3e21de9caa2ac9e", - "sha256:baee115a9ba6c5d2709a1e88ffe62b73ecc044852a925dcb67713a288c4ec70f", - "sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b", - "sha256:d29daa681517f4bc318cd8a23af87e1f2a7bad2fe361e8aa29c77d652a065de4", - "sha256:d5954acfe9e00bc83ed9f5cb082ed22c592fbbef86dc48b907238be64ead5c33", - "sha256:ec0bb1188c1d13426039af8ffcb4dbe3aad1d7680c35a62d8eaf2a529b5d3d4f", - "sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e", - "sha256:f0e6d631bae3f231d3634f91ae4da7a960f7ff87f2865b2d2b831af1dfb04e9a", - "sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2", - "sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3", - "sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128" - ], - "index": "pypi", - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.20.0" - }, "pydantic": { "extras": [ "email" @@ -1282,7 +1367,7 @@ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "python-fsutil": { @@ -1311,10 +1396,10 @@ }, "pywebpush": { "hashes": [ - "sha256:03ccc3e975b60374b7634c495595616be523bf2c7da0d976e84fda9ac8c63301" + "sha256:72e3586aec89a06373c0585e6f97c6fd0b81519e41f32896a3639909ecf45db0" ], "index": "pypi", - "version": "==2.0.0" + "version": "==2.0.1" }, "pyyaml": { "hashes": [ @@ -1523,20 +1608,20 @@ }, "s3transfer": { "hashes": [ - "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", - "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69" + "sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d", + "sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c" ], "markers": "python_version >= '3.8'", - "version": "==0.10.2" + "version": "==0.10.3" }, "sentry-sdk": { "hashes": [ - "sha256:1e0e2eaf6dad918c7d1e0edac868a7bf20017b177f242cefe2a6bcd47955961d", - "sha256:b8bc3dc51d06590df1291b7519b85c75e2ced4f28d9ea655b6d54033503b5bf4" + "sha256:625955884b862cc58748920f9e21efdfb8e0d4f98cca4ab0d3918576d5b606ad", + "sha256:dd0a05352b78ffeacced73a94e86f38b32e2eae15fff5f30ca5abb568a72eacf" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==2.14.0" + "version": "==2.17.0" }, "setuptools": { "hashes": [ @@ -1551,7 +1636,7 @@ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "sqlparse": { @@ -1587,26 +1672,26 @@ }, "types-redis": { "hashes": [ - "sha256:0e7537e5c085fe96b7d468d5edae0cf667b4ba4b62c6e4a5dfc340bd3b868c23", - "sha256:4bab1a378dbf23c2c95c370dfdb89a8f033957c4fd1a53fee71b529c182fe008" + "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e", + "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed" ], "markers": "python_version >= '3.8'", - "version": "==4.6.0.20240903" + "version": "==4.6.0.20241004" }, "types-setuptools": { "hashes": [ - "sha256:06f78307e68d1bbde6938072c57b81cf8a99bc84bd6dc7e4c5014730b097dc0c", - "sha256:12f12a165e7ed383f31def705e5c0fa1c26215dd466b0af34bd042f7d5331f55" + "sha256:2949913a518d5285ce00a3b7d88961c80a6e72ffb8f3da0a3f5650ea533bd45e", + "sha256:6721ac0f1a620321e2ccd87a9a747c4a383dc381f78d894ce37f2455b45fcf1c" ], "markers": "python_version >= '3.8'", - "version": "==75.1.0.20240917" + "version": "==75.2.0.20241025" }, "typing-extensions": { "hashes": [ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], - "markers": "python_version < '3.13'", + "markers": "python_version >= '3.8'", "version": "==4.12.2" }, "tzdata": { @@ -1665,101 +1750,91 @@ }, "yarl": { "hashes": [ - "sha256:08d7148ff11cb8e886d86dadbfd2e466a76d5dd38c7ea8ebd9b0e07946e76e4b", - "sha256:098b870c18f1341786f290b4d699504e18f1cd050ed179af8123fd8232513424", - "sha256:11b3ca8b42a024513adce810385fcabdd682772411d95bbbda3b9ed1a4257644", - "sha256:1891d69a6ba16e89473909665cd355d783a8a31bc84720902c5911dbb6373465", - "sha256:1bbb418f46c7f7355084833051701b2301092e4611d9e392360c3ba2e3e69f88", - "sha256:1d0828e17fa701b557c6eaed5edbd9098eb62d8838344486248489ff233998b8", - "sha256:1d8e3ca29f643dd121f264a7c89f329f0fcb2e4461833f02de6e39fef80f89da", - "sha256:1fa56f34b2236f5192cb5fceba7bbb09620e5337e0b6dfe2ea0ddbd19dd5b154", - "sha256:216a6785f296169ed52cd7dcdc2612f82c20f8c9634bf7446327f50398732a51", - "sha256:22b739f99c7e4787922903f27a892744189482125cc7b95b747f04dd5c83aa9f", - "sha256:2430cf996113abe5aee387d39ee19529327205cda975d2b82c0e7e96e5fdabdc", - "sha256:269c201bbc01d2cbba5b86997a1e0f73ba5e2f471cfa6e226bcaa7fd664b598d", - "sha256:298c1eecfd3257aa16c0cb0bdffb54411e3e831351cd69e6b0739be16b1bdaa8", - "sha256:2a93a4557f7fc74a38ca5a404abb443a242217b91cd0c4840b1ebedaad8919d4", - "sha256:2b2442a415a5f4c55ced0fade7b72123210d579f7d950e0b5527fc598866e62c", - "sha256:2db874dd1d22d4c2c657807562411ffdfabec38ce4c5ce48b4c654be552759dc", - "sha256:309c104ecf67626c033845b860d31594a41343766a46fa58c3309c538a1e22b2", - "sha256:31497aefd68036d8e31bfbacef915826ca2e741dbb97a8d6c7eac66deda3b606", - "sha256:373f16f38721c680316a6a00ae21cc178e3a8ef43c0227f88356a24c5193abd6", - "sha256:396e59b8de7e4d59ff5507fb4322d2329865b909f29a7ed7ca37e63ade7f835c", - "sha256:3bb83a0f12701c0b91112a11148b5217617982e1e466069d0555be9b372f2734", - "sha256:3de86547c820e4f4da4606d1c8ab5765dd633189791f15247706a2eeabc783ae", - "sha256:3fdbf0418489525231723cdb6c79e7738b3cbacbaed2b750cb033e4ea208f220", - "sha256:40c6e73c03a6befb85b72da213638b8aaa80fe4136ec8691560cf98b11b8ae6e", - "sha256:44a4c40a6f84e4d5955b63462a0e2a988f8982fba245cf885ce3be7618f6aa7d", - "sha256:44b07e1690f010c3c01d353b5790ec73b2f59b4eae5b0000593199766b3f7a5c", - "sha256:45d23c4668d4925688e2ea251b53f36a498e9ea860913ce43b52d9605d3d8177", - "sha256:45f209fb4bbfe8630e3d2e2052535ca5b53d4ce2d2026bed4d0637b0416830da", - "sha256:4afdf84610ca44dcffe8b6c22c68f309aff96be55f5ea2fa31c0c225d6b83e23", - "sha256:4feaaa4742517eaceafcbe74595ed335a494c84634d33961214b278126ec1485", - "sha256:576365c9f7469e1f6124d67b001639b77113cfd05e85ce0310f5f318fd02fe85", - "sha256:5820bd4178e6a639b3ef1db8b18500a82ceab6d8b89309e121a6859f56585b05", - "sha256:5989a38ba1281e43e4663931a53fbf356f78a0325251fd6af09dd03b1d676a09", - "sha256:5a9bacedbb99685a75ad033fd4de37129449e69808e50e08034034c0bf063f99", - "sha256:5b66c87da3c6da8f8e8b648878903ca54589038a0b1e08dde2c86d9cd92d4ac9", - "sha256:5c5e32fef09ce101fe14acd0f498232b5710effe13abac14cd95de9c274e689e", - "sha256:658e8449b84b92a4373f99305de042b6bd0d19bf2080c093881e0516557474a5", - "sha256:6a2acde25be0cf9be23a8f6cbd31734536a264723fca860af3ae5e89d771cd71", - "sha256:6a5185ad722ab4dd52d5fb1f30dcc73282eb1ed494906a92d1a228d3f89607b0", - "sha256:6b7f6e699304717fdc265a7e1922561b02a93ceffdaefdc877acaf9b9f3080b8", - "sha256:703b0f584fcf157ef87816a3c0ff868e8c9f3c370009a8b23b56255885528f10", - "sha256:7055bbade838d68af73aea13f8c86588e4bcc00c2235b4b6d6edb0dbd174e246", - "sha256:78f271722423b2d4851cf1f4fa1a1c4833a128d020062721ba35e1a87154a049", - "sha256:7addd26594e588503bdef03908fc207206adac5bd90b6d4bc3e3cf33a829f57d", - "sha256:81bad32c8f8b5897c909bf3468bf601f1b855d12f53b6af0271963ee67fff0d2", - "sha256:82e692fb325013a18a5b73a4fed5a1edaa7c58144dc67ad9ef3d604eccd451ad", - "sha256:84bbcdcf393139f0abc9f642bf03f00cac31010f3034faa03224a9ef0bb74323", - "sha256:86c438ce920e089c8c2388c7dcc8ab30dfe13c09b8af3d306bcabb46a053d6f7", - "sha256:8be8cdfe20787e6a5fcbd010f8066227e2bb9058331a4eccddec6c0db2bb85b2", - "sha256:8c723c91c94a3bc8033dd2696a0f53e5d5f8496186013167bddc3fb5d9df46a3", - "sha256:8ca53632007c69ddcdefe1e8cbc3920dd88825e618153795b57e6ebcc92e752a", - "sha256:8f722f30366474a99745533cc4015b1781ee54b08de73260b2bbe13316079851", - "sha256:942c80a832a79c3707cca46bd12ab8aa58fddb34b1626d42b05aa8f0bcefc206", - "sha256:94a993f976cdcb2dc1b855d8b89b792893220db8862d1a619efa7451817c836b", - "sha256:95c6737f28069153c399d875317f226bbdea939fd48a6349a3b03da6829fb550", - "sha256:9915300fe5a0aa663c01363db37e4ae8e7c15996ebe2c6cce995e7033ff6457f", - "sha256:9a18595e6a2ee0826bf7dfdee823b6ab55c9b70e8f80f8b77c37e694288f5de1", - "sha256:9c8854b9f80693d20cec797d8e48a848c2fb273eb6f2587b57763ccba3f3bd4b", - "sha256:9cec42a20eae8bebf81e9ce23fb0d0c729fc54cf00643eb251ce7c0215ad49fe", - "sha256:9d2e1626be8712333a9f71270366f4a132f476ffbe83b689dd6dc0d114796c74", - "sha256:9d74f3c335cfe9c21ea78988e67f18eb9822f5d31f88b41aec3a1ec5ecd32da5", - "sha256:9fb4134cc6e005b99fa29dbc86f1ea0a298440ab6b07c6b3ee09232a3b48f495", - "sha256:a0ae6637b173d0c40b9c1462e12a7a2000a71a3258fa88756a34c7d38926911c", - "sha256:a31d21089894942f7d9a8df166b495101b7258ff11ae0abec58e32daf8088813", - "sha256:a3442c31c11088e462d44a644a454d48110f0588de830921fd201060ff19612a", - "sha256:ab9524e45ee809a083338a749af3b53cc7efec458c3ad084361c1dbf7aaf82a2", - "sha256:b1481c048fe787f65e34cb06f7d6824376d5d99f1231eae4778bbe5c3831076d", - "sha256:b8c837ab90c455f3ea8e68bee143472ee87828bff19ba19776e16ff961425b57", - "sha256:bbf2c3f04ff50f16404ce70f822cdc59760e5e2d7965905f0e700270feb2bbfc", - "sha256:bbf9c2a589be7414ac4a534d54e4517d03f1cbb142c0041191b729c2fa23f320", - "sha256:bcd5bf4132e6a8d3eb54b8d56885f3d3a38ecd7ecae8426ecf7d9673b270de43", - "sha256:c14c16831b565707149c742d87a6203eb5597f4329278446d5c0ae7a1a43928e", - "sha256:c49f3e379177f4477f929097f7ed4b0622a586b0aa40c07ac8c0f8e40659a1ac", - "sha256:c92b89bffc660f1274779cb6fbb290ec1f90d6dfe14492523a0667f10170de26", - "sha256:cd66152561632ed4b2a9192e7f8e5a1d41e28f58120b4761622e0355f0fe034c", - "sha256:cf1ad338620249f8dd6d4b6a91a69d1f265387df3697ad5dc996305cf6c26fb2", - "sha256:d07b52c8c450f9366c34aa205754355e933922c79135125541daae6cbf31c799", - "sha256:d0d12fe78dcf60efa205e9a63f395b5d343e801cf31e5e1dda0d2c1fb618073d", - "sha256:d4ee1d240b84e2f213565f0ec08caef27a0e657d4c42859809155cf3a29d1735", - "sha256:d959fe96e5c2712c1876d69af0507d98f0b0e8d81bee14cfb3f6737470205419", - "sha256:dcaef817e13eafa547cdfdc5284fe77970b891f731266545aae08d6cce52161e", - "sha256:df4e82e68f43a07735ae70a2d84c0353e58e20add20ec0af611f32cd5ba43fb4", - "sha256:ec8cfe2295f3e5e44c51f57272afbd69414ae629ec7c6b27f5a410efc78b70a0", - "sha256:ec9dd328016d8d25702a24ee274932aebf6be9787ed1c28d021945d264235b3c", - "sha256:ef9b85fa1bc91c4db24407e7c4da93a5822a73dd4513d67b454ca7064e8dc6a3", - "sha256:f3bf60444269345d712838bb11cc4eadaf51ff1a364ae39ce87a5ca8ad3bb2c8", - "sha256:f452cc1436151387d3d50533523291d5f77c6bc7913c116eb985304abdbd9ec9", - "sha256:f7917697bcaa3bc3e83db91aa3a0e448bf5cde43c84b7fc1ae2427d2417c0224", - "sha256:f90575e9fe3aae2c1e686393a9689c724cd00045275407f71771ae5d690ccf38", - "sha256:fb382fd7b4377363cc9f13ba7c819c3c78ed97c36a82f16f3f92f108c787cbbf", - "sha256:fb9f59f3848edf186a76446eb8bcf4c900fe147cb756fbbd730ef43b2e67c6a7", - "sha256:fc2931ac9ce9c61c9968989ec831d3a5e6fcaaff9474e7cfa8de80b7aff5a093" - ], - "markers": "python_version >= '3.8'", - "version": "==1.13.1" + "sha256:019f5d58093402aa8f6661e60fd82a28746ad6d156f6c5336a70a39bd7b162b9", + "sha256:0fd9c227990f609c165f56b46107d0bc34553fe0387818c42c02f77974402c36", + "sha256:1208ca14eed2fda324042adf8d6c0adf4a31522fa95e0929027cd487875f0240", + "sha256:122d8e7986043d0549e9eb23c7fd23be078be4b70c9eb42a20052b3d3149c6f2", + "sha256:147b0fcd0ee33b4b5f6edfea80452d80e419e51b9a3f7a96ce98eaee145c1581", + "sha256:178ccb856e265174a79f59721031060f885aca428983e75c06f78aa24b91d929", + "sha256:1a5cf32539373ff39d97723e39a9283a7277cbf1224f7aef0c56c9598b6486c3", + "sha256:1a5e9d8ce1185723419c487758d81ac2bde693711947032cce600ca7c9cda7d6", + "sha256:1bc22e00edeb068f71967ab99081e9406cd56dbed864fc3a8259442999d71552", + "sha256:1cf936ba67bc6c734f3aa1c01391da74ab7fc046a9f8bbfa230b8393b90cf472", + "sha256:234f3a3032b505b90e65b5bc6652c2329ea7ea8855d8de61e1642b74b4ee65d2", + "sha256:26768342f256e6e3c37533bf9433f5f15f3e59e3c14b2409098291b3efaceacb", + "sha256:27e11db3f1e6a51081a981509f75617b09810529de508a181319193d320bc5c7", + "sha256:2bd6a51010c7284d191b79d3b56e51a87d8e1c03b0902362945f15c3d50ed46b", + "sha256:2f1fe2b2e3ee418862f5ebc0c0083c97f6f6625781382f828f6d4e9b614eba9b", + "sha256:32468f41242d72b87ab793a86d92f885355bcf35b3355aa650bfa846a5c60058", + "sha256:35b4f7842154176523e0a63c9b871168c69b98065d05a4f637fce342a6a2693a", + "sha256:38fec8a2a94c58bd47c9a50a45d321ab2285ad133adefbbadf3012c054b7e656", + "sha256:3a91654adb7643cb21b46f04244c5a315a440dcad63213033826549fa2435f71", + "sha256:3ab3ed42c78275477ea8e917491365e9a9b69bb615cb46169020bd0aa5e2d6d3", + "sha256:3d375a19ba2bfe320b6d873f3fb165313b002cef8b7cc0a368ad8b8a57453837", + "sha256:4199db024b58a8abb2cfcedac7b1292c3ad421684571aeb622a02f242280e8d6", + "sha256:4f32c4cb7386b41936894685f6e093c8dfaf0960124d91fe0ec29fe439e201d0", + "sha256:4ffb7c129707dd76ced0a4a4128ff452cecf0b0e929f2668ea05a371d9e5c104", + "sha256:504e1fe1cc4f170195320eb033d2b0ccf5c6114ce5bf2f617535c01699479bca", + "sha256:542fa8e09a581bcdcbb30607c7224beff3fdfb598c798ccd28a8184ffc18b7eb", + "sha256:5570e6d47bcb03215baf4c9ad7bf7c013e56285d9d35013541f9ac2b372593e7", + "sha256:571f781ae8ac463ce30bacebfaef2c6581543776d5970b2372fbe31d7bf31a07", + "sha256:595ca5e943baed31d56b33b34736461a371c6ea0038d3baec399949dd628560b", + "sha256:5b8e265a0545637492a7e12fd7038370d66c9375a61d88c5567d0e044ded9202", + "sha256:5b9101f528ae0f8f65ac9d64dda2bb0627de8a50344b2f582779f32fda747c1d", + "sha256:5ff96da263740779b0893d02b718293cc03400c3a208fc8d8cd79d9b0993e532", + "sha256:621280719c4c5dad4c1391160a9b88925bb8b0ff6a7d5af3224643024871675f", + "sha256:62c7da0ad93a07da048b500514ca47b759459ec41924143e2ddb5d7e20fd3db5", + "sha256:649bddcedee692ee8a9b7b6e38582cb4062dc4253de9711568e5620d8707c2a3", + "sha256:66ea8311422a7ba1fc79b4c42c2baa10566469fe5a78500d4e7754d6e6db8724", + "sha256:676d96bafc8c2d0039cea0cd3fd44cee7aa88b8185551a2bb93354668e8315c2", + "sha256:707ae579ccb3262dfaef093e202b4c3fb23c3810e8df544b1111bd2401fd7b09", + "sha256:7118bdb5e3ed81acaa2095cba7ec02a0fe74b52a16ab9f9ac8e28e53ee299732", + "sha256:789a3423f28a5fff46fbd04e339863c169ece97c827b44de16e1a7a42bc915d2", + "sha256:7ace71c4b7a0c41f317ae24be62bb61e9d80838d38acb20e70697c625e71f120", + "sha256:7c7c30fb38c300fe8140df30a046a01769105e4cf4282567a29b5cdb635b66c4", + "sha256:7d7aaa8ff95d0840e289423e7dc35696c2b058d635f945bf05b5cd633146b027", + "sha256:7f8713717a09acbfee7c47bfc5777e685539fefdd34fa72faf504c8be2f3df4e", + "sha256:858728086914f3a407aa7979cab743bbda1fe2bdf39ffcd991469a370dd7414d", + "sha256:8791d66d81ee45866a7bb15a517b01a2bcf583a18ebf5d72a84e6064c417e64b", + "sha256:87dd10bc0618991c66cee0cc65fa74a45f4ecb13bceec3c62d78ad2e42b27a16", + "sha256:8994c42f4ca25df5380ddf59f315c518c81df6a68fed5bb0c159c6cb6b92f120", + "sha256:8a0296040e5cddf074c7f5af4a60f3fc42c0237440df7bcf5183be5f6c802ed5", + "sha256:8b37d5ec034e668b22cf0ce1074d6c21fd2a08b90d11b1b73139b750a8b0dd97", + "sha256:8c42998fd1cbeb53cd985bff0e4bc25fbe55fd6eb3a545a724c1012d69d5ec84", + "sha256:8f639e3f5795a6568aa4f7d2ac6057c757dcd187593679f035adbf12b892bb00", + "sha256:921b81b8d78f0e60242fb3db615ea3f368827a76af095d5a69f1c3366db3f596", + "sha256:995d0759004c08abd5d1b81300a91d18c8577c6389300bed1c7c11675105a44d", + "sha256:99a9dcd4b71dd5f5f949737ab3f356cfc058c709b4f49833aeffedc2652dac56", + "sha256:9a91217208306d82357c67daeef5162a41a28c8352dab7e16daa82e3718852a7", + "sha256:a5ace0177520bd4caa99295a9b6fb831d0e9a57d8e0501a22ffaa61b4c024283", + "sha256:a5b6c09b9b4253d6a208b0f4a2f9206e511ec68dce9198e0fbec4f160137aa67", + "sha256:a9394c65ae0ed95679717d391c862dece9afacd8fa311683fc8b4362ce8a410c", + "sha256:aa7943f04f36d6cafc0cf53ea89824ac2c37acbdb4b316a654176ab8ffd0f968", + "sha256:ab2b2ac232110a1fdb0d3ffcd087783edd3d4a6ced432a1bf75caf7b7be70916", + "sha256:ad7a852d1cd0b8d8b37fc9d7f8581152add917a98cfe2ea6e241878795f917ae", + "sha256:b140e532fe0266003c936d017c1ac301e72ee4a3fd51784574c05f53718a55d8", + "sha256:b439cae82034ade094526a8f692b9a2b5ee936452de5e4c5f0f6c48df23f8604", + "sha256:b6f687ced5510a9a2474bbae96a4352e5ace5fa34dc44a217b0537fec1db00b4", + "sha256:b9ca7b9147eb1365c8bab03c003baa1300599575effad765e0b07dd3501ea9af", + "sha256:bdcf667a5dec12a48f669e485d70c54189f0639c2157b538a4cffd24a853624f", + "sha256:cdcffe1dbcb4477d2b4202f63cd972d5baa155ff5a3d9e35801c46a415b7f71a", + "sha256:d1aab176dd55b59f77a63b27cffaca67d29987d91a5b615cbead41331e6b7428", + "sha256:d1b0796168b953bca6600c5f97f5ed407479889a36ad7d17183366260f29a6b9", + "sha256:d3f1cc3d3d4dc574bebc9b387f6875e228ace5748a7c24f49d8f01ac1bc6c31b", + "sha256:d743e3118b2640cef7768ea955378c3536482d95550222f908f392167fe62059", + "sha256:d8643975a0080f361639787415a038bfc32d29208a4bf6b783ab3075a20b1ef3", + "sha256:d9525f03269e64310416dbe6c68d3b23e5d34aaa8f47193a1c45ac568cecbc49", + "sha256:de6c14dd7c7c0badba48157474ea1f03ebee991530ba742d381b28d4f314d6f3", + "sha256:e49e0fd86c295e743fd5be69b8b0712f70a686bc79a16e5268386c2defacaade", + "sha256:e6980a558d8461230c457218bd6c92dfc1d10205548215c2c21d79dc8d0a96f3", + "sha256:e8be3aff14f0120ad049121322b107f8a759be76a6a62138322d4c8a337a9e2c", + "sha256:e9951afe6557c75a71045148890052cb942689ee4c9ec29f5436240e1fcc73b7", + "sha256:ed097b26f18a1f5ff05f661dc36528c5f6735ba4ce8c9645e83b064665131349", + "sha256:f1d1f45e3e8d37c804dca99ab3cf4ab3ed2e7a62cd82542924b14c0a4f46d243", + "sha256:fe8bba2545427418efc1929c5c42852bdb4143eb8d0a46b09de88d1fe99258e7" + ], + "markers": "python_version >= '3.9'", + "version": "==1.16.0" } }, "develop": { @@ -1788,12 +1863,12 @@ }, "boto3": { "hashes": [ - "sha256:2244044cdfa8ac345d7400536dc15a4824835e7ec5c55bc267e118af66bb27db", - "sha256:7bbb1ee649e09e956952285782cfdebd7e81fc78384f48dfab3d66c6eaf3f63f" + "sha256:b660c649a27a6b47a34f6f858f5bd7c3b0a798a16dec8dda7cbebeee80fd1f60", + "sha256:ddecb27f5699ca9f97711c52b6c0652c2e63bf6c2bfbc13b819b4f523b4d30ff" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.29" + "version": "==1.35.49" }, "boto3-stubs": { "extras": [ @@ -1801,27 +1876,27 @@ "s3" ], "hashes": [ - "sha256:048e664389c3fb53e8ab0f810eb280ba02c2f8213a63dc5d3da075ffd54b4504", - "sha256:6e5f082f7cd028bdf3bfc57c9db3b784e0f6ec2232b10482859a919d6cd6bfc9" + "sha256:2a2e08ba2383df6f478127f9754a02a590131249b40c59d7c6ca9fce76906785", + "sha256:daad87dcff906f7c09dde4ef3c252e2c47b6e1e8e669f5a8311658ac0d1182c0" ], "markers": "python_version >= '3.8'", - "version": "==1.35.29" + "version": "==1.35.49" }, "botocore": { "hashes": [ - "sha256:4cee814875bc78656aef4011d3d6b2231e96f53ea3661ee428201afb579d5c31", - "sha256:f7bfa910cf2cbcc8c2307c1cf7b93495d614c2d699883417893e0a337fe4eb63" + "sha256:07d0c1325fdbfa49a4a054413dbdeab0a6030449b2aa66099241af2dac48afd8", + "sha256:aed4d3643afd702920792b68fbe712a8c3847993820d1048cd238a6469354da1" ], "markers": "python_version >= '3.8'", - "version": "==1.35.31" + "version": "==1.35.49" }, "botocore-stubs": { "hashes": [ - "sha256:b26f79253d8f2460aa8f2d49ae933c3f352fe38be72ea033651c15cd47a822a9", - "sha256:b7289c27b759dad40759c1421519cbd7ec65b79f7aa8be5dfc76004a61aef42b" + "sha256:367ce067e003de7e9b76320f551ba4fc8369a4b7ef10210f6071d3593fea2605", + "sha256:c5006e31d77e290eca215e6a71292ea7b029b54900310ed0f87da8e844f1db38" ], "markers": "python_version >= '3.8'", - "version": "==1.35.31" + "version": "==1.35.49" }, "certifi": { "hashes": [ @@ -1841,207 +1916,216 @@ }, "charset-normalizer": { "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", + "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", + "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", + "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", + "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", + "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", + "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", + "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", + "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", + "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", + "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", + "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", + "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", + "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", + "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", + "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", + "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", + "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", + "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", + "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", + "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", + "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", + "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", + "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", + "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", + "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", + "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", + "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", + "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", + "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", + "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", + "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", + "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", + "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", + "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", + "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", + "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", + "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", + "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", + "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", + "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", + "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", + "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", + "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", + "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", + "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", + "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", + "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", + "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", + "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", + "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", + "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", + "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", + "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", + "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", + "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", + "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", + "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", + "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", + "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", + "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", + "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", + "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", + "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", + "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", + "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", + "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", + "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", + "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", + "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", + "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", + "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", + "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", + "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", + "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", + "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", + "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", + "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", + "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", + "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", + "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", + "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", + "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", + "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", + "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", + "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", + "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", + "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", + "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", + "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", + "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" + "version": "==3.4.0" }, "coverage": { "hashes": [ - "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", - "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", - "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", - "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", - "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", - "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", - "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", - "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", - "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", - "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", - "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", - "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", - "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", - "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", - "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", - "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", - "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", - "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", - "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", - "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", - "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", - "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", - "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", - "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", - "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", - "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", - "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", - "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", - "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", - "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", - "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", - "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", - "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", - "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", - "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", - "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", - "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", - "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", - "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", - "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", - "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", - "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", - "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", - "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", - "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", - "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", - "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", - "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", - "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", - "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", - "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", - "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", - "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", - "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", - "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", - "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", - "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", - "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", - "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", - "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", - "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", - "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", - "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", - "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", - "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", - "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", - "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", - "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", - "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", - "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", - "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", - "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc" + "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376", + "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", + "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111", + "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172", + "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491", + "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", + "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", + "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", + "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", + "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c", + "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", + "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", + "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", + "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0", + "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", + "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", + "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", + "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", + "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", + "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e", + "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", + "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", + "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", + "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea", + "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", + "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", + "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07", + "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", + "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa", + "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901", + "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", + "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", + "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0", + "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", + "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", + "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", + "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51", + "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", + "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3", + "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", + "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076", + "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", + "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", + "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", + "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", + "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", + "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", + "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09", + "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", + "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", + "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f", + "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72", + "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a", + "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", + "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b", + "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", + "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", + "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", + "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", + "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", + "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", + "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==7.6.1" + "markers": "python_version >= '3.9'", + "version": "==7.6.4" }, "debugpy": { "hashes": [ - "sha256:0a85707c6a84b0c5b3db92a2df685b5230dd8fb8c108298ba4f11dba157a615a", - "sha256:22140bc02c66cda6053b6eb56dfe01bbe22a4447846581ba1dd6df2c9f97982d", - "sha256:30f467c5345d9dfdcc0afdb10e018e47f092e383447500f125b4e013236bf14b", - "sha256:3358aa619a073b620cd0d51d8a6176590af24abcc3fe2e479929a154bf591b51", - "sha256:43996632bee7435583952155c06881074b9a742a86cee74e701d87ca532fe833", - "sha256:538c6cdcdcdad310bbefd96d7850be1cd46e703079cc9e67d42a9ca776cdc8a8", - "sha256:567419081ff67da766c898ccf21e79f1adad0e321381b0dfc7a9c8f7a9347972", - "sha256:5d73d8c52614432f4215d0fe79a7e595d0dd162b5c15233762565be2f014803b", - "sha256:67479a94cf5fd2c2d88f9615e087fcb4fec169ec780464a3f2ba4a9a2bb79955", - "sha256:9fb8653f6cbf1dd0a305ac1aa66ec246002145074ea57933978346ea5afdf70b", - "sha256:b48892df4d810eff21d3ef37274f4c60d32cdcafc462ad5647239036b0f0649f", - "sha256:c1cef65cffbc96e7b392d9178dbfd524ab0750da6c0023c027ddcac968fd1caa", - "sha256:c931a9371a86784cee25dec8d65bc2dc7a21f3f1552e3833d9ef8f919d22280a", - "sha256:c9834dfd701a1f6bf0f7f0b8b1573970ae99ebbeee68314116e0ccc5c78eea3c", - "sha256:cdaf0b9691879da2d13fa39b61c01887c34558d1ff6e5c30e2eb698f5384cd43", - "sha256:db891b141fc6ee4b5fc6d1cc8035ec329cabc64bdd2ae672b4550c87d4ecb128", - "sha256:df5dc9eb4ca050273b8e374a4cd967c43be1327eeb42bfe2f58b3cdfe7c68dcb", - "sha256:e3a82da039cfe717b6fb1886cbbe5c4a3f15d7df4765af857f4307585121c2dd", - "sha256:e3e182cd98eac20ee23a00653503315085b29ab44ed66269482349d307b08df9", - "sha256:e4ce0570aa4aca87137890d23b86faeadf184924ad892d20c54237bcaab75d8f", - "sha256:f1e60bd06bb3cc5c0e957df748d1fab501e01416c43a7bdc756d2a992ea1b881", - "sha256:f7158252803d0752ed5398d291dee4c553bb12d14547c0e1843ab74ee9c31123" + "sha256:11ad72eb9ddb436afb8337891a986302e14944f0f755fd94e90d0d71e9100bba", + "sha256:171899588bcd412151e593bd40d9907133a7622cd6ecdbdb75f89d1551df13c2", + "sha256:18b8f731ed3e2e1df8e9cdaa23fb1fc9c24e570cd0081625308ec51c82efe42e", + "sha256:29e1571c276d643757ea126d014abda081eb5ea4c851628b33de0c2b6245b037", + "sha256:2efb84d6789352d7950b03d7f866e6d180284bc02c7e12cb37b489b7083d81aa", + "sha256:2f729228430ef191c1e4df72a75ac94e9bf77413ce5f3f900018712c9da0aaca", + "sha256:45c30aaefb3e1975e8a0258f5bbd26cd40cde9bfe71e9e5a7ac82e79bad64e39", + "sha256:4b908291a1d051ef3331484de8e959ef3e66f12b5e610c203b5b75d2725613a7", + "sha256:4d27d842311353ede0ad572600c62e4bcd74f458ee01ab0dd3a1a4457e7e3706", + "sha256:57b00de1c8d2c84a61b90880f7e5b6deaf4c312ecbde3a0e8912f2a56c4ac9ae", + "sha256:628a11f4b295ffb4141d8242a9bb52b77ad4a63a2ad19217a93be0f77f2c28c9", + "sha256:6a9d9d6d31846d8e34f52987ee0f1a904c7baa4912bf4843ab39dadf9b8f3e0d", + "sha256:6e1c4ffb0c79f66e89dfd97944f335880f0d50ad29525dc792785384923e2211", + "sha256:703c1fd62ae0356e194f3e7b7a92acd931f71fe81c4b3be2c17a7b8a4b546ec2", + "sha256:85ce9c1d0eebf622f86cc68618ad64bf66c4fc3197d88f74bb695a416837dd55", + "sha256:90d93e4f2db442f8222dec5ec55ccfc8005821028982f1968ebf551d32b28907", + "sha256:93176e7672551cb5281577cdb62c63aadc87ec036f0c6a486f0ded337c504596", + "sha256:95fe04a573b8b22896c404365e03f4eda0ce0ba135b7667a1e57bd079793b96b", + "sha256:a6cf2510740e0c0b4a40330640e4b454f928c7b99b0c9dbf48b11efba08a8cda", + "sha256:b12515e04720e9e5c2216cc7086d0edadf25d7ab7e3564ec8b4521cf111b4f8c", + "sha256:b6db2a370e2700557a976eaadb16243ec9c91bd46f1b3bb15376d7aaa7632c81", + "sha256:caf528ff9e7308b74a1749c183d6808ffbedbb9fb6af78b033c28974d9b8831f", + "sha256:cba1d078cf2e1e0b8402e6bda528bf8fda7ccd158c3dba6c012b7897747c41a0", + "sha256:d050a1ec7e925f514f0f6594a1e522580317da31fbda1af71d1530d6ea1f2b40", + "sha256:da8df5b89a41f1fd31503b179d0a84a5fdb752dddd5b5388dbd1ae23cda31ce9", + "sha256:f2f4349a28e3228a42958f8ddaa6333d6f8282d5edaea456070e48609c5983b7" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.8.6" + "version": "==1.8.7" }, "decorator": { "hashes": [ @@ -2053,19 +2137,19 @@ }, "distlib": { "hashes": [ - "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", - "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64" + "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", + "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403" ], - "version": "==0.3.8" + "version": "==0.3.9" }, "django": { "hashes": [ - "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2", - "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f" + "sha256:bd7376f90c99f96b643722eee676498706c9fd7dc759f55ebfaf2c08ebcdf4f0", + "sha256:f11aa87ad8d5617171e3f77e1d5d16f004b79a2cf5d2e1d2b97a6a1f8e9ba5ed" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==5.1.1" + "version": "==5.1.2" }, "django-coverage-plugin": { "hashes": [ @@ -2095,19 +2179,19 @@ }, "django-stubs": { "hashes": [ - "sha256:86128c228b65e6c9a85e5dc56eb1c6f41125917dae0e21e6cfecdf1b27e630c5", - "sha256:b98d49a80aa4adf1433a97407102d068de26c739c405431d93faad96dd282c40" + "sha256:126d354bbdff4906c4e93e6361197f6fbfb6231c3df6def85a291dae6f9f577b", + "sha256:c4dc64260bd72e6d32b9e536e8dd0d9247922f0271f82d1d5132a18f24b388ac" ], "markers": "python_version >= '3.8'", - "version": "==5.1.0" + "version": "==5.1.1" }, "django-stubs-ext": { "hashes": [ - "sha256:a455fc222c90b30b29ad8c53319559f5b54a99b4197205ddbb385aede03b395d", - "sha256:ed7d51c0b731651879fc75f331fb0806d98b67bfab464e96e2724db6b46ef926" + "sha256:3907f99e178c93323e2ce908aef8352adb8c047605161f8d9e5e7b4efb5a6a9c", + "sha256:db7364e4f50ae7e5360993dbd58a3a57ea4b2e7e5bab0fbd525ccdb3e7975d1c" ], "markers": "python_version >= '3.8'", - "version": "==5.1.0" + "version": "==5.1.1" }, "djangorestframework-stubs": { "hashes": [ @@ -2137,11 +2221,11 @@ }, "faker": { "hashes": [ - "sha256:dbf81295c948270a9e96cd48a9a3ebec73acac9a153d0c854fbbd0294557609f", - "sha256:e0593931bd7be9a9ea984b5d8c302ef1cec19392585d1e90d444199271d0a94d" + "sha256:4f7f133560b9d4d2a915581f4ba86f9a6a83421b89e911f36c4c96cff58135a5", + "sha256:93e8b70813f76d05d98951154681180cb795cfbcff3eced7680d963bcc0da2a9" ], "markers": "python_version >= '3.8'", - "version": "==30.1.0" + "version": "==30.8.1" }, "filelock": { "hashes": [ @@ -2186,12 +2270,12 @@ }, "ipython": { "hashes": [ - "sha256:0b99a2dc9f15fd68692e898e5568725c6d49c527d36a9fb5960ffbdeaa82ff7e", - "sha256:f68b3cb8bde357a5d7adc9598d57e22a45dfbea19eb6b98286fa3b288c9cd55c" + "sha256:0d0d15ca1e01faeb868ef56bc7ee5a0de5bd66885735682e8a322ae289a13d1a", + "sha256:530ef1e7bb693724d3cdc37287c80b07ad9b25986c007a53aa1857272dac3f35" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==8.27.0" + "version": "==8.28.0" }, "jedi": { "hashes": [ @@ -2211,69 +2295,70 @@ }, "markupsafe": { "hashes": [ - "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", - "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", - "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", - "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", - "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", - "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", - "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", - "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", - "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", - "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", - "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", - "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", - "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", - "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", - "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", - "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", - "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", - "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", - "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", - "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", - "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", - "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", - "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", - "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", - "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", - "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", - "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", - "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", - "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", - "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", - "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", - "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", - "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", - "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", - "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", - "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", - "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", - "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", - "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", - "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", - "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", - "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", - "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", - "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", - "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", - "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", - "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", - "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", - "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", - "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", - "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", - "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", - "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", - "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", - "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", - "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", - "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", - "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", - "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", - "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" + "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", + "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", + "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", + "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", + "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", + "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", + "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", + "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", + "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", + "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", + "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", + "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", + "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", + "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", + "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", + "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", + "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", + "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", + "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", + "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", + "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", + "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", + "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", + "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", + "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", + "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", + "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", + "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", + "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", + "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", + "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", + "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", + "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", + "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", + "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", + "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", + "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", + "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", + "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", + "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", + "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", + "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", + "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", + "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", + "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", + "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", + "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", + "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", + "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", + "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", + "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", + "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", + "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", + "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", + "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", + "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", + "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", + "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", + "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", + "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", + "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" ], - "markers": "python_version >= '3.7'", - "version": "==2.1.5" + "markers": "python_version >= '3.9'", + "version": "==3.0.2" }, "matplotlib-inline": { "hashes": [ @@ -2285,44 +2370,49 @@ }, "mypy": { "hashes": [ - "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", - "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce", - "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6", - "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b", - "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca", - "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24", - "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383", - "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7", - "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86", - "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d", - "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4", - "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8", - "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987", - "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385", - "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", - "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef", - "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6", - "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70", - "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca", - "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70", - "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", - "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104", - "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a", - "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318", - "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1", - "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b", - "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d" + "sha256:02dcfe270c6ea13338210908f8cadc8d31af0f04cee8ca996438fe6a97b4ec66", + "sha256:0dcc1e843d58f444fce19da4cce5bd35c282d4bde232acdeca8279523087088a", + "sha256:0e6fe449223fa59fbee351db32283838a8fee8059e0028e9e6494a03802b4004", + "sha256:1230048fec1380faf240be6385e709c8570604d2d27ec6ca7e573e3bc09c3735", + "sha256:186e0c8346efc027ee1f9acf5ca734425fc4f7dc2b60144f0fbe27cc19dc7931", + "sha256:19bf51f87a295e7ab2894f1d8167622b063492d754e69c3c2fed6563268cb42a", + "sha256:20db6eb1ca3d1de8ece00033b12f793f1ea9da767334b7e8c626a4872090cf02", + "sha256:389e307e333879c571029d5b93932cf838b811d3f5395ed1ad05086b52148fb0", + "sha256:3d7d4371829184e22fda4015278fbfdef0327a4b955a483012bd2d423a788801", + "sha256:427878aa54f2e2c5d8db31fa9010c599ed9f994b3b49e64ae9cd9990c40bd635", + "sha256:4ee5932370ccf7ebf83f79d1c157a5929d7ea36313027b0d70a488493dc1b179", + "sha256:5fcde63ea2c9f69d6be859a1e6dd35955e87fa81de95bc240143cf00de1f7f81", + "sha256:673ba1140a478b50e6d265c03391702fa11a5c5aff3f54d69a62a48da32cb811", + "sha256:8135ffec02121a75f75dc97c81af7c14aa4ae0dda277132cfcd6abcd21551bfd", + "sha256:843826966f1d65925e8b50d2b483065c51fc16dc5d72647e0236aae51dc8d77e", + "sha256:94b2048a95a21f7a9ebc9fbd075a4fcd310410d078aa0228dbbad7f71335e042", + "sha256:96af62050971c5241afb4701c15189ea9507db89ad07794a4ee7b4e092dc0627", + "sha256:9fb83a7be97c498176fb7486cafbb81decccaef1ac339d837c377b0ce3743a7f", + "sha256:9fe20f89da41a95e14c34b1ddb09c80262edcc295ad891f22cc4b60013e8f78d", + "sha256:a5a437c9102a6a252d9e3a63edc191a3aed5f2fcb786d614722ee3f4472e33f6", + "sha256:a7b76fa83260824300cc4834a3ab93180db19876bce59af921467fd03e692810", + "sha256:b16fe09f9c741d85a2e3b14a5257a27a4f4886c171d562bc5a5e90d8591906b8", + "sha256:b947097fae68004b8328c55161ac9db7d3566abfef72d9d41b47a021c2fba6b1", + "sha256:ce561a09e3bb9863ab77edf29ae3a50e65685ad74bba1431278185b7e5d5486e", + "sha256:d34167d43613ffb1d6c6cdc0cc043bb106cac0aa5d6a4171f77ab92a3c758bcc", + "sha256:d54d840f6c052929f4a3d2aab2066af0f45a020b085fe0e40d4583db52aab4e4", + "sha256:d90da248f4c2dba6c44ddcfea94bb361e491962f05f41990ff24dbd09969ce20", + "sha256:dc6e2a2195a290a7fd5bac3e60b586d77fc88e986eba7feced8b778c373f9afe", + "sha256:de5b2a8988b4e1269a98beaf0e7cc71b510d050dce80c343b53b4955fff45f19", + "sha256:e10ba7de5c616e44ad21005fa13450cd0de7caaa303a626147d45307492e4f2d", + "sha256:f59f1dfbf497d473201356966e353ef09d4daec48caeacc0254db8ef633a28a5", + "sha256:f5b3936f7a6d0e8280c9bdef94c7ce4847f5cdfc258fbb2c29a8c1711e8bb96d" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.11.2" + "version": "==1.12.1" }, "mypy-boto3-s3": { "hashes": [ - "sha256:5b5aec56e16ef48766ae81b1807263127eebc4e6bfbeeef3f2f2cb7c30c06d03", - "sha256:9f64e1196ffecc2c6ab7bee95e848692b5f464a1df14211361ea6bbbc2038387" + "sha256:34d19dfba400f5b9bd6b64f09eb8f8eedef60545b410a3753fe99fec0c41ba78", + "sha256:f0087a3765d103b2db565cd8065ebc2b0f70f2dd4e92c132f64b8945dd869940" ], - "version": "==1.35.22" + "version": "==1.35.46" }, "mypy-extensions": { "hashes": [ @@ -2366,12 +2456,12 @@ }, "pre-commit": { "hashes": [ - "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af", - "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f" + "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", + "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==3.8.0" + "version": "==4.0.1" }, "prompt-toolkit": { "hashes": [ @@ -2416,7 +2506,7 @@ "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==2.9.0.post0" }, "pyyaml": { @@ -2498,43 +2588,43 @@ }, "ruff": { "hashes": [ - "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750", - "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa", - "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c", - "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0", - "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f", - "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098", - "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0", - "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f", - "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44", - "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2", - "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a", - "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc", - "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb", - "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18", - "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5", - "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce", - "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263", - "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87" + "sha256:0cdf20c2b6ff98e37df47b2b0bd3a34aaa155f59a11182c1303cce79be715628", + "sha256:10842f69c245e78d6adec7e1db0a7d9ddc2fff0621d730e61657b64fa36f207e", + "sha256:194d6c46c98c73949a106425ed40a576f52291c12bc21399eb8f13a0f7073495", + "sha256:1eb54986f770f49edb14f71d33312d79e00e629a57387382200b1ef12d6a4ef9", + "sha256:211d877674e9373d4bb0f1c80f97a0201c61bcd1e9d045b6e9726adc42c156aa", + "sha256:214b88498684e20b6b2b8852c01d50f0651f3cc6118dfa113b4def9f14faaf06", + "sha256:47a86360cf62d9cd53ebfb0b5eb0e882193fc191c6d717e8bef4462bc3b9ea2b", + "sha256:496494d350c7fdeb36ca4ef1c9f21d80d182423718782222c29b3e72b3512737", + "sha256:4b406c2dce5be9bad59f2de26139a86017a517e6bcd2688da515481c05a2cb11", + "sha256:630fce3fefe9844e91ea5bbf7ceadab4f9981f42b704fae011bb8efcaf5d84be", + "sha256:82c2579b82b9973a110fab281860403b397c08c403de92de19568f32f7178598", + "sha256:9af971fe85dcd5eaed8f585ddbc6bdbe8c217fb8fcf510ea6bca5bdfff56040e", + "sha256:ab7d98c7eed355166f367597e513a6c82408df4181a937628dbec79abb2a1fe4", + "sha256:b641c7f16939b7d24b7bfc0be4102c56562a18281f84f635604e8a6989948914", + "sha256:d71672336e46b34e0c90a790afeac8a31954fd42872c1f6adaea1dff76fd44f9", + "sha256:dc452ba6f2bb9cf8726a84aa877061a2462afe9ae0ea1d411c53d226661c601d", + "sha256:f6c968509f767776f524a8430426539587d5ec5c662f6addb6aa25bc2e8195ec", + "sha256:ff4aabfbaaba880e85d394603b9e75d32b0693152e16fa659a3064a85df7fce2" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.6.8" + "version": "==0.7.0" }, "s3transfer": { "hashes": [ - "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", - "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69" + "sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d", + "sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c" ], "markers": "python_version >= '3.8'", - "version": "==0.10.2" + "version": "==0.10.3" }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", "version": "==1.16.0" }, "sqlparse": { @@ -2571,11 +2661,11 @@ }, "types-awscrt": { "hashes": [ - "sha256:67a660c90bad360c339f6a79310cc17094d12472042c7ca5a41450aaf5fc9a54", - "sha256:b2c196bbd3226bab42d80fae13c34548de9ddc195f5a366d79c15d18e5897aa9" + "sha256:3fd1edeac923d1956c0e907c973fb83bda465beae7f054716b371b293f9b5fdc", + "sha256:517d9d06f19cf58d778ca90ad01e52e0489466bf70dcf78c7f47f74fdf151a60" ], "markers": "python_version >= '3.8'", - "version": "==0.22.0" + "version": "==0.23.0" }, "types-pyyaml": { "hashes": [ @@ -2587,26 +2677,26 @@ }, "types-requests": { "hashes": [ - "sha256:2850e178db3919d9bf809e434eef65ba49d0e7e33ac92d588f4a5e295fffd405", - "sha256:59c2f673eb55f32a99b2894faf6020e1a9f4a402ad0f192bfee0b64469054310" + "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95", + "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747" ], "markers": "python_version >= '3.8'", - "version": "==2.32.0.20240914" + "version": "==2.32.0.20241016" }, "types-s3transfer": { "hashes": [ - "sha256:60167a3bfb5c536ec6cdb5818f7f9a28edca9dc3e0b5ff85ae374526fc5e576e", - "sha256:7a3fec8cd632e2b5efb665a355ef93c2a87fdd5a45b74a949f95a9e628a86356" + "sha256:d34c5a82f531af95bb550927136ff5b737a1ed3087f90a59d545591dfde5b4cc", + "sha256:f761b2876ac4c208e6c6b75cdf5f6939009768be9950c545b11b0225e7703ee7" ], "markers": "python_version >= '3.8'", - "version": "==0.10.2" + "version": "==0.10.3" }, "typing-extensions": { "hashes": [ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8" ], - "markers": "python_version < '3.13'", + "markers": "python_version >= '3.8'", "version": "==4.12.2" }, "urllib3": { @@ -2619,11 +2709,11 @@ }, "virtualenv": { "hashes": [ - "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48", - "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2" + "sha256:2ca56a68ed615b8fe4326d11a0dca5dfbe8fd68510fb6c6349163bed3c15f2b2", + "sha256:44a72c29cceb0ee08f300b314848c86e57bf8d1f13107a5e671fb9274138d655" ], - "markers": "python_version >= '3.7'", - "version": "==20.26.6" + "markers": "python_version >= '3.8'", + "version": "==20.27.0" }, "watchdog": { "hashes": [ @@ -2671,12 +2761,12 @@ }, "werkzeug": { "hashes": [ - "sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c", - "sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306" + "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17", + "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==3.0.4" + "version": "==3.0.6" } }, "docs": { @@ -2714,99 +2804,114 @@ }, "charset-normalizer": { "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", + "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", + "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", + "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", + "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", + "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", + "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", + "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", + "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", + "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", + "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", + "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", + "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", + "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", + "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", + "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", + "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", + "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", + "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", + "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", + "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", + "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", + "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", + "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", + "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", + "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", + "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", + "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", + "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", + "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", + "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", + "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", + "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", + "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", + "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", + "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", + "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", + "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", + "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", + "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", + "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", + "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", + "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", + "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", + "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", + "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", + "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", + "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", + "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", + "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", + "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", + "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", + "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", + "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", + "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", + "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", + "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", + "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", + "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", + "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", + "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", + "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", + "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", + "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", + "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", + "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", + "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", + "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", + "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", + "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", + "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", + "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", + "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", + "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", + "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", + "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", + "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", + "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", + "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", + "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", + "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", + "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", + "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", + "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", + "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", + "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", + "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", + "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", + "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", + "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", + "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", + "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", + "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", + "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", + "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", + "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", + "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", + "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4", + "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", + "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", + "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", + "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748", + "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", + "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", + "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482" ], "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" + "version": "==3.4.0" }, "docutils": { "hashes": [ @@ -2859,69 +2964,70 @@ }, "markupsafe": { "hashes": [ - "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", - "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", - "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", - "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", - "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", - "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", - "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", - "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df", - "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", - "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", - "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", - "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", - "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", - "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371", - "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2", - "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", - "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52", - "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", - "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", - "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", - "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", - "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", - "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", - "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", - "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", - "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", - "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", - "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", - "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", - "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9", - "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", - "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", - "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", - "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", - "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", - "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", - "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a", - "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", - "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", - "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", - "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", - "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", - "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", - "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", - "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", - "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f", - "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50", - "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", - "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", - "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", - "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", - "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", - "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", - "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", - "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf", - "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", - "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", - "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", - "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", - "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68" + "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", + "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", + "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", + "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", + "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", + "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", + "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", + "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", + "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", + "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", + "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", + "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", + "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", + "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", + "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", + "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", + "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", + "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", + "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", + "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", + "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", + "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", + "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", + "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", + "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", + "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", + "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", + "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", + "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", + "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", + "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", + "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", + "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", + "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", + "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", + "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", + "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", + "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", + "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", + "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", + "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", + "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", + "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", + "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", + "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", + "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", + "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", + "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", + "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", + "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", + "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", + "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", + "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", + "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", + "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", + "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", + "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", + "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", + "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", + "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", + "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" ], - "markers": "python_version >= '3.7'", - "version": "==2.1.5" + "markers": "python_version >= '3.9'", + "version": "==3.0.2" }, "mdit-py-plugins": { "hashes": [ diff --git a/aws/backend.json b/aws/backend.json index fcacb36194..145b36061b 100644 --- a/aws/backend.json +++ b/aws/backend.json @@ -114,29 +114,21 @@ "value": "True" }, { - "name": "ENABLE_ABDM", - "value": "True" - }, - { - "name": "ABDM_URL", + "name": "ABDM_GATEWAY_URL", "value": "https://dev.abdm.gov.in" }, { - "name": "HEALTH_SERVICE_API_URL", - "value": "https://healthidsbx.abdm.gov.in/api" + "name": "ABDM_ABHA_URL", + "value": "https://abhasbx.abdm.gov.in" }, { "name": "ABDM_FACILITY_URL", "value": "https://facilitysbx.abdm.gov.in" }, { - "name": "X_CM_ID", + "name": "ABDM_CM_ID", "value": "sbx" }, - { - "name": "FIDELIUS_URL", - "value": "https://fidelius.ohc.network" - }, { "name": "SENTRY_TRACES_SAMPLE_RATE", "value": "1.0" diff --git a/aws/celery.json b/aws/celery.json index efb6182134..197c6d7346 100644 --- a/aws/celery.json +++ b/aws/celery.json @@ -11,9 +11,7 @@ } }, "portMappings": [], - "command": [ - "/app/celery_beat-ecs.sh" - ], + "command": ["/app/celery_beat-ecs.sh"], "cpu": 128, "environment": [ { @@ -97,29 +95,21 @@ "value": "True" }, { - "name": "ENABLE_ABDM", - "value": "True" - }, - { - "name": "ABDM_URL", + "name": "ABDM_GATEWAY_URL", "value": "https://dev.abdm.gov.in" }, { - "name": "HEALTH_SERVICE_API_URL", - "value": "https://healthidsbx.abdm.gov.in/api" + "name": "ABDM_ABHA_URL", + "value": "https://abhasbx.abdm.gov.in" }, { "name": "ABDM_FACILITY_URL", "value": "https://facilitysbx.abdm.gov.in" }, { - "name": "X_CM_ID", + "name": "ABDM_CM_ID", "value": "sbx" }, - { - "name": "FIDELIUS_URL", - "value": "https://fidelius.ohc.network" - }, { "name": "SENTRY_TRACES_SAMPLE_RATE", "value": "1.0" @@ -296,9 +286,7 @@ "awslogs-stream-prefix": "ecs" } }, - "command": [ - "/app/celery_worker-ecs.sh" - ], + "command": ["/app/celery_worker-ecs.sh"], "cpu": 384, "memory": 1536, "memoryReservation": 1536, @@ -384,24 +372,20 @@ "value": "True" }, { - "name": "ENABLE_ABDM", - "value": "True" - }, - { - "name": "ABDM_URL", + "name": "ABDM_GATEWAY_URL", "value": "https://dev.abdm.gov.in" }, { - "name": "HEALTH_SERVICE_API_URL", - "value": "https://healthidsbx.abdm.gov.in/api" + "name": "ABDM_ABHA_URL", + "value": "https://abhasbx.abdm.gov.in" }, { - "name": "X_CM_ID", - "value": "sbx" + "name": "ABDM_FACILITY_URL", + "value": "https://facilitysbx.abdm.gov.in" }, { - "name": "FIDELIUS_URL", - "value": "https://fidelius.ohc.network" + "name": "ABDM_CM_ID", + "value": "sbx" }, { "name": "SENTRY_TRACES_SAMPLE_RATE", diff --git a/care/abdm/admin.py b/care/abdm/admin.py deleted file mode 100644 index 846f6b4061..0000000000 --- a/care/abdm/admin.py +++ /dev/null @@ -1 +0,0 @@ -# Register your models here. diff --git a/care/abdm/api/serializers/abha_number.py b/care/abdm/api/serializers/abha_number.py deleted file mode 100644 index c166d57228..0000000000 --- a/care/abdm/api/serializers/abha_number.py +++ /dev/null @@ -1,20 +0,0 @@ -# ModelSerializer -from rest_framework import serializers - -from care.abdm.models import AbhaNumber -from care.facility.api.serializers.patient import PatientDetailSerializer -from care.facility.models import PatientRegistration -from care.utils.serializers.fields import ExternalIdSerializerField - - -class AbhaNumberSerializer(serializers.ModelSerializer): - id = serializers.CharField(source="external_id", read_only=True) - patient = ExternalIdSerializerField( - queryset=PatientRegistration.objects.all(), required=False, allow_null=True - ) - patient_object = PatientDetailSerializer(source="patient", read_only=True) - new = serializers.BooleanField(read_only=True) - - class Meta: - model = AbhaNumber - exclude = ("deleted", "access_token", "refresh_token", "txn_id") diff --git a/care/abdm/api/serializers/auth.py b/care/abdm/api/serializers/auth.py deleted file mode 100644 index 9b533d0c9b..0000000000 --- a/care/abdm/api/serializers/auth.py +++ /dev/null @@ -1,24 +0,0 @@ -from rest_framework.serializers import CharField, IntegerField, Serializer - - -class AbdmAuthResponseSerializer(Serializer): - """ - Serializer for the response of the authentication API - """ - - accessToken = CharField() - refreshToken = CharField() - expiresIn = IntegerField() - refreshExpiresIn = IntegerField() - tokenType = CharField() - - -class AbdmAuthInitResponseSerializer(Serializer): - """ - Serializer for the response of the authentication API - """ - - token = CharField() - refreshToken = CharField() - expiresIn = IntegerField() - refreshExpiresIn = IntegerField() diff --git a/care/abdm/api/serializers/consent.py b/care/abdm/api/serializers/consent.py deleted file mode 100644 index e2f3d6cb93..0000000000 --- a/care/abdm/api/serializers/consent.py +++ /dev/null @@ -1,33 +0,0 @@ -from rest_framework import serializers - -from care.abdm.api.serializers.abha_number import AbhaNumberSerializer -from care.abdm.models.consent import ConsentArtefact, ConsentRequest -from care.users.api.serializers.user import UserBaseMinimumSerializer - - -class ConsentArtefactSerializer(serializers.ModelSerializer): - id = serializers.CharField(source="external_id", read_only=True) - - class Meta: - model = ConsentArtefact - exclude = ( - "deleted", - "external_id", - "key_material_private_key", - "key_material_public_key", - "key_material_nonce", - "key_material_algorithm", - "key_material_curve", - "signature", - ) - - -class ConsentRequestSerializer(serializers.ModelSerializer): - id = serializers.CharField(source="external_id", read_only=True) - patient_abha_object = AbhaNumberSerializer(source="patient_abha", read_only=True) - requester = UserBaseMinimumSerializer(read_only=True) - consent_artefacts = ConsentArtefactSerializer(many=True, read_only=True) - - class Meta: - model = ConsentRequest - exclude = ("deleted", "external_id") diff --git a/care/abdm/api/serializers/health_facility.py b/care/abdm/api/serializers/health_facility.py deleted file mode 100644 index 336f348584..0000000000 --- a/care/abdm/api/serializers/health_facility.py +++ /dev/null @@ -1,12 +0,0 @@ -from rest_framework import serializers - -from care.abdm.models import HealthFacility - - -class HealthFacilitySerializer(serializers.ModelSerializer): - id = serializers.CharField(source="external_id", read_only=True) - registered = serializers.BooleanField(read_only=True) - - class Meta: - model = HealthFacility - exclude = ("deleted",) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py deleted file mode 100644 index 2c1910b823..0000000000 --- a/care/abdm/api/serializers/healthid.py +++ /dev/null @@ -1,66 +0,0 @@ -from rest_framework.serializers import CharField, Serializer, UUIDField - - -class AadharOtpGenerateRequestPayloadSerializer(Serializer): - aadhaar = CharField(max_length=16, min_length=12, required=True) - - -class AadharOtpResendRequestPayloadSerializer(Serializer): - txnId = CharField(max_length=64, min_length=1, required=True) - - -class HealthIdSerializer(Serializer): - healthId = CharField(max_length=64, min_length=1, required=True) - - -class QRContentSerializer(Serializer): - hidn = CharField(max_length=17, min_length=17, required=True) - phr = CharField(max_length=64, min_length=1, required=True) - name = CharField(max_length=64, min_length=1, required=True) - gender = CharField(max_length=1, min_length=1, required=True) - dob = CharField(max_length=10, min_length=8, required=True) - - -class HealthIdAuthSerializer(Serializer): - authMethod = CharField(max_length=64, min_length=1, required=True) - healthid = CharField(max_length=64, min_length=1, required=True) - - -class ABHASearchRequestSerializer: - name = CharField(max_length=64, min_length=1, required=False) - mobile = CharField( - max_length=10, - min_length=10, - required=False, - ) - gender = CharField(max_length=1, min_length=1, required=False) - yearOfBirth = CharField(max_length=4, min_length=4, required=False) - - -class GenerateMobileOtpRequestPayloadSerializer(Serializer): - mobile = CharField(max_length=10, min_length=10, required=True) - txnId = CharField(max_length=64, min_length=1, required=True) - - -class VerifyOtpRequestPayloadSerializer(Serializer): - otp = CharField(max_length=6, min_length=6, required=True, help_text="OTP") - txnId = CharField(max_length=64, min_length=1, required=True) - patientId = UUIDField(required=False) - - -class VerifyDemographicsRequestPayloadSerializer(Serializer): - gender = CharField(max_length=10, min_length=1, required=True) - name = CharField(max_length=64, min_length=1, required=True) - yearOfBirth = CharField(max_length=4, min_length=4, required=True) - txnId = CharField(max_length=64, min_length=1, required=True) - - -class CreateHealthIdSerializer(Serializer): - healthId = CharField(max_length=64, min_length=1, required=False) - txnId = CharField(max_length=64, min_length=1, required=True) - patientId = UUIDField(required=False) - - -class LinkPatientSerializer(Serializer): - abha_number = UUIDField(required=True) - patient = UUIDField(required=True) diff --git a/care/abdm/api/serializers/hip.py b/care/abdm/api/serializers/hip.py deleted file mode 100644 index 4e3bb0f9ab..0000000000 --- a/care/abdm/api/serializers/hip.py +++ /dev/null @@ -1,33 +0,0 @@ -from rest_framework.serializers import CharField, IntegerField, Serializer - - -class AddressSerializer(Serializer): - line = CharField() - district = CharField() - state = CharField() - pincode = CharField() - - -class PatientSerializer(Serializer): - healthId = CharField(allow_null=True) - healthIdNumber = CharField() - name = CharField() - gender = CharField() - yearOfBirth = IntegerField() - dayOfBirth = IntegerField() - monthOfBirth = IntegerField() - address = AddressSerializer() - - -class ProfileSerializer(Serializer): - hipCode = CharField() - patient = PatientSerializer() - - -class HipShareProfileSerializer(Serializer): - """ - Serializer for the request of the share_profile - """ - - requestId = CharField() - profile = ProfileSerializer() diff --git a/care/abdm/api/viewsets/abha_number.py b/care/abdm/api/viewsets/abha_number.py deleted file mode 100644 index 2e94f2aae6..0000000000 --- a/care/abdm/api/viewsets/abha_number.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.db.models import Q -from django.http import Http404 -from rest_framework.decorators import action -from rest_framework.mixins import RetrieveModelMixin -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.abdm.api.serializers.abha_number import AbhaNumberSerializer -from care.abdm.models import AbhaNumber -from care.abdm.utils.api_call import HealthIdGateway -from care.utils.queryset.patient import get_patient_queryset - - -class AbhaNumberViewSet( - GenericViewSet, - RetrieveModelMixin, -): - serializer_class = AbhaNumberSerializer - model = AbhaNumber - queryset = AbhaNumber.objects.all() - - def get_object(self): - id = self.kwargs.get("pk") - - instance = self.queryset.filter( - Q(abha_number=id) | Q(health_id=id) | Q(patient__external_id=id) - ).first() - - if not instance or not get_patient_queryset(self.request.user).contains( - instance.patient - ): - raise Http404 - - self.check_object_permissions(self.request, instance) - - return instance - - @action(detail=True, methods=["GET"]) - def qr_code(self, request, *args, **kwargs): - obj = self.get_object() - serializer = self.get_serializer(obj) - response = HealthIdGateway().get_qr_code(serializer.data) - return Response(response) - - @action(detail=True, methods=["GET"]) - def profile(self, request, *args, **kwargs): - obj = self.get_object() - serializer = self.get_serializer(obj) - response = HealthIdGateway().get_profile(serializer.data) - return Response(response) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py deleted file mode 100644 index b63b484583..0000000000 --- a/care/abdm/api/viewsets/auth.py +++ /dev/null @@ -1,341 +0,0 @@ -import json -import logging -from datetime import datetime, timedelta - -from django.core.cache import cache -from rest_framework import status -from rest_framework.generics import GenericAPIView, get_object_or_404 -from rest_framework.response import Response - -from care.abdm.utils.api_call import AbdmGateway -from care.abdm.utils.cipher import Cipher -from care.abdm.utils.fhir import Fhir -from care.facility.models.patient import PatientRegistration -from care.facility.models.patient_consultation import PatientConsultation -from config.authentication import ABDMAuthentication - -logger = logging.getLogger(__name__) - - -class OnFetchView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - AbdmGateway().init(data["resp"]["requestId"]) - - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class OnInitView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - AbdmGateway().confirm(data["auth"]["transactionId"], data["resp"]["requestId"]) - - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class OnConfirmView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - if "validity" in data["auth"]: - if data["auth"]["validity"]["purpose"] == "LINK": - AbdmGateway().add_care_context( - data["auth"]["accessToken"], - data["resp"]["requestId"], - ) - else: - AbdmGateway().save_linking_token( - data["auth"]["patient"], - data["auth"]["accessToken"], - data["resp"]["requestId"], - ) - else: - AbdmGateway().save_linking_token( - data["auth"]["patient"], - data["auth"]["accessToken"], - data["resp"]["requestId"], - ) - AbdmGateway().add_care_context( - data["auth"]["accessToken"], - data["resp"]["requestId"], - ) - - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class AuthNotifyView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - if data["auth"]["status"] != "GRANTED": - return - - AbdmGateway.auth_on_notify({"request_id": data["auth"]["transactionId"]}) - - # AbdmGateway().add_care_context( - # data["auth"]["accessToken"], - # data["resp"]["requestId"], - # ) - - -class OnAddContextsView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class DiscoverView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - patients = PatientRegistration.objects.all() - verified_identifiers = data["patient"]["verifiedIdentifiers"] - matched_by = [] - if len(verified_identifiers) == 0: - return Response( - "No matching records found, need more data", - status=status.HTTP_404_NOT_FOUND, - ) - else: - for identifier in verified_identifiers: - if identifier["value"] is None: - continue - - # if identifier["type"] == "MOBILE": - # matched_by.append(identifier["value"]) - # mobile = identifier["value"].replace("+91", "").replace("-", "") - # patients = patients.filter( - # Q(phone_number=f"+91{mobile}") | Q(phone_number=mobile) - # ) - - if identifier["type"] == "NDHM_HEALTH_NUMBER": - matched_by.append(identifier["value"]) - patients = patients.filter( - abha_number__abha_number=identifier["value"] - ) - - if identifier["type"] == "HEALTH_ID": - matched_by.append(identifier["value"]) - patients = patients.filter( - abha_number__health_id=identifier["value"] - ) - - # TODO: also filter by demographics - patient = patients.last() - - if not patient: - return Response( - "No matching records found, need more data", - status=status.HTTP_404_NOT_FOUND, - ) - - AbdmGateway().on_discover( - { - "request_id": data["requestId"], - "transaction_id": data["transactionId"], - "patient_id": str(patient.external_id), - "patient_name": patient.name, - "care_contexts": list( - map( - lambda consultation: { - "id": str(consultation.external_id), - "name": f"Encounter: {consultation.created_date.date()!s}", - }, - PatientConsultation.objects.filter(patient=patient), - ) - ), - "matched_by": matched_by, - } - ) - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class LinkInitView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - # TODO: send otp to patient - - AbdmGateway().on_link_init( - { - "request_id": data["requestId"], - "transaction_id": data["transactionId"], - "patient_id": data["patient"]["referenceNumber"], - "phone": "7639899448", - } - ) - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class LinkConfirmView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - # TODO: verify otp - - patient = get_object_or_404( - PatientRegistration.objects.filter( - external_id=data["confirmation"]["linkRefNumber"] - ) - ) - AbdmGateway().on_link_confirm( - { - "request_id": data["requestId"], - "patient_id": str(patient.external_id), - "patient_name": patient.name, - "care_contexts": list( - map( - lambda consultation: { - "id": str(consultation.external_id), - "name": f"Encounter: {consultation.created_date.date()!s}", - }, - PatientConsultation.objects.filter(patient=patient), - ) - ), - } - ) - - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class NotifyView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - cache.set(data["notification"]["consentId"], json.dumps(data)) - - AbdmGateway().on_notify( - { - "request_id": data["requestId"], - "consent_id": data["notification"]["consentId"], - } - ) - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class RequestDataView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - consent_id = data["hiRequest"]["consent"]["id"] - consent = json.loads(cache.get(consent_id)) if consent_id in cache else None - if not consent or consent["notification"]["status"] != "GRANTED": - return Response({}, status=status.HTTP_401_UNAUTHORIZED) - - # TODO: check if from and to are in range and consent expiry is greater than today - # consent_from = datetime.fromisoformat( - # consent["notification"]["permission"]["dateRange"]["from"][:-1] - # ) - # consent_to = datetime.fromisoformat( - # consent["notification"]["permission"]["dateRange"]["to"][:-1] - # ) - # now = datetime.now() - # if not consent_from < now and now > consent_to: - # return Response({}, status=status.HTTP_403_FORBIDDEN) - - on_data_request_response = AbdmGateway().on_data_request( - {"request_id": data["requestId"], "transaction_id": data["transactionId"]} - ) - - if on_data_request_response.status_code != 202: - return Response({}, status=status.HTTP_202_ACCEPTED) - return Response( - on_data_request_response, status=status.HTTP_400_BAD_REQUEST - ) - - cipher = Cipher( - data["hiRequest"]["keyMaterial"]["dhPublicKey"]["keyValue"], - data["hiRequest"]["keyMaterial"]["nonce"], - ) - - data_transfer_response = AbdmGateway().data_transfer( - { - "transaction_id": data["transactionId"], - "data_push_url": data["hiRequest"]["dataPushUrl"], - "care_contexts": sum( - list( - map( - lambda context: list( - map( - lambda record: { - "patient_id": context["patientReference"], - "consultation_id": context[ - "careContextReference" - ], - "data": cipher.encrypt( - Fhir( - PatientConsultation.objects.filter( - external_id=context[ - "careContextReference" - ] - ).first() - ).create_record(record) - )["data"], - }, - consent["notification"]["consentDetail"]["hiTypes"], - ) - ), - consent["notification"]["consentDetail"]["careContexts"][ - :-2:-1 - ], - ) - ), - [], - ), - "key_material": { - "cryptoAlg": "ECDH", - "curve": "Curve25519", - "dhPublicKey": { - "expiry": (datetime.now() + timedelta(days=2)).isoformat(), - "parameters": "Curve25519/32byte random key", - "keyValue": cipher.key_to_share, - }, - "nonce": cipher.internal_nonce, - }, - } - ) - - AbdmGateway().data_notify( - { - "health_id": consent["notification"]["consentDetail"]["patient"]["id"], - "consent_id": data["hiRequest"]["consent"]["id"], - "transaction_id": data["transactionId"], - "session_status": ( - "TRANSFERRED" - if data_transfer_response - and data_transfer_response.status_code == 202 - else "FAILED" - ), - "care_contexts": list( - map( - lambda context: {"id": context["careContextReference"]}, - consent["notification"]["consentDetail"]["careContexts"][ - :-2:-1 - ], - ) - ), - } - ) - - return Response({}, status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/consent.py b/care/abdm/api/viewsets/consent.py deleted file mode 100644 index da6fc0ac4f..0000000000 --- a/care/abdm/api/viewsets/consent.py +++ /dev/null @@ -1,260 +0,0 @@ -import logging - -from django_filters import rest_framework as filters -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.mixins import ListModelMixin, RetrieveModelMixin -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.abdm.api.serializers.consent import ConsentRequestSerializer -from care.abdm.api.viewsets.health_information import HealthInformationViewSet -from care.abdm.models.base import Status -from care.abdm.models.consent import ConsentArtefact, ConsentRequest -from care.abdm.service.gateway import Gateway -from care.utils.queryset.facility import get_facility_queryset -from config.auth_views import CaptchaRequiredException -from config.authentication import ABDMAuthentication -from config.ratelimit import USER_READABLE_RATE_LIMIT_TIME, ratelimit - -logger = logging.getLogger(__name__) - - -class ConsentRequestFilter(filters.FilterSet): - patient = filters.UUIDFilter(field_name="patient_abha__patient__external_id") - health_id = filters.CharFilter(field_name="patient_abha__health_id") - ordering = filters.OrderingFilter( - fields=( - "created_date", - "updated_date", - ) - ) - facility = filters.UUIDFilter( - field_name="patient_abha__patient__facility__external_id" - ) - - class Meta: - model = ConsentRequest - fields = ["patient", "health_id", "purpose"] - - -class ConsentViewSet(GenericViewSet, ListModelMixin, RetrieveModelMixin): - serializer_class = ConsentRequestSerializer - model = ConsentRequest - queryset = ConsentRequest.objects.all() - filter_backends = (filters.DjangoFilterBackend,) - filterset_class = ConsentRequestFilter - - def get_queryset(self): - queryset = self.queryset - facilities = get_facility_queryset(self.request.user) - return queryset.filter(requester__facility__in=facilities).distinct() - - def create(self, request): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - - if ratelimit( - request, "consent__create", [serializer.validated_data["patient_abha"]] - ): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - consent = ConsentRequest(**serializer.validated_data, requester=request.user) - - response = Gateway().consent_requests__init(consent) - if response.status_code != 202: - return Response(response.json(), status=response.status_code) - - consent.save() - return Response( - ConsentRequestSerializer(consent).data, status=status.HTTP_201_CREATED - ) - - @action(detail=True, methods=["GET"]) - def status(self, request, pk): - if ratelimit(request, "consent__status", [pk]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - consent = self.queryset.filter(external_id=pk).first() - - if not consent: - return Response(status=status.HTTP_404_NOT_FOUND) - - response = Gateway().consent_requests__status(str(consent.consent_id)) - if response.status_code != 202: - return Response(response.json(), status=response.status_code) - - return Response( - ConsentRequestSerializer(consent).data, status=status.HTTP_200_OK - ) - - @action(detail=True, methods=["GET"]) - def fetch(self, request, pk): - if ratelimit(request, "consent__fetch", [pk]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - consent = self.queryset.filter(external_id=pk).first() - - if not consent: - return Response(status=status.HTTP_404_NOT_FOUND) - - for artefact in consent.consent_artefacts.all(): - response = Gateway().consents__fetch(str(artefact.artefact_id)) - - if response.status_code != 202: - return Response(response.json(), status=response.status_code) - - return Response( - ConsentRequestSerializer(consent).data, status=status.HTTP_200_OK - ) - - -class ConsentCallbackViewSet(GenericViewSet): - authentication_classes = [ABDMAuthentication] - - def consent_request__on_init(self, request): - data = request.data - consent = ConsentRequest.objects.filter( - external_id=data["resp"]["requestId"] - ).first() - - if not consent: - return Response(status=status.HTTP_404_NOT_FOUND) - - consent.consent_id = data["consentRequest"]["id"] - consent.save() - - return Response(status=status.HTTP_202_ACCEPTED) - - def consent_request__on_status(self, request): - data = request.data - consent = ConsentRequest.objects.filter( - consent_id=data["consentRequest"]["id"] - ).first() - - if not consent: - return Response(status=status.HTTP_404_NOT_FOUND) - - if "notification" not in data: - return Response(status=status.HTTP_202_ACCEPTED) - - if data["notification"]["status"] != Status.DENIED: - consent_artefacts = data["notification"]["consentArtefacts"] or [] - for artefact in consent_artefacts: - consent_artefact = ConsentArtefact.objects.filter( - external_id=artefact["id"] - ).first() - if not consent_artefact: - consent_artefact = ConsentArtefact( - external_id=artefact["id"], - consent_request=consent, - **consent.consent_details_dict(), - ) - - consent_artefact.status = data["notification"]["status"] - consent_artefact.save() - consent.status = data["notification"]["status"] - consent.save() - - return Response(status=status.HTTP_202_ACCEPTED) - - def consents__hiu__notify(self, request): - data = request.data - - if not data["notification"]["consentRequestId"]: - for artefact in data["notification"]["consentArtefacts"]: - consent_artefact = ConsentArtefact.objects.filter( - external_id=artefact["id"] - ).first() - - consent_artefact.status = Status.REVOKED - consent_artefact.save() - return Response(status=status.HTTP_202_ACCEPTED) - - consent = ConsentRequest.objects.filter( - consent_id=data["notification"]["consentRequestId"] - ).first() - - if not consent: - return Response(status=status.HTTP_404_NOT_FOUND) - - if data["notification"]["status"] != Status.DENIED: - consent_artefacts = data["notification"]["consentArtefacts"] or [] - for artefact in consent_artefacts: - consent_artefact = ConsentArtefact.objects.filter( - external_id=artefact["id"] - ).first() - if not consent_artefact: - consent_artefact = ConsentArtefact( - external_id=artefact["id"], - consent_request=consent, - **consent.consent_details_dict(), - ) - - consent_artefact.status = data["notification"]["status"] - consent_artefact.save() - consent.status = data["notification"]["status"] - consent.save() - - Gateway().consents__hiu__on_notify(consent, data["requestId"]) - - if data["notification"]["status"] == Status.GRANTED: - ConsentViewSet().fetch(request, consent.external_id) - - return Response(status=status.HTTP_202_ACCEPTED) - - def consents__on_fetch(self, request): - data = request.data["consent"] - artefact = ConsentArtefact.objects.filter( - external_id=data["consentDetail"]["consentId"] - ).first() - - if not artefact: - return Response(status=status.HTTP_404_NOT_FOUND) - - artefact.hip = data["consentDetail"]["hip"]["id"] - artefact.hiu = data["consentDetail"]["hiu"]["id"] - artefact.cm = data["consentDetail"]["consentManager"]["id"] - - artefact.care_contexts = data["consentDetail"]["careContexts"] - artefact.hi_types = data["consentDetail"]["hiTypes"] - - artefact.access_mode = data["consentDetail"]["permission"]["accessMode"] - artefact.from_time = data["consentDetail"]["permission"]["dateRange"]["from"] - artefact.to_time = data["consentDetail"]["permission"]["dateRange"]["to"] - artefact.expiry = data["consentDetail"]["permission"]["dataEraseAt"] - - artefact.frequency_unit = data["consentDetail"]["permission"]["frequency"][ - "unit" - ] - artefact.frequency_value = data["consentDetail"]["permission"]["frequency"][ - "value" - ] - artefact.frequency_repeats = data["consentDetail"]["permission"]["frequency"][ - "repeats" - ] - - artefact.signature = data["signature"] - artefact.save() - - HealthInformationViewSet().request(request, artefact.external_id) - - return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/health_facility.py b/care/abdm/api/viewsets/health_facility.py deleted file mode 100644 index 8b1acbeab0..0000000000 --- a/care/abdm/api/viewsets/health_facility.py +++ /dev/null @@ -1,117 +0,0 @@ -import re - -from celery import shared_task -from django.conf import settings -from dry_rest_permissions.generics import DRYPermissions -from rest_framework.decorators import action -from rest_framework.mixins import ( - CreateModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, -) -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.abdm.api.serializers.health_facility import HealthFacilitySerializer -from care.abdm.models import HealthFacility -from care.abdm.utils.api_call import Facility -from care.utils.queryset.facility import get_facility_queryset - - -@shared_task -def register_health_facility_as_service(facility_external_id): - if settings.ENABLE_ABDM: - return [False, "ABDM Services are currently disabled"] - - health_facility = HealthFacility.objects.filter( - facility__external_id=facility_external_id - ).first() - - if not health_facility: - return [False, "Health Facility Not Found"] - - if health_facility.registered: - return [True, None] - - clean_facility_name = re.sub(r"[^A-Za-z0-9 ]+", " ", health_facility.facility.name) - clean_facility_name = re.sub(r"\s+", " ", clean_facility_name).strip() - hip_name = settings.HIP_NAME_PREFIX + clean_facility_name + settings.HIP_NAME_SUFFIX - response = Facility().add_update_service( - { - "facilityId": health_facility.hf_id, - "facilityName": hip_name, - "HRP": [ - { - "bridgeId": settings.ABDM_CLIENT_ID, - "hipName": hip_name, - "type": "HIP", - "active": True, - "alias": ["CARE_HIP"], - } - ], - } - ) - - if response.status_code == 200: - data = response.json()[0] - - if "error" in data: - if ( - data["error"].get("code") == "2500" - and settings.ABDM_CLIENT_ID in data["error"].get("message") - and "already associated" in data["error"].get("message") - ): - health_facility.registered = True - health_facility.save() - return [True, None] - - return [ - False, - data["error"].get("message", "Error while registering HIP as service"), - ] - - if "servicesLinked" in data: - health_facility.registered = True - health_facility.save() - return [True, None] - - return [False, None] - - -class HealthFacilityViewSet( - GenericViewSet, - CreateModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, -): - serializer_class = HealthFacilitySerializer - model = HealthFacility - queryset = HealthFacility.objects.all() - permission_classes = (IsAuthenticated, DRYPermissions) - lookup_field = "facility__external_id" - - def get_queryset(self): - queryset = self.queryset - facilities = get_facility_queryset(self.request.user) - return queryset.filter(facility__in=facilities) - - @action(detail=True, methods=["POST"]) - def register_service(self, request, facility__external_id): - [registered, error] = register_health_facility_as_service(facility__external_id) - - if error: - return Response({"detail": error}, status=400) - - return Response({"registered": registered}) - - def perform_create(self, serializer): - instance = serializer.save() - register_health_facility_as_service.delay(instance.facility.external_id) - - def perform_update(self, serializer): - serializer.validated_data["registered"] = False - instance = serializer.save() - register_health_facility_as_service.delay(instance.facility.external_id) diff --git a/care/abdm/api/viewsets/health_information.py b/care/abdm/api/viewsets/health_information.py deleted file mode 100644 index 98a2825276..0000000000 --- a/care/abdm/api/viewsets/health_information.py +++ /dev/null @@ -1,144 +0,0 @@ -import json -import logging - -from django.db.models import Q -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.abdm.models.consent import ConsentArtefact -from care.abdm.service.gateway import Gateway -from care.abdm.utils.cipher import Cipher -from care.facility.models.file_upload import FileUpload -from config.auth_views import CaptchaRequiredException -from config.authentication import ABDMAuthentication -from config.ratelimit import USER_READABLE_RATE_LIMIT_TIME, ratelimit - -logger = logging.getLogger(__name__) - - -class HealthInformationViewSet(GenericViewSet): - - def retrieve(self, request, pk): - files = FileUpload.objects.filter( - Q(internal_name=f"{pk}.json") | Q(associating_id=pk), - file_type=FileUpload.FileType.ABDM_HEALTH_INFORMATION.value, - ) - - if files.count() == 0 or all([not file.upload_completed for file in files]): - return Response( - {"detail": "No Health Information found with the given id"}, - status=status.HTTP_404_NOT_FOUND, - ) - - if files.count() == 1: - file = files.first() - - if file.is_archived: - return Response( - { - "is_archived": True, - "archived_reason": file.archive_reason, - "archived_time": file.archived_datetime, - "detail": f"This file has been archived as {file.archive_reason} at {file.archived_datetime}", - }, - status=status.HTTP_404_NOT_FOUND, - ) - - contents = [] - for file in files: - if file.upload_completed: - content_type, content = file.file_contents() - contents.extend(content) - - return Response({"data": json.loads(content)}, status=status.HTTP_200_OK) - - @action(detail=True, methods=["POST"]) - def request(self, request, pk): - if ratelimit(request, "health_information__request", [pk]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - artefact = ConsentArtefact.objects.filter(external_id=pk).first() - - if not artefact: - return Response( - {"detail": "No Consent artefact found with the given id"}, - status=status.HTTP_404_NOT_FOUND, - ) - - response = Gateway().health_information__cm__request(artefact) - if response.status_code != 202: - return Response(response.json(), status=response.status_code) - - return Response(status=status.HTTP_200_OK) - - -class HealthInformationCallbackViewSet(GenericViewSet): - authentication_classes = [ABDMAuthentication] - - def health_information__hiu__on_request(self, request): - data = request.data - - artefact = ConsentArtefact.objects.filter( - consent_id=data["resp"]["requestId"] - ).first() - - if not artefact: - return Response(status=status.HTTP_404_NOT_FOUND) - - if "hiRequest" in data: - artefact.consent_id = data["hiRequest"]["transactionId"] - artefact.save() - - return Response(status=status.HTTP_202_ACCEPTED) - - def health_information__transfer(self, request): - data = request.data - - artefact = ConsentArtefact.objects.filter( - consent_id=data["transactionId"] - ).first() - - if not artefact: - return Response(status=status.HTTP_404_NOT_FOUND) - - cipher = Cipher( - data["keyMaterial"]["dhPublicKey"]["keyValue"], - data["keyMaterial"]["nonce"], - artefact.key_material_private_key, - artefact.key_material_public_key, - artefact.key_material_nonce, - ) - entries = [] - for entry in data["entries"]: - if "content" in entry: - entries.append( - { - "content": cipher.decrypt(entry["content"]), - "care_context_reference": entry["careContextReference"], - } - ) - - if "link" in entry: - # TODO: handle link - pass - - file = FileUpload( - internal_name=f"{artefact.external_id}.json", - file_type=FileUpload.FileType.ABDM_HEALTH_INFORMATION.value, - associating_id=artefact.consent_request.external_id, - ) - file.put_object(json.dumps(entries), ContentType="application/json") - file.upload_completed = True - file.save() - - Gateway().health_information__notify(artefact) - - return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py deleted file mode 100644 index 347f1a01b2..0000000000 --- a/care/abdm/api/viewsets/healthid.py +++ /dev/null @@ -1,765 +0,0 @@ -# ABDM HealthID APIs - -import logging -from datetime import datetime - -from drf_spectacular.utils import extend_schema -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.exceptions import ValidationError -from rest_framework.mixins import CreateModelMixin -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.abdm.api.serializers.abha_number import AbhaNumberSerializer -from care.abdm.api.serializers.healthid import ( - AadharOtpGenerateRequestPayloadSerializer, - AadharOtpResendRequestPayloadSerializer, - CreateHealthIdSerializer, - GenerateMobileOtpRequestPayloadSerializer, - HealthIdAuthSerializer, - HealthIdSerializer, - LinkPatientSerializer, - QRContentSerializer, - VerifyDemographicsRequestPayloadSerializer, - VerifyOtpRequestPayloadSerializer, -) -from care.abdm.models import AbhaNumber -from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway -from care.facility.api.serializers.patient import PatientDetailSerializer -from care.facility.models.patient import PatientConsultation, PatientRegistration -from care.utils.queryset.patient import get_patient_queryset -from config.auth_views import CaptchaRequiredException -from config.ratelimit import USER_READABLE_RATE_LIMIT_TIME, ratelimit - -logger = logging.getLogger(__name__) - - -# API for Generating OTP for HealthID -class ABDMHealthIDViewSet(GenericViewSet, CreateModelMixin): - base_name = "healthid" - model = AbhaNumber - - @extend_schema( - operation_id="generate_aadhaar_otp", - request=AadharOtpGenerateRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def generate_aadhaar_otp(self, request): - data = request.data - - if ratelimit(request, "generate_aadhaar_otp", [data["aadhaar"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = AadharOtpGenerateRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().generate_aadhaar_otp(data) - return Response(response, status=status.HTTP_200_OK) - - @extend_schema( - # /v1/registration/aadhaar/resendAadhaarOtp - operation_id="resend_aadhaar_otp", - request=AadharOtpResendRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def resend_aadhaar_otp(self, request): - data = request.data - - if ratelimit(request, "resend_aadhaar_otp", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = AadharOtpResendRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().resend_aadhaar_otp(data) - return Response(response, status=status.HTTP_200_OK) - - @extend_schema( - # /v1/registration/aadhaar/verifyAadhaarOtp - operation_id="verify_aadhaar_otp", - request=VerifyOtpRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def verify_aadhaar_otp(self, request): - data = request.data - - if ratelimit(request, "verify_aadhaar_otp", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = VerifyOtpRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().verify_aadhaar_otp( - data - ) # HealthIdGatewayV2().verify_document_mobile_otp(data) - return Response(response, status=status.HTTP_200_OK) - - @extend_schema( - # /v1/registration/aadhaar/generateMobileOTP - operation_id="generate_mobile_otp", - request=GenerateMobileOtpRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def generate_mobile_otp(self, request): - data = request.data - - if ratelimit(request, "generate_mobile_otp", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = GenerateMobileOtpRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().generate_mobile_otp(data) - return Response(response, status=status.HTTP_200_OK) - - @extend_schema( - # /v1/registration/aadhaar/verifyMobileOTP - operation_id="verify_mobile_otp", - request=VerifyOtpRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def verify_mobile_otp(self, request): - data = request.data - - if ratelimit(request, "verify_mobile_otp", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = VerifyOtpRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().verify_mobile_otp(data) - return Response(response, status=status.HTTP_200_OK) - - def create_abha(self, abha_profile, token): - abha_object = AbhaNumber.objects.filter( - abha_number=abha_profile["healthIdNumber"] - ).first() - - if abha_object: - return abha_object - - abha_object = AbhaNumber.objects.create( - abha_number=abha_profile["healthIdNumber"], - health_id=abha_profile["healthId"], - name=abha_profile["name"], - first_name=abha_profile["firstName"], - middle_name=abha_profile["middleName"], - last_name=abha_profile["lastName"], - gender=abha_profile["gender"], - date_of_birth=str( - datetime.strptime( - f"{abha_profile['yearOfBirth']}-{abha_profile['monthOfBirth']}-{abha_profile['dayOfBirth']}", - "%Y-%m-%d", - ) - )[0:10], - address=abha_profile["address"] if "address" in abha_profile else "", - district=abha_profile["districtName"], - state=abha_profile["stateName"], - pincode=abha_profile["pincode"], - email=abha_profile["email"], - profile_photo=abha_profile["profilePhoto"], - new=abha_profile["new"], - txn_id=token["txn_id"], - access_token=token["access_token"], - refresh_token=token["refresh_token"], - ) - abha_object.save() - - return abha_object - - def add_abha_details_to_patient(self, abha_object, patient_object): - if abha_object.patient is not None: - raise ValidationError(detail="Abha Number is already linked to a patient") - - if getattr(patient_object, "abha_number", None) is not None: - raise ValidationError(detail="Patient already has an Abha Number linked") - - abha_object.patient = patient_object - abha_object.save() - - @extend_schema( - # /v1/registration/aadhaar/createHealthId - operation_id="create_health_id", - request=CreateHealthIdSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def create_health_id(self, request): - data = request.data - - if ratelimit(request, "create_health_id", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = CreateHealthIdSerializer(data=data) - serializer.is_valid(raise_exception=True) - abha_profile = HealthIdGateway().create_health_id(data) - - if "token" not in abha_profile: - raise ValidationError( - detail="\n\n".join( - detail.get("message", "") - for detail in abha_profile.get("details", []) - ) - or abha_profile.get("message", "Error while fetching abha profile") - ) - - # have a serializer to verify data of abha_profile - abha_object = self.create_abha( - abha_profile, - { - "txn_id": data["txnId"], - "access_token": abha_profile["token"], - "refresh_token": abha_profile["refreshToken"], - }, - ) - - if "patientId" in data: - patient_id = data.pop("patientId") - allowed_patients = get_patient_queryset(request.user) - patient_obj = allowed_patients.filter(external_id=patient_id).first() - if not patient_obj: - raise ValidationError(detail="Patient not found") - - self.add_abha_details_to_patient(abha_object, patient_obj) - - return Response( - {"id": abha_object.external_id, "abha_profile": abha_profile}, - status=status.HTTP_200_OK, - ) - - # APIs to Find & Link Existing HealthID - # searchByHealthId - @extend_schema( - # /v1/registration/aadhaar/searchByHealthId - operation_id="search_by_health_id", - request=HealthIdSerializer, - responses={"200": "{'status': 'boolean'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def search_by_health_id(self, request): - data = request.data - - if ratelimit( - request, "search_by_health_id", [data["healthId"]], increment=False - ): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = HealthIdSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().search_by_health_id(data) - return Response(response, status=status.HTTP_200_OK) - - @action(detail=False, methods=["post"]) - def get_abha_card(self, request): - data = request.data - - if ratelimit(request, "get_abha_card", [data["patient"]], increment=False): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - allowed_patients = get_patient_queryset(request.user) - patient = allowed_patients.filter(external_id=data["patient"]).first() - if not patient: - raise ValidationError(detail="Patient not found") - - if getattr(patient, "abha_number", None) is None: - raise ValidationError(detail="Patient hasn't linked thier abha") - - if data["type"] == "png": - response = HealthIdGateway().get_abha_card_png( - {"refreshToken": patient.abha_number.refresh_token} - ) - return Response(response, status=status.HTTP_200_OK) - - response = HealthIdGateway().get_abha_card_pdf( - {"refreshToken": patient.abha_number.refresh_token} - ) - return Response(response, status=status.HTTP_200_OK) - - @extend_schema( - # /v1/registration/aadhaar/searchByHealthId - operation_id="link_via_qr", - request=HealthIdSerializer, - responses={"200": "{'status': 'boolean'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def link_via_qr(self, request): - data = request.data - - if ratelimit(request, "link_via_qr", [data["hidn"]], increment=False): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = QRContentSerializer(data=data) - serializer.is_valid(raise_exception=True) - - dob = datetime.strptime(data["dob"], "%d-%m-%Y").date() - - patient = PatientRegistration.objects.filter( - abha_number__abha_number=data["hidn"] - ).first() - if patient: - return Response( - { - "message": "A patient is already associated with the provided Abha Number" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - abha_number = AbhaNumber.objects.filter(abha_number=data["hidn"]).first() - - if not abha_number: - abha_number = AbhaNumber.objects.create( - abha_number=data["hidn"], - health_id=data["phr"], - name=data["name"], - gender=data["gender"], - date_of_birth=str(dob)[0:10], - address=data["address"], - district=data["dist name"], - state=data["state name"], - ) - - AbdmGateway().fetch_modes( - { - "healthId": data["phr"] or data["hidn"], - "name": data["name"], - "gender": data["gender"], - "dateOfBirth": str(datetime.strptime(data["dob"], "%d-%m-%Y"))[ - 0:10 - ], - } - ) - - abha_number.save() - - if "patientId" in data and data["patientId"] is not None: - patient = PatientRegistration.objects.filter( - external_id=data["patientId"] - ).first() - - if not patient: - return Response( - {"message": "Enter a valid patientId"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - abha_number.patient = patient - abha_number.save() - - abha_serialized = AbhaNumberSerializer(abha_number).data - return Response( - {"id": abha_serialized["external_id"], "abha_profile": abha_serialized}, - status=status.HTTP_200_OK, - ) - - @extend_schema( - operation_id="search_by_health_id", - request=LinkPatientSerializer, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def link_patient(self, request): - data = request.data - - serializer = LinkPatientSerializer(data=data) - serializer.is_valid(raise_exception=True) - - patient_queryset = get_patient_queryset(request.user) - patient = patient_queryset.filter(external_id=data.get("patient")).first() - - if not patient: - return Response( - { - "detail": "Patient not found or you do not have permission to access the patient", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if hasattr(patient, "abha_number"): - return Response( - { - "detail": "Patient already linked to an ABHA Number", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - abha_number = AbhaNumber.objects.filter( - external_id=data.get("abha_number") - ).first() - - if not abha_number: - return Response( - { - "detail": "ABHA Number not found", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if abha_number.patient is not None: - return Response( - { - "detail": "ABHA Number already linked to a patient", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - abha_number.patient = patient - abha_number.save() - - return Response( - AbhaNumberSerializer(abha_number).data, - status=status.HTTP_200_OK, - ) - - @extend_schema( - operation_id="get_new_linking_token", - responses={"200": "{'status': 'boolean'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def get_new_linking_token(self, request): - data = request.data - - if ratelimit(request, "get_new_linking_token", [data["patient"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - patient = PatientDetailSerializer( - PatientRegistration.objects.get(external_id=data["patient"]) - ).data - - AbdmGateway().fetch_modes( - { - "healthId": patient["abha_number_object"]["abha_number"], - "name": patient["abha_number_object"]["name"], - "gender": patient["abha_number_object"]["gender"], - "dateOfBirth": str(patient["abha_number_object"]["date_of_birth"]), - } - ) - - return Response({}, status=status.HTTP_200_OK) - - @action(detail=False, methods=["POST"]) - def add_care_context(self, request, *args, **kwargs): - consultation_id = request.data["consultation"] - - if ratelimit(request, "add_care_context", [consultation_id]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - consultation = PatientConsultation.objects.get(external_id=consultation_id) - - if not consultation: - raise ValidationError(detail="Consultation not found") - - if getattr(consultation.patient, "abha_number", None) is None: - raise ValidationError(detail="Patient hasn't linked thier abha") - - AbdmGateway().fetch_modes( - { - "healthId": consultation.patient.abha_number.health_id, - "name": ( - request.data["name"] - if "name" in request.data - else consultation.patient.abha_number.name - ), - "gender": ( - request.data["gender"] - if "gender" in request.data - else consultation.patient.abha_number.gender - ), - "dateOfBirth": ( - request.data["dob"] - if "dob" in request.data - else str(consultation.patient.abha_number.date_of_birth) - ), - "consultationId": consultation_id, - # "authMode": "DIRECT", - "purpose": "LINK", - } - ) - - return Response(status=status.HTTP_202_ACCEPTED) - - @action(detail=False, methods=["POST"]) - def patient_sms_notify(self, request, *args, **kwargs): - patient_id = request.data["patient"] - - if ratelimit(request, "patient_sms_notify", [patient_id]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - patient = PatientRegistration.objects.filter(external_id=patient_id).first() - - if not patient: - return Response( - {"patient": "No matching records found"}, - status=status.HTTP_404_NOT_FOUND, - ) - - if getattr(patient, "abha_number", None) is None: - return Response( - {"abha": "Patient hasn't linked thier abha"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - response = AbdmGateway().patient_sms_notify( - { - "phone": patient.phone_number, - "healthId": patient.abha_number.health_id, - } - ) - - return Response(response, status=status.HTTP_202_ACCEPTED) - - # auth/init - @extend_schema( - # /v1/auth/init - operation_id="auth_init", - request=HealthIdAuthSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def auth_init(self, request): - data = request.data - - if ratelimit(request, "auth_init", [data["healthid"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = HealthIdAuthSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().auth_init(data) - return Response(response, status=status.HTTP_200_OK) - - # /v1/auth/confirmWithAadhaarOtp - @extend_schema( - operation_id="confirm_with_aadhaar_otp", - request=VerifyOtpRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def confirm_with_aadhaar_otp(self, request): - data = request.data - - if ratelimit(request, "confirm_with_aadhaar_otp", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = VerifyOtpRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().confirm_with_aadhaar_otp(data) - abha_profile = HealthIdGateway().get_profile(response) - - # have a serializer to verify data of abha_profile - abha_object = self.create_abha( - abha_profile, - { - "access_token": response["token"], - "refresh_token": response["refreshToken"], - "txn_id": data["txnId"], - }, - ) - - if "patientId" in data: - patient_id = data.pop("patientId") - allowed_patients = get_patient_queryset(request.user) - patient_obj = allowed_patients.filter(external_id=patient_id).first() - if not patient_obj: - raise ValidationError(detail="Patient not found") - - self.add_abha_details_to_patient(abha_object, patient_obj) - - return Response( - {"id": abha_object.external_id, "abha_profile": abha_profile}, - status=status.HTTP_200_OK, - ) - - # /v1/auth/confirmWithMobileOtp - @extend_schema( - operation_id="confirm_with_mobile_otp", - request=VerifyOtpRequestPayloadSerializer, - # responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def confirm_with_mobile_otp(self, request): - data = request.data - - if ratelimit(request, "confirm_with_mobile_otp", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = VerifyOtpRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().confirm_with_mobile_otp(data) - abha_profile = HealthIdGateway().get_profile(response) - - # have a serializer to verify data of abha_profile - abha_object = self.create_abha( - abha_profile, - { - "access_token": response["token"], - "refresh_token": response["refreshToken"], - "txn_id": data["txnId"], - }, - ) - - if "patientId" in data: - patient_id = data.pop("patientId") - allowed_patients = get_patient_queryset(request.user) - patient_obj = allowed_patients.filter(external_id=patient_id).first() - if not patient_obj: - raise ValidationError(detail="Patient not found") - - self.add_abha_details_to_patient(abha_object, patient_obj) - - return Response( - {"id": abha_object.external_id, "abha_profile": abha_profile}, - status=status.HTTP_200_OK, - ) - - @extend_schema( - operation_id="confirm_with_demographics", - request=VerifyDemographicsRequestPayloadSerializer, - responses={"200": "{'status': true}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def confirm_with_demographics(self, request): - data = request.data - - if ratelimit(request, "confirm_with_demographics", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = VerifyDemographicsRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().confirm_with_demographics(data) - return Response(response, status=status.HTTP_200_OK) - - ############################################################################################################ - # HealthID V2 APIs - @extend_schema( - # /v2/registration/aadhaar/checkAndGenerateMobileOTP - operation_id="check_and_generate_mobile_otp", - request=GenerateMobileOtpRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID V2"], - ) - @action(detail=False, methods=["post"]) - def check_and_generate_mobile_otp(self, request): - data = request.data - - if ratelimit(request, "check_and_generate_mobile_otp", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = GenerateMobileOtpRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().check_and_generate_mobile_otp(data) - return Response(response, status=status.HTTP_200_OK) diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py deleted file mode 100644 index aa6abb5b1e..0000000000 --- a/care/abdm/api/viewsets/hip.py +++ /dev/null @@ -1,146 +0,0 @@ -import uuid -from datetime import UTC, datetime - -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.abdm.api.serializers.hip import HipShareProfileSerializer -from care.abdm.models import AbhaNumber -from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway -from care.facility.models.facility import Facility -from care.facility.models.patient import PatientRegistration -from config.authentication import ABDMAuthentication - - -class HipViewSet(GenericViewSet): - authentication_classes = [ABDMAuthentication] - - def get_linking_token(self, data): - AbdmGateway().fetch_modes(data) - return True - - @action(detail=False, methods=["POST"]) - def share(self, request, *args, **kwargs): - data = request.data - - patient_data = data["profile"]["patient"] - counter_id = ( - data["profile"]["hipCode"] - if len(data["profile"]["hipCode"]) == 36 - else Facility.objects.first().external_id - ) - - patient_data["mobile"] = "" - for identifier in patient_data["identifiers"]: - if identifier["type"] == "MOBILE": - patient_data["mobile"] = identifier["value"] - - serializer = HipShareProfileSerializer(data=data) - serializer.is_valid(raise_exception=True) - - if HealthIdGateway().verify_demographics( - patient_data["healthIdNumber"], - patient_data["name"], - patient_data["gender"], - patient_data["yearOfBirth"], - ): - patient = PatientRegistration.objects.filter( - abha_number__abha_number=patient_data["healthIdNumber"] - ).first() - - if not patient: - patient = PatientRegistration.objects.create( - facility=Facility.objects.get(external_id=counter_id), - name=patient_data["name"], - gender={"M": 1, "F": 2}.get(patient_data["gender"], 3), - is_antenatal=False, - phone_number=patient_data["mobile"], - emergency_phone_number=patient_data["mobile"], - date_of_birth=datetime.strptime( - f"{patient_data['yearOfBirth']}-{patient_data['monthOfBirth']}-{patient_data['dayOfBirth']}", - "%Y-%m-%d", - ).date(), - blood_group="UNK", - nationality="India", - address=patient_data["address"]["line"], - pincode=patient_data["address"]["pincode"], - ) - - abha_number = AbhaNumber.objects.create( - abha_number=patient_data["healthIdNumber"], - health_id=patient_data["healthId"], - name=patient_data["name"], - gender=patient_data["gender"], - date_of_birth=str( - datetime.strptime( - f"{patient_data['yearOfBirth']}-{patient_data['monthOfBirth']}-{patient_data['dayOfBirth']}", - "%Y-%m-%d", - ) - )[0:10], - address=patient_data["address"]["line"], - district=patient_data["address"]["district"], - state=patient_data["address"]["state"], - pincode=patient_data["address"]["pincode"], - ) - - try: - self.get_linking_token( - { - "healthId": patient_data["healthId"] - or patient_data["healthIdNumber"], - "name": patient_data["name"], - "gender": patient_data["gender"], - "dateOfBirth": str( - datetime.strptime( - f"{patient_data['yearOfBirth']}-{patient_data['monthOfBirth']}-{patient_data['dayOfBirth']}", - "%Y-%m-%d", - ) - )[0:10], - } - ) - except Exception: - return Response( - { - "status": "FAILED", - "healthId": patient_data["healthId"] - or patient_data["healthIdNumber"], - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - abha_number.patient = patient - abha_number.save() - - payload = { - "requestId": str(uuid.uuid4()), - "timestamp": str( - datetime.now(tz=UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z") - ), - "acknowledgement": { - "status": "SUCCESS", - "healthId": patient_data["healthId"] - or patient_data["healthIdNumber"], - "tokenNumber": "100", - }, - "error": None, - "resp": { - "requestId": data["requestId"], - }, - } - - on_share_response = AbdmGateway().on_share(payload) - if on_share_response.status_code == 202: - return Response( - on_share_response.request.body, - status=status.HTTP_202_ACCEPTED, - ) - - return Response( - { - "status": "ACCEPTED", - "healthId": patient_data["healthId"] or patient_data["healthIdNumber"], - }, - status=status.HTTP_202_ACCEPTED, - ) diff --git a/care/abdm/api/viewsets/monitoring.py b/care/abdm/api/viewsets/monitoring.py deleted file mode 100644 index b1ee830398..0000000000 --- a/care/abdm/api/viewsets/monitoring.py +++ /dev/null @@ -1,22 +0,0 @@ -from datetime import UTC, datetime - -from rest_framework import status -from rest_framework.generics import GenericAPIView -from rest_framework.response import Response - - -class HeartbeatView(GenericAPIView): - permission_classes = () - authentication_classes = () - - def get(self, request, *args, **kwargs): - return Response( - { - "timestamp": str( - datetime.now(tz=UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z") - ), - "status": "UP", - "error": None, - }, - status=status.HTTP_200_OK, - ) diff --git a/care/abdm/api/viewsets/patients.py b/care/abdm/api/viewsets/patients.py deleted file mode 100644 index e29a72487f..0000000000 --- a/care/abdm/api/viewsets/patients.py +++ /dev/null @@ -1,77 +0,0 @@ -import json - -from django.core.cache import cache -from django.db.models import Q -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.abdm.models.abha_number import AbhaNumber -from care.abdm.service.gateway import Gateway -from care.utils.notification_handler import send_webpush -from config.auth_views import CaptchaRequiredException -from config.authentication import ABDMAuthentication -from config.ratelimit import USER_READABLE_RATE_LIMIT_TIME, ratelimit - - -class PatientsViewSet(GenericViewSet): - - @action(detail=False, methods=["POST"]) - def find(self, request): - identifier = request.data["id"] - - if ratelimit(request, "patients__find", [identifier]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - abha_object = AbhaNumber.objects.filter( - Q(abha_number=identifier) | Q(health_id=identifier) - ).first() - - if not abha_object: - return Response( - {"error": "Patient with given id not found"}, - status=status.HTTP_404_NOT_FOUND, - ) - - response = Gateway().patients__find(abha_object) - if response.status_code != 202: - return Response(response.text, status=status.HTTP_400_BAD_REQUEST) - - cache.set( - f"abdm__patients__find__{json.loads(response.request.body)['requestId']}", - request.user.username, - timeout=60 * 60, - ) - return Response( - {"detail": "Requested ABDM for patient details"}, status=status.HTTP_200_OK - ) - - -class PatientsCallbackViewSet(GenericViewSet): - authentication_classes = [ABDMAuthentication] - - def patients__on_find(self, request): - username = cache.get( - f"abdm__patients__find__{request.data['resp']['requestId']}" - ) - - if username: - send_webpush( - username=username, - message=json.dumps( - { - "type": "MESSAGE", - "from": "patients/on_find", - "message": request.data, - } - ), - ) - - return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/status.py b/care/abdm/api/viewsets/status.py deleted file mode 100644 index 72913c847a..0000000000 --- a/care/abdm/api/viewsets/status.py +++ /dev/null @@ -1,33 +0,0 @@ -from rest_framework import status -from rest_framework.generics import GenericAPIView -from rest_framework.response import Response - -from care.abdm.models import AbhaNumber -from care.abdm.utils.api_call import AbdmGateway -from care.facility.models.patient import PatientRegistration -from config.authentication import ABDMAuthentication - - -class NotifyView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - PatientRegistration.objects.filter( - abha_number__health_id=data["notification"]["patient"]["id"] - ).update(abha_number=None) - AbhaNumber.objects.filter( - health_id=data["notification"]["patient"]["id"] - ).delete() - - AbdmGateway().patient_status_on_notify({"request_id": data["requestId"]}) - - return Response(status=status.HTTP_202_ACCEPTED) - - -class SMSOnNotifyView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/apps.py b/care/abdm/apps.py deleted file mode 100644 index 54e278d631..0000000000 --- a/care/abdm/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig -from django.utils.translation import gettext_lazy as _ - - -class AbdmConfig(AppConfig): - name = "care.abdm" - verbose_name = _("ABDM Integration") diff --git a/care/abdm/migrations/0001_initial_squashed_0007_alter_abhanumber_id.py b/care/abdm/migrations/0001_initial_squashed_0007_alter_abhanumber_id.py deleted file mode 100644 index ad5d70caa0..0000000000 --- a/care/abdm/migrations/0001_initial_squashed_0007_alter_abhanumber_id.py +++ /dev/null @@ -1,69 +0,0 @@ -# Generated by Django 4.2.2 on 2023-07-20 17:41 - -import uuid - -from django.db import migrations, models - - -class Migration(migrations.Migration): - replaces = [ - ("abdm", "0001_initial"), - ("abdm", "0002_auto_20221220_2312"), - ("abdm", "0003_auto_20221220_2321"), - ("abdm", "0004_auto_20221220_2325"), - ("abdm", "0005_auto_20221220_2327"), - ("abdm", "0006_auto_20230208_0915"), - ("abdm", "0007_alter_abhanumber_id"), - ] - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="AbhaNumber", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ("abha_number", models.TextField(blank=True, null=True)), - ("email", models.EmailField(blank=True, max_length=254, null=True)), - ("first_name", models.TextField(blank=True, null=True)), - ("health_id", models.TextField(blank=True, null=True)), - ("last_name", models.TextField(blank=True, null=True)), - ("middle_name", models.TextField(blank=True, null=True)), - ("profile_photo", models.TextField(blank=True, null=True)), - ("txn_id", models.TextField(blank=True, null=True)), - ("access_token", models.TextField(blank=True, null=True)), - ("refresh_token", models.TextField(blank=True, null=True)), - ("address", models.TextField(blank=True, null=True)), - ("date_of_birth", models.TextField(blank=True, null=True)), - ("district", models.TextField(blank=True, null=True)), - ("gender", models.TextField(blank=True, null=True)), - ("name", models.TextField(blank=True, null=True)), - ("pincode", models.TextField(blank=True, null=True)), - ("state", models.TextField(blank=True, null=True)), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/care/abdm/migrations/0008_abhanumber_new.py b/care/abdm/migrations/0008_abhanumber_new.py deleted file mode 100644 index 74fdc32e78..0000000000 --- a/care/abdm/migrations/0008_abhanumber_new.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.2 on 2023-08-07 07:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("abdm", "0001_initial_squashed_0007_alter_abhanumber_id"), - ] - - operations = [ - migrations.AddField( - model_name="abhanumber", - name="new", - field=models.BooleanField(default=False), - ), - ] diff --git a/care/abdm/migrations/0009_healthfacility.py b/care/abdm/migrations/0009_healthfacility.py deleted file mode 100644 index 0480e91d9a..0000000000 --- a/care/abdm/migrations/0009_healthfacility.py +++ /dev/null @@ -1,55 +0,0 @@ -# Generated by Django 4.2.2 on 2023-08-21 09:53 - -import uuid - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("facility", "0378_consultationbedasset_consultationbed_assets"), - ("abdm", "0008_abhanumber_new"), - ] - - operations = [ - migrations.CreateModel( - name="HealthFacility", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ("hf_id", models.CharField(max_length=50, unique=True)), - ( - "facility", - models.OneToOneField( - on_delete=django.db.models.deletion.PROTECT, - to="facility.facility", - to_field="external_id", - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/care/abdm/migrations/0010_healthfacility_registered.py b/care/abdm/migrations/0010_healthfacility_registered.py deleted file mode 100644 index 5a5d753925..0000000000 --- a/care/abdm/migrations/0010_healthfacility_registered.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.2 on 2023-09-05 06:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("abdm", "0009_healthfacility"), - ] - - operations = [ - migrations.AddField( - model_name="healthfacility", - name="registered", - field=models.BooleanField(default=False), - ), - ] diff --git a/care/abdm/migrations/0011_alter_abhanumber_abha_number_and_more.py b/care/abdm/migrations/0011_alter_abhanumber_abha_number_and_more.py deleted file mode 100644 index 905cd09719..0000000000 --- a/care/abdm/migrations/0011_alter_abhanumber_abha_number_and_more.py +++ /dev/null @@ -1,429 +0,0 @@ -# Generated by Django 4.2.2 on 2023-10-01 16:44 - -import uuid - -import django.contrib.postgres.fields -import django.core.validators -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - -import care.abdm.models.consent -import care.utils.models.validators - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("abdm", "0010_healthfacility_registered"), - ] - - operations = [ - migrations.AlterField( - model_name="abhanumber", - name="abha_number", - field=models.TextField(blank=True, null=True, unique=True), - ), - migrations.AlterField( - model_name="abhanumber", - name="health_id", - field=models.TextField(blank=True, null=True, unique=True), - ), - migrations.CreateModel( - name="ConsentRequest", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ("consent_id", models.UUIDField(blank=True, null=True, unique=True)), - ( - "care_contexts", - models.JSONField( - default=list, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "content": [ - { - "additionalProperties": False, - "properties": { - "careContextReference": { - "type": "string" - }, - "patientReference": {"type": "string"}, - }, - "required": [ - "patientReference", - "careContextReference", - ], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - ( - "purpose", - models.CharField( - choices=[ - ("CAREMGT", "Care Management"), - ("BTG", "Break The Glass"), - ("PUBHLTH", "Public Health"), - ("HPAYMT", "Healthcare Payment"), - ("DSRCH", "Disease Specific Healthcare Research"), - ("PATRQT", "Self Requested"), - ], - default="CAREMGT", - max_length=20, - ), - ), - ( - "hi_types", - django.contrib.postgres.fields.ArrayField( - base_field=models.CharField( - choices=[ - ("Prescription", "Prescription"), - ("DiagnosticReport", "Diagnostic Report"), - ("OPConsultation", "Op Consultation"), - ("DischargeSummary", "Discharge Summary"), - ("ImmunizationRecord", "Immunization Record"), - ("HealthDocumentRecord", "Record Artifact"), - ("WellnessRecord", "Wellness Record"), - ], - max_length=20, - ), - default=list, - size=None, - ), - ), - ("hip", models.CharField(blank=True, max_length=50, null=True)), - ("hiu", models.CharField(blank=True, max_length=50, null=True)), - ( - "access_mode", - models.CharField( - choices=[ - ("VIEW", "View"), - ("STORE", "Store"), - ("QUERY", "Query"), - ("STREAM", "Stream"), - ], - default="VIEW", - max_length=20, - ), - ), - ( - "from_time", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_from_time, - null=True, - ), - ), - ( - "to_time", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_to_time, - null=True, - ), - ), - ( - "expiry", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_expiry, - null=True, - ), - ), - ( - "frequency_unit", - models.CharField( - choices=[ - ("HOUR", "Hour"), - ("DAY", "Day"), - ("WEEK", "Week"), - ("MONTH", "Month"), - ("YEAR", "Year"), - ], - default="HOUR", - max_length=20, - ), - ), - ( - "frequency_value", - models.PositiveSmallIntegerField( - default=1, - validators=[django.core.validators.MinValueValidator(1)], - ), - ), - ("frequency_repeats", models.PositiveSmallIntegerField(default=0)), - ( - "patient_abha", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - to="abdm.abhanumber", - to_field="health_id", - ), - ), - ( - "requester", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.CreateModel( - name="ConsentArtefact", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ("consent_id", models.UUIDField(blank=True, null=True, unique=True)), - ( - "care_contexts", - models.JSONField( - default=list, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "content": [ - { - "additionalProperties": False, - "properties": { - "careContextReference": { - "type": "string" - }, - "patientReference": {"type": "string"}, - }, - "required": [ - "patientReference", - "careContextReference", - ], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - ( - "purpose", - models.CharField( - choices=[ - ("CAREMGT", "Care Management"), - ("BTG", "Break The Glass"), - ("PUBHLTH", "Public Health"), - ("HPAYMT", "Healthcare Payment"), - ("DSRCH", "Disease Specific Healthcare Research"), - ("PATRQT", "Self Requested"), - ], - default="CAREMGT", - max_length=20, - ), - ), - ( - "hi_types", - django.contrib.postgres.fields.ArrayField( - base_field=models.CharField( - choices=[ - ("Prescription", "Prescription"), - ("DiagnosticReport", "Diagnostic Report"), - ("OPConsultation", "Op Consultation"), - ("DischargeSummary", "Discharge Summary"), - ("ImmunizationRecord", "Immunization Record"), - ("HealthDocumentRecord", "Record Artifact"), - ("WellnessRecord", "Wellness Record"), - ], - max_length=20, - ), - default=list, - size=None, - ), - ), - ("hip", models.CharField(blank=True, max_length=50, null=True)), - ("hiu", models.CharField(blank=True, max_length=50, null=True)), - ( - "access_mode", - models.CharField( - choices=[ - ("VIEW", "View"), - ("STORE", "Store"), - ("QUERY", "Query"), - ("STREAM", "Stream"), - ], - default="VIEW", - max_length=20, - ), - ), - ( - "from_time", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_from_time, - null=True, - ), - ), - ( - "to_time", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_to_time, - null=True, - ), - ), - ( - "expiry", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_expiry, - null=True, - ), - ), - ( - "frequency_unit", - models.CharField( - choices=[ - ("HOUR", "Hour"), - ("DAY", "Day"), - ("WEEK", "Week"), - ("MONTH", "Month"), - ("YEAR", "Year"), - ], - default="HOUR", - max_length=20, - ), - ), - ( - "frequency_value", - models.PositiveSmallIntegerField( - default=1, - validators=[django.core.validators.MinValueValidator(1)], - ), - ), - ("frequency_repeats", models.PositiveSmallIntegerField(default=0)), - ( - "status", - models.CharField( - choices=[ - ("REQUESTED", "Requested"), - ("GRANTED", "Granted"), - ("DENIED", "Denied"), - ("EXPIRED", "Expired"), - ("REVOKED", "Revoked"), - ], - default="REQUESTED", - max_length=20, - ), - ), - ("cm", models.CharField(blank=True, max_length=50, null=True)), - ( - "key_material_algorithm", - models.CharField( - blank=True, default="ECDH", max_length=20, null=True - ), - ), - ( - "key_material_curve", - models.CharField( - blank=True, default="Curve25519", max_length=20, null=True - ), - ), - ( - "key_material_public_key", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "key_material_private_key", - models.CharField(blank=True, max_length=200, null=True), - ), - ( - "key_material_nonce", - models.CharField(blank=True, max_length=100, null=True), - ), - ("signature", models.TextField(blank=True, null=True)), - ( - "consent_request", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="consent_artefacts", - to="abdm.consentrequest", - to_field="consent_id", - ), - ), - ( - "patient_abha", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - to="abdm.abhanumber", - to_field="health_id", - ), - ), - ( - "requester", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/care/abdm/migrations/0012_consentrequest_status.py b/care/abdm/migrations/0012_consentrequest_status.py deleted file mode 100644 index 43bf1ecb62..0000000000 --- a/care/abdm/migrations/0012_consentrequest_status.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 4.2.2 on 2023-12-02 04:08 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("abdm", "0011_alter_abhanumber_abha_number_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="consentrequest", - name="status", - field=models.CharField( - choices=[ - ("REQUESTED", "Requested"), - ("GRANTED", "Granted"), - ("DENIED", "Denied"), - ("EXPIRED", "Expired"), - ("REVOKED", "Revoked"), - ], - default="REQUESTED", - max_length=20, - ), - ), - ] diff --git a/care/abdm/migrations/0014_replace_0013.py b/care/abdm/migrations/0014_replace_0013.py new file mode 100644 index 0000000000..1c30a4e0a7 --- /dev/null +++ b/care/abdm/migrations/0014_replace_0013.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.10 on 2024-04-21 17:40 + +# This is a replacement migration for abdm.0013 that omits the RunPython operation (reverse_patient_abhanumber_relation) and facility migration dependency. + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("abdm", "0012_consentrequest_status"), + ] + + replaces = [ + ("abdm", "0013_abhanumber_patient"), + ] + + operations = [ + migrations.AddField( + model_name="abhanumber", + name="patient", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="abha_number", + to="facility.patientregistration", + ), + ), + ] diff --git a/care/abdm/migrations/0013_abhanumber_patient.py b/care/abdm/migrations_old/0013_abhanumber_patient.py similarity index 89% rename from care/abdm/migrations/0013_abhanumber_patient.py rename to care/abdm/migrations_old/0013_abhanumber_patient.py index 41d3854797..433dea8576 100644 --- a/care/abdm/migrations/0013_abhanumber_patient.py +++ b/care/abdm/migrations_old/0013_abhanumber_patient.py @@ -11,9 +11,7 @@ def reverse_patient_abhanumber_relation(apps, schema_editor): AbhaNumber = apps.get_model("abdm", "AbhaNumber") patients = ( - Patient.objects.annotate(removed_field=RawSQL("abha_number_id", ())) - .filter(abha_number__isnull=False) - .select_related("abha_number") + Patient.objects.filter(abha_number__isnull=False) ) abha_numbers_to_update = [] diff --git a/care/abdm/models/__init__.py b/care/abdm/models/__init__.py deleted file mode 100644 index 5b7edbb6fb..0000000000 --- a/care/abdm/models/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .abha_number import * # noqa -from .consent import * # noqa -from .health_facility import * # noqa diff --git a/care/abdm/models/abha_number.py b/care/abdm/models/abha_number.py deleted file mode 100644 index f5d67d2132..0000000000 --- a/care/abdm/models/abha_number.py +++ /dev/null @@ -1,41 +0,0 @@ -from django.db import models - -from care.utils.models.base import BaseModel - - -class AbhaNumber(BaseModel): - abha_number = models.TextField(null=True, blank=True, unique=True) - health_id = models.TextField(null=True, blank=True, unique=True) - - patient = models.OneToOneField( - "facility.PatientRegistration", - related_name="abha_number", - on_delete=models.PROTECT, - null=True, - blank=True, - ) - - name = models.TextField(null=True, blank=True) - first_name = models.TextField(null=True, blank=True) - middle_name = models.TextField(null=True, blank=True) - last_name = models.TextField(null=True, blank=True) - - gender = models.TextField(null=True, blank=True) - date_of_birth = models.TextField(null=True, blank=True) - - address = models.TextField(null=True, blank=True) - district = models.TextField(null=True, blank=True) - state = models.TextField(null=True, blank=True) - pincode = models.TextField(null=True, blank=True) - - email = models.EmailField(null=True, blank=True) - profile_photo = models.TextField(null=True, blank=True) - - new = models.BooleanField(default=False) - - txn_id = models.TextField(null=True, blank=True) - access_token = models.TextField(null=True, blank=True) - refresh_token = models.TextField(null=True, blank=True) - - def __str__(self): - return f"{self.pk} {self.abha_number}" diff --git a/care/abdm/models/base.py b/care/abdm/models/base.py deleted file mode 100644 index 82ff16ca85..0000000000 --- a/care/abdm/models/base.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.db import models - - -class Status(models.TextChoices): - REQUESTED = "REQUESTED" - GRANTED = "GRANTED" - DENIED = "DENIED" - EXPIRED = "EXPIRED" - REVOKED = "REVOKED" - - -class Purpose(models.TextChoices): - CARE_MANAGEMENT = "CAREMGT" - BREAK_THE_GLASS = "BTG" - PUBLIC_HEALTH = "PUBHLTH" - HEALTHCARE_PAYMENT = "HPAYMT" - DISEASE_SPECIFIC_HEALTHCARE_RESEARCH = "DSRCH" - SELF_REQUESTED = "PATRQT" - - -class HealthInformationTypes(models.TextChoices): - PRESCRIPTION = "Prescription" - DIAGNOSTIC_REPORT = "DiagnosticReport" - OP_CONSULTATION = "OPConsultation" - DISCHARGE_SUMMARY = "DischargeSummary" - IMMUNIZATION_RECORD = "ImmunizationRecord" - RECORD_ARTIFACT = "HealthDocumentRecord" - WELLNESS_RECORD = "WellnessRecord" - - -class AccessMode(models.TextChoices): - VIEW = "VIEW" - STORE = "STORE" - QUERY = "QUERY" - STREAM = "STREAM" - - -class FrequencyUnit(models.TextChoices): - HOUR = "HOUR" - DAY = "DAY" - WEEK = "WEEK" - MONTH = "MONTH" - YEAR = "YEAR" diff --git a/care/abdm/models/consent.py b/care/abdm/models/consent.py deleted file mode 100644 index ee4b0f8264..0000000000 --- a/care/abdm/models/consent.py +++ /dev/null @@ -1,160 +0,0 @@ -from django.contrib.postgres.fields import ArrayField -from django.core.validators import MinValueValidator -from django.db import models -from django.utils import timezone - -from care.abdm.models import AbhaNumber -from care.abdm.models.base import ( - AccessMode, - FrequencyUnit, - HealthInformationTypes, - Purpose, - Status, -) -from care.abdm.models.json_schema import CARE_CONTEXTS -from care.abdm.utils.cipher import Cipher -from care.facility.models.file_upload import FileUpload -from care.users.models import User -from care.utils.models.base import BaseModel -from care.utils.models.validators import JSONFieldSchemaValidator - - -class Consent(BaseModel): - class Meta: - abstract = True - - def default_expiry(): - return timezone.now() + timezone.timedelta(days=30) - - def default_from_time(): - return timezone.now() - timezone.timedelta(days=30) - - def default_to_time(): - return timezone.now() - - consent_id = models.UUIDField(null=True, blank=True, unique=True) - - patient_abha = models.ForeignKey( - AbhaNumber, on_delete=models.PROTECT, to_field="health_id" - ) - - care_contexts = models.JSONField( - default=list, validators=[JSONFieldSchemaValidator(CARE_CONTEXTS)] - ) - - status = models.CharField(choices=Status, max_length=20, default=Status.REQUESTED) - purpose = models.CharField( - choices=Purpose, max_length=20, default=Purpose.CARE_MANAGEMENT - ) - hi_types = ArrayField( - models.CharField(choices=HealthInformationTypes, max_length=20), - default=list, - ) - - hip = models.CharField(max_length=50, null=True, blank=True) - hiu = models.CharField(max_length=50, null=True, blank=True) - - requester = models.ForeignKey( - User, on_delete=models.SET_NULL, null=True, blank=True - ) - - access_mode = models.CharField( - choices=AccessMode, max_length=20, default=AccessMode.VIEW - ) - from_time = models.DateTimeField(null=True, blank=True, default=default_from_time) - to_time = models.DateTimeField(null=True, blank=True, default=default_to_time) - expiry = models.DateTimeField(null=True, blank=True, default=default_expiry) - - frequency_unit = models.CharField( - choices=FrequencyUnit, max_length=20, default=FrequencyUnit.HOUR - ) - frequency_value = models.PositiveSmallIntegerField( - default=1, validators=[MinValueValidator(1)] - ) - frequency_repeats = models.PositiveSmallIntegerField(default=0) - - def consent_details_dict(self): - return { - "patient_abha": self.patient_abha, - "care_contexts": self.care_contexts, - "status": self.status, - "purpose": self.purpose, - "hi_types": self.hi_types, - "hip": self.hip, - "hiu": self.hiu, - "requester": self.requester, - "access_mode": self.access_mode, - "from_time": self.from_time, - "to_time": self.to_time, - "expiry": self.expiry, - "frequency_unit": self.frequency_unit, - "frequency_value": self.frequency_value, - "frequency_repeats": self.frequency_repeats, - } - - -class ConsentRequest(Consent): - @property - def request_id(self): - return self.consent_id - - -class ConsentArtefact(Consent): - @property - def artefact_id(self): - return self.external_id - - @property - def transaction_id(self): - return self.consent_id - - def save(self, *args, **kwargs): - if self.key_material_private_key is None: - cipher = Cipher("", "") - key_material = cipher.generate_key_pair() - - self.key_material_algorithm = "ECDH" - self.key_material_curve = "Curve25519" - self.key_material_public_key = key_material["publicKey"] - self.key_material_private_key = key_material["privateKey"] - self.key_material_nonce = key_material["nonce"] - - if self.status in [Status.REVOKED.value, Status.EXPIRED.value]: - file = FileUpload.objects.filter( - internal_name=f"{self.external_id}.json", - file_type=FileUpload.FileType.ABDM_HEALTH_INFORMATION.value, - ).first() - - if file: - file.is_archived = True - file.archived_datetime = timezone.now() - file.archive_reason = self.status - file.save() - - return super().save(*args, **kwargs) - - consent_request = models.ForeignKey( - ConsentRequest, - on_delete=models.PROTECT, - to_field="consent_id", - null=True, - blank=True, - related_name="consent_artefacts", - ) - - cm = models.CharField(max_length=50, null=True, blank=True) - - key_material_algorithm = models.CharField( - max_length=20, - null=True, - blank=True, - default="ECDH", - ) - key_material_curve = models.CharField( - max_length=20, null=True, blank=True, default="Curve25519" - ) - key_material_public_key = models.CharField(max_length=100, null=True, blank=True) - key_material_private_key = models.CharField(max_length=200, null=True, blank=True) - key_material_nonce = models.CharField(max_length=100, null=True, blank=True) - - signature = models.TextField(null=True, blank=True) diff --git a/care/abdm/models/health_facility.py b/care/abdm/models/health_facility.py deleted file mode 100644 index a38bf1367c..0000000000 --- a/care/abdm/models/health_facility.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.db import models - -from care.abdm.models.permissions.health_facility import HealthFacilityPermissions -from care.utils.models.base import BaseModel - - -class HealthFacility(BaseModel, HealthFacilityPermissions): - hf_id = models.CharField(max_length=50, unique=True) - registered = models.BooleanField(default=False) - facility = models.OneToOneField( - "facility.Facility", on_delete=models.PROTECT, to_field="external_id" - ) - - def __str__(self): - return f"{self.hf_id} {self.facility}" diff --git a/care/abdm/models/json_schema.py b/care/abdm/models/json_schema.py deleted file mode 100644 index 081b65cc7c..0000000000 --- a/care/abdm/models/json_schema.py +++ /dev/null @@ -1,15 +0,0 @@ -CARE_CONTEXTS = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "content": [ - { - "type": "object", - "properties": { - "patientReference": {"type": "string"}, - "careContextReference": {"type": "string"}, - }, - "additionalProperties": False, - "required": ["patientReference", "careContextReference"], - } - ], -} diff --git a/care/abdm/models/permissions/health_facility.py b/care/abdm/models/permissions/health_facility.py deleted file mode 100644 index f1ccd0045e..0000000000 --- a/care/abdm/models/permissions/health_facility.py +++ /dev/null @@ -1,29 +0,0 @@ -from care.facility.models.mixins.permissions.base import BasePermissionMixin -from care.users.models import User - - -class HealthFacilityPermissions(BasePermissionMixin): - """ - Permissions for HealthFacilityViewSet - """ - - def has_object_read_permission(self, request): - return self.facility.has_object_read_permission(request) - - def has_object_write_permission(self, request): - allowed_user_types = [ - User.TYPE_VALUE_MAP["WardAdmin"], - User.TYPE_VALUE_MAP["LocalBodyAdmin"], - User.TYPE_VALUE_MAP["DistrictAdmin"], - User.TYPE_VALUE_MAP["StateAdmin"], - ] - return request.user.is_superuser or ( - request.user.user_type in allowed_user_types - and self.facility.has_object_write_permission(request) - ) - - def has_object_update_permission(self, request): - return self.has_object_write_permission(request) - - def has_object_destroy_permission(self, request): - return self.has_object_write_permission(request) diff --git a/care/abdm/receivers/consultation.py b/care/abdm/receivers/consultation.py deleted file mode 100644 index 82c9c3c847..0000000000 --- a/care/abdm/receivers/consultation.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.db.models.signals import post_save -from django.dispatch import receiver - -from care.abdm.utils.api_call import AbdmGateway -from care.facility.models import PatientConsultation - - -@receiver(post_save, sender=PatientConsultation) -def create_care_context(sender, instance, created, **kwargs): - patient = instance.patient - - if created and getattr(patient, "abha_number", None) is not None: - abha_number = patient.abha_number - - try: - AbdmGateway().fetch_modes( - { - "healthId": abha_number.abha_number, - "name": abha_number.name, - "gender": abha_number.gender, - "dateOfBirth": str(abha_number.date_of_birth), - "consultationId": instance.external_id, - "purpose": "LINK", - } - ) - except Exception: - pass diff --git a/care/abdm/service/gateway.py b/care/abdm/service/gateway.py deleted file mode 100644 index 45f0c781e0..0000000000 --- a/care/abdm/service/gateway.py +++ /dev/null @@ -1,215 +0,0 @@ -import uuid -from datetime import UTC, datetime - -from django.conf import settings -from django.db.models import Q -from rest_framework.exceptions import ValidationError - -from care.abdm.models.abha_number import AbhaNumber -from care.abdm.models.base import Purpose -from care.abdm.models.consent import ConsentArtefact, ConsentRequest -from care.abdm.service.request import Request - - -class Gateway: - def __init__(self): - self.request = Request(settings.ABDM_URL + "/gateway") - - def consent_requests__init(self, consent: ConsentRequest): - data = { - "requestId": str(consent.external_id), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "consent": { - "purpose": { - "text": Purpose(consent.purpose).label, - "code": Purpose(consent.purpose).value, - }, - "patient": {"id": consent.patient_abha.health_id}, - "hiu": { - "id": self.get_hf_id_by_health_id(consent.patient_abha.health_id) - }, - "requester": { - "name": f"{consent.requester.REVERSE_TYPE_MAP[consent.requester.user_type]}, {consent.requester.first_name} {consent.requester.last_name}", - "identifier": { - "type": "Care Username", - "value": consent.requester.username, - "system": settings.CURRENT_DOMAIN, - }, - }, - "hiTypes": consent.hi_types, - "permission": { - "accessMode": consent.access_mode, - "dateRange": { - "from": consent.from_time.astimezone(UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "to": consent.to_time.astimezone(UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - }, - "dataEraseAt": consent.expiry.astimezone(UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "frequency": { - "unit": consent.frequency_unit, - "value": consent.frequency_value, - "repeats": consent.frequency_repeats, - }, - }, - }, - } - - path = "/v0.5/consent-requests/init" - return self.request.post(path, data, headers={"X-CM-ID": settings.X_CM_ID}) - - def consent_requests__status(self, consent_request_id: str): - data = { - "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "consentRequestId": consent_request_id, - } - - return self.request.post( - "/v0.5/consent-requests/status", data, headers={"X-CM-ID": settings.X_CM_ID} - ) - - def consents__hiu__on_notify(self, consent: ConsentRequest, request_id: str): - data = { - "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "resp": {"requestId": request_id}, - } - - if len(consent.consent_artefacts.all()): - data["acknowledgement"] = [] - - for aretefact in consent.consent_artefacts.all(): - data["acknowledgement"].append( - { - "consentId": str(aretefact.artefact_id), - "status": "OK", - } - ) - - return self.request.post( - "/v0.5/consents/hiu/on-notify", data, headers={"X-CM-ID": settings.X_CM_ID} - ) - - def consents__fetch(self, consent_artefact_id: str): - data = { - "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "consentId": consent_artefact_id, - } - - return self.request.post( - "/v0.5/consents/fetch", data, headers={"X-CM-ID": settings.X_CM_ID} - ) - - def health_information__cm__request(self, artefact: ConsentArtefact): - request_id = str(uuid.uuid4()) - artefact.consent_id = request_id - artefact.save() - - data = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "hiRequest": { - "consent": {"id": str(artefact.artefact_id)}, - "dataPushUrl": settings.BACKEND_DOMAIN - + "/v0.5/health-information/transfer", - "keyMaterial": { - "cryptoAlg": artefact.key_material_algorithm, - "curve": artefact.key_material_curve, - "dhPublicKey": { - "expiry": artefact.expiry.astimezone(UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "parameters": f"{artefact.key_material_curve}/{artefact.key_material_algorithm}", - "keyValue": artefact.key_material_public_key, - }, - "nonce": artefact.key_material_nonce, - }, - "dateRange": { - "from": artefact.from_time.strftime("%Y-%m-%dT%H:%M:%S.000Z"), - "to": artefact.to_time.strftime("%Y-%m-%dT%H:%M:%S.000Z"), - }, - }, - } - - return self.request.post( - "/v0.5/health-information/cm/request", - data, - headers={"X-CM-ID": settings.X_CM_ID}, - ) - - def get_hf_id_by_health_id(self, health_id): - abha_number = AbhaNumber.objects.filter( - Q(abha_number=health_id) | Q(health_id=health_id) - ).first() - if not abha_number: - raise ValidationError(detail="No ABHA Number found") - - patient_facility = abha_number.patient.last_consultation.facility - if not hasattr(patient_facility, "healthfacility"): - raise ValidationError(detail="Health Facility not linked") - - return patient_facility.healthfacility.hf_id - - def health_information__notify(self, artefact: ConsentArtefact): - data = { - "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "notification": { - "consentId": str(artefact.artefact_id), - "transactionId": str(artefact.transaction_id), - "doneAt": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "notifier": { - "type": "HIU", - "id": self.get_hf_id_by_health_id(artefact.patient_abha.health_id), - }, - "statusNotification": { - "sessionStatus": "TRANSFERRED", - "hipId": artefact.hip, - }, - }, - } - - return self.request.post( - "/v0.5/health-information/notify", - data, - headers={"X-CM-ID": settings.X_CM_ID}, - ) - - def patients__find(self, abha_number: AbhaNumber): - data = { - "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "query": { - "patient": {"id": abha_number.health_id}, - "requester": { - "type": "HIU", - "id": self.get_hf_id_by_health_id(abha_number.health_id), - }, - }, - } - - return self.request.post( - "/v0.5/patients/find", data, headers={"X-CM-ID": settings.X_CM_ID} - ) diff --git a/care/abdm/service/request.py b/care/abdm/service/request.py deleted file mode 100644 index 55b9f12127..0000000000 --- a/care/abdm/service/request.py +++ /dev/null @@ -1,106 +0,0 @@ -import json -import logging - -import requests -from django.conf import settings -from django.core.cache import cache - -ABDM_GATEWAY_URL = settings.ABDM_URL + "/gateway" -ABDM_TOKEN_URL = ABDM_GATEWAY_URL + "/v0.5/sessions" -ABDM_TOKEN_CACHE_KEY = "abdm_token" - -logger = logging.getLogger(__name__) - - -class Request: - def __init__(self, base_url): - self.url = base_url - - def user_header(self, user_token): - if not user_token: - return {} - return {"X-Token": "Bearer " + user_token} - - def auth_header(self): - token = cache.get(ABDM_TOKEN_CACHE_KEY) - if not token: - data = json.dumps( - { - "clientId": settings.ABDM_CLIENT_ID, - "clientSecret": settings.ABDM_CLIENT_SECRET, - } - ) - headers = { - "Content-Type": "application/json", - "Accept": "application/json", - } - - logger.info("No Token in Cache") - response = requests.post(ABDM_TOKEN_URL, data=data, headers=headers) - - if response.status_code < 300: - if response.headers["Content-Type"] != "application/json": - logger.info( - f"Unsupported Content-Type: {response.headers['Content-Type']}" - ) - logger.info(f"Response: {response.text}") - - return None - else: - data = response.json() - token = data["accessToken"] - expires_in = data["expiresIn"] - - logger.info(f"New Token: {token}") - logger.info(f"Expires in: {expires_in}") - - cache.set(ABDM_TOKEN_CACHE_KEY, token, expires_in) - else: - logger.info(f"Bad Response: {response.text}") - return None - - return {"Authorization": f"Bearer {token}"} - - def headers(self, additional_headers=None, auth=None): - return { - "Content-Type": "application/json", - "Accept": "*/*", - **(additional_headers or {}), - **(self.user_header(auth) or {}), - **(self.auth_header() or {}), - } - - def get(self, path, params=None, headers=None, auth=None): - url = self.url + path - headers = self.headers(headers, auth) - - logger.info(f"GET: {url}") - response = requests.get(url, headers=headers, params=params) - logger.info(f"{response.status_code} Response: {response.text}") - - return self._handle_response(response) - - def post(self, path, data=None, headers=None, auth=None): - url = self.url + path - payload = json.dumps(data) - headers = self.headers(headers, auth) - - logger.info(f"POST: {url}, {headers}, {data}") - response = requests.post(url, data=payload, headers=headers) - logger.info(f"{response.status_code} Response: {response.text}") - - return self._handle_response(response) - - def _handle_response(self, response: requests.Response): - def custom_json(): - try: - return response.json() - except json.JSONDecodeError as json_err: - logger.error(f"JSON Decode error: {json_err}") - return {"error": response.text} - except Exception as err: - logger.error(f"Unknown error while decoding json: {err}") - return {} - - response.json = custom_json - return response diff --git a/care/abdm/tests.py b/care/abdm/tests.py deleted file mode 100644 index a79ca8be56..0000000000 --- a/care/abdm/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -# from django.test import TestCase - -# Create your tests here. diff --git a/care/abdm/urls.py b/care/abdm/urls.py deleted file mode 100644 index bf4eb8a5d9..0000000000 --- a/care/abdm/urls.py +++ /dev/null @@ -1,141 +0,0 @@ -from django.urls import path -from rest_framework.routers import SimpleRouter - -from care.abdm.api.viewsets.auth import ( - AuthNotifyView, - DiscoverView, - LinkConfirmView, - LinkInitView, - NotifyView, - OnAddContextsView, - OnConfirmView, - OnFetchView, - OnInitView, - RequestDataView, -) -from care.abdm.api.viewsets.consent import ConsentCallbackViewSet -from care.abdm.api.viewsets.health_information import HealthInformationCallbackViewSet -from care.abdm.api.viewsets.hip import HipViewSet -from care.abdm.api.viewsets.monitoring import HeartbeatView -from care.abdm.api.viewsets.patients import PatientsCallbackViewSet -from care.abdm.api.viewsets.status import NotifyView as PatientStatusNotifyView -from care.abdm.api.viewsets.status import SMSOnNotifyView - - -class OptionalSlashRouter(SimpleRouter): - def __init__(self): - super().__init__() - self.trailing_slash = "/?" - - -abdm_router = OptionalSlashRouter() - -abdm_router.register("profile/v1.0/patients/", HipViewSet, basename="hip") - -abdm_urlpatterns = [ - *abdm_router.urls, - path( - "v0.5/consent-requests/on-init", - ConsentCallbackViewSet.as_view({"post": "consent_request__on_init"}), - name="abdm__consent_request__on_init", - ), - path( - "v0.5/consent-requests/on-status", - ConsentCallbackViewSet.as_view({"post": "consent_request__on_status"}), - name="abdm__consent_request__on_status", - ), - path( - "v0.5/consents/hiu/notify", - ConsentCallbackViewSet.as_view({"post": "consents__hiu__notify"}), - name="abdm__consents__hiu__notify", - ), - path( - "v0.5/consents/on-fetch", - ConsentCallbackViewSet.as_view({"post": "consents__on_fetch"}), - name="abdm__consents__on_fetch", - ), - path( - "v0.5/health-information/hiu/on-request", - HealthInformationCallbackViewSet.as_view( - {"post": "health_information__hiu__on_request"} - ), - name="abdm__health_information__hiu__on_request", - ), - path( - "v0.5/health-information/transfer", - HealthInformationCallbackViewSet.as_view( - {"post": "health_information__transfer"} - ), - name="abdm__health_information__transfer", - ), - path( - "v0.5/patients/on-find", - PatientsCallbackViewSet.as_view({"post": "patients__on_find"}), - name="abdm__patients__on_find", - ), - path( - "v0.5/users/auth/on-fetch-modes", - OnFetchView.as_view(), - name="abdm_on_fetch_modes_view", - ), - path( - "v0.5/users/auth/on-init", - OnInitView.as_view(), - name="abdm_on_init_view", - ), - path( - "v0.5/users/auth/on-confirm", - OnConfirmView.as_view(), - name="abdm_on_confirm_view", - ), - path( - "v0.5/users/auth/notify", - AuthNotifyView.as_view(), - name="abdm_auth_notify_view", - ), - path( - "v0.5/links/link/on-add-contexts", - OnAddContextsView.as_view(), - name="abdm_on_add_context_view", - ), - path( - "v0.5/care-contexts/discover", - DiscoverView.as_view(), - name="abdm_discover_view", - ), - path( - "v0.5/links/link/init", - LinkInitView.as_view(), - name="abdm_link_init_view", - ), - path( - "v0.5/links/link/confirm", - LinkConfirmView.as_view(), - name="abdm_link_confirm_view", - ), - path( - "v0.5/consents/hip/notify", - NotifyView.as_view(), - name="abdm_notify_view", - ), - path( - "v0.5/health-information/hip/request", - RequestDataView.as_view(), - name="abdm_request_data_view", - ), - path( - "v0.5/patients/status/notify", - PatientStatusNotifyView.as_view(), - name="abdm_patient_status_notify_view", - ), - path( - "v0.5/patients/sms/on-notify", - SMSOnNotifyView.as_view(), - name="abdm_patient_status_notify_view", - ), - path( - "v0.5/heartbeat", - HeartbeatView.as_view(), - name="abdm_monitoring_heartbeat_view", - ), -] diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py deleted file mode 100644 index e357a6f781..0000000000 --- a/care/abdm/utils/api_call.py +++ /dev/null @@ -1,812 +0,0 @@ -import json -import logging -import uuid -from base64 import b64encode -from datetime import UTC, datetime, timedelta - -import requests -from Crypto.Cipher import PKCS1_v1_5 -from Crypto.PublicKey import RSA -from django.conf import settings -from django.core.cache import cache -from django.db.models import Q -from rest_framework.exceptions import ValidationError - -from care.abdm.models import AbhaNumber -from care.abdm.service.request import Request -from care.facility.models.patient_consultation import PatientConsultation - -GATEWAY_API_URL = settings.ABDM_URL -HEALTH_SERVICE_API_URL = settings.HEALTH_SERVICE_API_URL -ABDM_DEVSERVICE_URL = GATEWAY_API_URL + "/devservice" -ABDM_GATEWAY_URL = GATEWAY_API_URL + "/gateway" -ABDM_TOKEN_URL = ABDM_GATEWAY_URL + "/v0.5/sessions" -ABDM_TOKEN_CACHE_KEY = "abdm_token" -ABDM_FACILITY_URL = settings.ABDM_FACILITY_URL - -# TODO: Exception handling for all api calls, need to gracefully handle known exceptions - -logger = logging.getLogger(__name__) - - -def encrypt_with_public_key(a_message): - rsa_public_key = RSA.importKey( - requests.get(HEALTH_SERVICE_API_URL + "/v2/auth/cert").text.strip() - ) - rsa_public_key = PKCS1_v1_5.new(rsa_public_key) - encrypted_text = rsa_public_key.encrypt(a_message.encode()) - return b64encode(encrypted_text).decode() - - -class APIGateway: - def __init__(self, gateway, token): - if gateway == "health": - self.url = HEALTH_SERVICE_API_URL - elif gateway == "abdm": - self.url = GATEWAY_API_URL - elif gateway == "abdm_gateway": - self.url = ABDM_GATEWAY_URL - elif gateway == "abdm_devservice": - self.url = ABDM_DEVSERVICE_URL - elif gateway == "facility": - self.url = ABDM_FACILITY_URL - else: - self.url = GATEWAY_API_URL - self.token = token - - # def encrypt(self, data): - # cert = cache.get("abdm_cert") - # if not cert: - # cert = requests.get(settings.ABDM_CERT_URL).text - # cache.set("abdm_cert", cert, 3600) - - def add_user_header(self, headers, user_token): - headers.update( - { - "X-Token": "Bearer " + user_token, - } - ) - return headers - - def add_auth_header(self, headers): - token = cache.get(ABDM_TOKEN_CACHE_KEY) - if not token: - logger.info("No Token in Cache") - data = { - "clientId": settings.ABDM_CLIENT_ID, - "clientSecret": settings.ABDM_CLIENT_SECRET, - } - auth_headers = { - "Content-Type": "application/json", - "Accept": "application/json", - } - resp = requests.post( - ABDM_TOKEN_URL, data=json.dumps(data), headers=auth_headers - ) - logger.info(f"Token Response Status: {resp.status_code}") - if resp.status_code < 300: - # Checking if Content-Type is application/json - if resp.headers["Content-Type"] != "application/json": - logger.info( - "Unsupported Content-Type: {}".format( - resp.headers["Content-Type"] - ) - ) - logger.info(f"Response: {resp.text}") - return None - else: - data = resp.json() - token = data["accessToken"] - expires_in = data["expiresIn"] - logger.info(f"New Token: {token}") - logger.info(f"Expires in: {expires_in}") - cache.set(ABDM_TOKEN_CACHE_KEY, token, expires_in) - else: - logger.info(f"Bad Response: {resp.text}") - return None - # logger.info("Returning Authorization Header: Bearer {}".format(token)) - logger.info("Adding Authorization Header") - auth_header = {"Authorization": f"Bearer {token}"} - return {**headers, **auth_header} - - def add_additional_headers(self, headers, additional_headers): - return {**headers, **additional_headers} - - def get(self, path, params=None, auth=None): - url = self.url + path - headers = {} - headers = self.add_auth_header(headers) - if auth: - headers = self.add_user_header(headers, auth) - logger.info(f"Making GET Request to: {url}") - response = requests.get(url, headers=headers, params=params) - logger.info(f"{response.status_code} Response: {response.text}") - return response - - def post(self, path, data=None, auth=None, additional_headers=None, method="POST"): - url = self.url + path - headers = { - "Content-Type": "application/json", - "accept": "*/*", - "Accept-Language": "en-US", - } - headers = self.add_auth_header(headers) - if auth: - headers = self.add_user_header(headers, auth) - if additional_headers: - headers = self.add_additional_headers(headers, additional_headers) - # headers_string = " ".join( - # ['-H "{}: {}"'.format(k, v) for k, v in headers.items()] - # ) - data_json = json.dumps(data) - # logger.info("curl -X POST {} {} -d {}".format(url, headers_string, data_json)) - logger.info(f"Posting Request to: {url}") - response = requests.request(method, url, headers=headers, data=data_json) - logger.info(f"{response.status_code} Response: {response.text}") - return response - - -class HealthIdGateway: - def __init__(self): - self.api = APIGateway("health", None) - - def generate_aadhaar_otp(self, data): - path = "/v1/registration/aadhaar/generateOtp" - response = self.api.post(path, data) - logger.info(f"{response.status_code} Response: {response.text}") - return response.json() - - def resend_aadhaar_otp(self, data): - path = "/v1/registration/aadhaar/resendAadhaarOtp" - response = self.api.post(path, data) - return response.json() - - def verify_aadhaar_otp(self, data): - path = "/v1/registration/aadhaar/verifyOTP" - response = self.api.post(path, data) - return response.json() - - def check_and_generate_mobile_otp(self, data): - path = "/v2/registration/aadhaar/checkAndGenerateMobileOTP" - response = self.api.post(path, data) - return response.json() - - def generate_mobile_otp(self, data): - path = "/v2/registration/aadhaar/generateMobileOTP" - response = self.api.post(path, data) - return response.json() - - # /v1/registration/aadhaar/verifyMobileOTP - def verify_mobile_otp(self, data): - path = "/v1/registration/aadhaar/verifyMobileOTP" - response = self.api.post(path, data) - return response.json() - - # /v1/registration/aadhaar/createHealthIdWithPreVerified - def create_health_id(self, data): - path = "/v1/registration/aadhaar/createHealthIdWithPreVerified" - logger.info(f"Creating Health ID with data: {data}") - # data.pop("healthId", None) - response = self.api.post(path, data) - return response.json() - - # /v1/search/existsByHealthId - # API checks if ABHA Address/ABHA Number is reserved/used which includes permanently deleted ABHA Addresses - # Return { status: true } - def exists_by_health_id(self, data): - path = "/v1/search/existsByHealthId" - response = self.api.post(path, data) - return response.json() - - # /v1/search/searchByHealthId - # API returns only Active or Deactive ABHA Number/ Address (Never returns Permanently Deleted ABHA Number/Address) - # Returns { - # "authMethods": [ - # "AADHAAR_OTP" - # ], - # "healthId": "deepakndhm", - # "healthIdNumber": "43-4221-5105-6749", - # "name": "kishan kumar singh", - # "status": "ACTIVE" - # } - def search_by_health_id(self, data): - path = "/v1/search/searchByHealthId" - response = self.api.post(path, data) - return response.json() - - # /v1/search/searchByMobile - def search_by_mobile(self, data): - path = "/v1/search/searchByMobile" - response = self.api.post(path, data) - return response.json() - - # Auth APIs - - # /v1/auth/init - def auth_init(self, data): - path = "/v1/auth/init" - response = self.api.post(path, data) - return response.json() - - # /v1/auth/confirmWithAadhaarOtp - def confirm_with_aadhaar_otp(self, data): - path = "/v1/auth/confirmWithAadhaarOtp" - response = self.api.post(path, data) - return response.json() - - # /v1/auth/confirmWithMobileOTP - def confirm_with_mobile_otp(self, data): - path = "/v1/auth/confirmWithMobileOTP" - response = self.api.post(path, data) - return response.json() - - # /v1/auth/confirmWithDemographics - def confirm_with_demographics(self, data): - path = "/v1/auth/confirmWithDemographics" - response = self.api.post(path, data) - return response.json() - - def verify_demographics(self, health_id, name, gender, year_of_birth): - auth_init_response = HealthIdGateway().auth_init( - {"authMethod": "DEMOGRAPHICS", "healthid": health_id} - ) - if "txnId" in auth_init_response: - demographics_response = HealthIdGateway().confirm_with_demographics( - { - "txnId": auth_init_response["txnId"], - "name": name, - "gender": gender, - "yearOfBirth": year_of_birth, - } - ) - return "status" in demographics_response and demographics_response["status"] - - return False - - # /v1/auth/generate/access-token - def generate_access_token(self, data): - if "access_token" in data: - return data["access_token"] - elif "accessToken" in data: - return data["accessToken"] - elif "token" in data: - return data["token"] - - if "refreshToken" in data: - refreshToken = data["refreshToken"] - elif "refresh_token" in data: - refreshToken = data["refresh_token"] - else: - return None - path = "/v1/auth/generate/access-token" - response = self.api.post(path, {"refreshToken": refreshToken}) - return response.json()["accessToken"] - - # Account APIs - - # /v1/account/profile - def get_profile(self, data): - path = "/v1/account/profile" - access_token = self.generate_access_token(data) - response = self.api.get(path, {}, access_token) - return response.json() - - # /v1/account/getPngCard - def get_abha_card_png(self, data): - path = "/v1/account/getPngCard" - access_token = self.generate_access_token(data) - response = self.api.get(path, {}, access_token) - - return b64encode(response.content) - - def get_abha_card_pdf(self, data): - path = "/v1/account/getCard" - access_token = self.generate_access_token(data) - response = self.api.get(path, {}, access_token) - - return b64encode(response.content) - - # /v1/account/qrCode - def get_qr_code(self, data, auth): - path = "/v1/account/qrCode" - access_token = self.generate_access_token(data) - logger.info(f"Getting QR Code for: {data}") - response = self.api.get(path, {}, access_token) - logger.info(f"QR Code Response: {response.text}") - return response.json() - - -class HealthIdGatewayV2: - def __init__(self): - self.api = APIGateway("health", None) - - # V2 APIs - def generate_aadhaar_otp(self, data): - path = "/v2/registration/aadhaar/generateOtp" - data["aadhaar"] = encrypt_with_public_key(data["aadhaar"]) - data.pop("cancelToken", {}) - response = self.api.post(path, data) - return response.json() - - def generate_document_mobile_otp(self, data): - path = "/v2/document/generate/mobile/otp" - data["mobile"] = "ENTER MOBILE NUMBER HERE" # Hard Coding for test - data.pop("cancelToken", {}) - response = self.api.post(path, data) - return response.json() - - def verify_document_mobile_otp(self, data): - path = "/v2/document/verify/mobile/otp" - data["otp"] = encrypt_with_public_key(data["otp"]) - data.pop("cancelToken", {}) - response = self.api.post(path, data) - return response.json() - - -class AbdmGateway: - # TODO: replace this with in-memory db (redis) - temp_memory = {} - - def __init__(self): - self.api = APIGateway("abdm_gateway", None) - - def get_hip_id_by_health_id(self, health_id): - abha_number = AbhaNumber.objects.filter( - Q(abha_number=health_id) | Q(health_id=health_id) - ).first() - if not abha_number: - raise ValidationError(detail="No ABHA Number found") - - patient_facility = abha_number.patient.last_consultation.facility - if not getattr(patient_facility, "healthfacility", None): - raise ValidationError(detail="Health Facility not linked") - - return patient_facility.healthfacility.hf_id - - def add_care_context(self, access_token, request_id): - if request_id not in self.temp_memory: - return None - - data = self.temp_memory[request_id] - - if "consultationId" in data: - consultation = PatientConsultation.objects.get( - external_id=data["consultationId"] - ) - - response = self.add_contexts( - { - "access_token": access_token, - "patient_id": str(consultation.patient.external_id), - "patient_name": consultation.patient.name, - "context_id": str(consultation.external_id), - "context_name": f"Encounter: {consultation.created_date.date()!s}", - } - ) - - return response - - return False - - def save_linking_token(self, patient, access_token, request_id): - if request_id not in self.temp_memory: - return None - - data = self.temp_memory[request_id] - health_id = patient and patient["id"] or data["healthId"] - - abha_object = AbhaNumber.objects.filter( - Q(abha_number=health_id) | Q(health_id=health_id) - ).first() - - if abha_object: - abha_object.access_token = access_token - abha_object.save() - return True - - return False - - # /v0.5/users/auth/fetch-modes - def fetch_modes(self, data): - path = "/v0.5/users/auth/fetch-modes" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - request_id = str(uuid.uuid4()) - - self.temp_memory[request_id] = data - if "authMode" in data and data["authMode"] == "DIRECT": - self.init(request_id) - return None - - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "query": { - "id": data["healthId"], - "purpose": data["purpose"] if "purpose" in data else "KYC_AND_LINK", - "requester": { - "type": "HIP", - "id": self.get_hip_id_by_health_id(data["healthId"]), - }, - }, - } - response = self.api.post(path, payload, None, additional_headers) - return response - - # "/v0.5/users/auth/init" - def init(self, prev_request_id): - if prev_request_id not in self.temp_memory: - return None - - path = "/v0.5/users/auth/init" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - - data = self.temp_memory[prev_request_id] - self.temp_memory[request_id] = data - - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "query": { - "id": data["healthId"], - "purpose": data["purpose"] if "purpose" in data else "KYC_AND_LINK", - "authMode": data["authMode"] if "authMode" in data else "DEMOGRAPHICS", - "requester": { - "type": "HIP", - "id": self.get_hip_id_by_health_id(data["healthId"]), - }, - }, - } - response = self.api.post(path, payload, None, additional_headers) - return response - - # "/v0.5/users/auth/confirm" - def confirm(self, transaction_id, prev_request_id): - if prev_request_id not in self.temp_memory: - return None - - path = "/v0.5/users/auth/confirm" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - - data = self.temp_memory[prev_request_id] - self.temp_memory[request_id] = data - - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "transactionId": transaction_id, - "credential": { - "demographic": { - "name": data["name"], - "gender": data["gender"], - "dateOfBirth": data["dateOfBirth"], - }, - "authCode": "", - }, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def auth_on_notify(self, data): - path = "/v0.5/links/link/on-init" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "acknowledgement": {"status": "OK"}, - # "error": {"code": 1000, "message": "string"}, - "resp": {"requestId": data["request_id"]}, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - # TODO: make it dynamic and call it at discharge (call it from on_confirm) - def add_contexts(self, data): - path = "/v0.5/links/link/add-contexts" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "link": { - "accessToken": data["access_token"], - "patient": { - "referenceNumber": data["patient_id"], - "display": data["patient_name"], - "careContexts": [ - { - "referenceNumber": data["context_id"], - "display": data["context_name"], - } - ], - }, - }, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def on_discover(self, data): - path = "/v0.5/care-contexts/on-discover" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "transactionId": data["transaction_id"], - "patient": { - "referenceNumber": data["patient_id"], - "display": data["patient_name"], - "careContexts": list( - map( - lambda context: { - "referenceNumber": context["id"], - "display": context["name"], - }, - data["care_contexts"], - ) - ), - "matchedBy": data["matched_by"], - }, - # "error": {"code": 1000, "message": "string"}, - "resp": {"requestId": data["request_id"]}, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def on_link_init(self, data): - path = "/v0.5/links/link/on-init" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "transactionId": data["transaction_id"], - "link": { - "referenceNumber": data["patient_id"], - "authenticationType": "DIRECT", - "meta": { - "communicationMedium": "MOBILE", - "communicationHint": data["phone"], - "communicationExpiry": str( - (datetime.now() + timedelta(minutes=15)).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ) - ), - }, - }, - # "error": {"code": 1000, "message": "string"}, - "resp": {"requestId": data["request_id"]}, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def on_link_confirm(self, data): - path = "/v0.5/links/link/on-confirm" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "patient": { - "referenceNumber": data["patient_id"], - "display": data["patient_name"], - "careContexts": list( - map( - lambda context: { - "referenceNumber": context["id"], - "display": context["name"], - }, - data["care_contexts"], - ) - ), - }, - # "error": {"code": 1000, "message": "string"}, - "resp": {"requestId": data["request_id"]}, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def on_notify(self, data): - path = "/v0.5/consents/hip/on-notify" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "acknowledgement": {"status": "OK", "consentId": data["consent_id"]}, - # "error": {"code": 1000, "message": "string"}, - "resp": {"requestId": data["request_id"]}, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def on_data_request(self, data): - path = "/v0.5/health-information/hip/on-request" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "hiRequest": { - "transactionId": data["transaction_id"], - "sessionStatus": "ACKNOWLEDGED", - }, - # "error": {"code": 1000, "message": "string"}, - "resp": {"requestId": data["request_id"]}, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def data_transfer(self, data): - auth_header = Request("").auth_header() - - if not auth_header: - return None - - headers = { - "Content-Type": "application/json", - **auth_header, - } - - payload = { - "pageNumber": 1, - "pageCount": 1, - "transactionId": data["transaction_id"], - "entries": list( - map( - lambda context: { - "content": context["data"], - "media": "application/fhir+json", - "checksum": "string", - "careContextReference": context["consultation_id"], - }, - data["care_contexts"], - ) - ), - "keyMaterial": data["key_material"], - } - - response = requests.post( - data["data_push_url"], data=json.dumps(payload), headers=headers - ) - return response - - def data_notify(self, data): - path = "/v0.5/health-information/notify" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "notification": { - "consentId": data["consent_id"], - "transactionId": data["transaction_id"], - "doneAt": str( - datetime.now(tz=UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z") - ), - "statusNotification": { - "sessionStatus": data["session_status"], - "hipId": self.get_hip_id_by_health_id(data["health_id"]), - "statusResponses": list( - map( - lambda context: { - "careContextReference": context["id"], - "hiStatus": "OK", - "description": "success", # not sure what to put - }, - data["care_contexts"], - ) - ), - }, - }, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def patient_status_on_notify(self, data): - path = "/v0.5/patients/status/on-notify" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "acknowledgement": {"status": "OK"}, - # "error": {"code": 1000, "message": "string"}, - "resp": {"requestId": data["request_id"]}, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - def patient_sms_notify(self, data): - path = "/v0.5/patients/sms/notify2" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "notification": { - "phoneNo": f"+91-{data['phone']}", - "hip": {"id": self.get_hip_id_by_health_id(data["healthId"])}, - }, - } - - response = self.api.post(path, payload, None, additional_headers) - return response - - # /v1.0/patients/profile/on-share - def on_share(self, data): - path = "/v1.0/patients/profile/on-share" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - response = self.api.post(path, data, None, additional_headers) - return response - - -class Bridge: - def __init__(self): - self.api = APIGateway("abdm_devservice", None) - - def add_update_service(self, data): - path = "/v1/bridges/addUpdateServices" - response = self.api.post(path, data, method="PUT") - return response - - -class Facility: - def __init__(self) -> None: - self.api = APIGateway("facility", None) - - def add_update_service(self, data): - path = "/v1/bridges/MutipleHRPAddUpdateServices" - response = self.api.post(path, data, method="POST") - return response diff --git a/care/abdm/utils/cipher.py b/care/abdm/utils/cipher.py deleted file mode 100644 index 5a48430956..0000000000 --- a/care/abdm/utils/cipher.py +++ /dev/null @@ -1,95 +0,0 @@ -import json - -import requests -from django.conf import settings - - -class Cipher: - server_url = settings.FIDELIUS_URL - - def __init__( - self, - external_public_key, - external_nonce, - internal_private_key=None, - internal_public_key=None, - internal_nonce=None, - ): - self.external_public_key = external_public_key - self.external_nonce = external_nonce - - self.internal_private_key = internal_private_key - self.internal_public_key = internal_public_key - self.internal_nonce = internal_nonce - - self.key_to_share = None - - def generate_key_pair(self): - response = requests.get(f"{self.server_url}/keys/generate") - - if response.status_code == 200: - key_material = response.json() - - self.internal_private_key = key_material["privateKey"] - self.internal_public_key = key_material["publicKey"] - self.internal_nonce = key_material["nonce"] - - return key_material - - return None - - def encrypt(self, paylaod): - if not self.internal_private_key: - key_material = self.generate_key_pair() - - if not key_material: - return None - - response = requests.post( - f"{self.server_url}/encrypt", - headers={"Content-Type": "application/json"}, - data=json.dumps( - { - "receiverPublicKey": self.external_public_key, - "receiverNonce": self.external_nonce, - "senderPrivateKey": self.internal_private_key, - "senderPublicKey": self.internal_public_key, - "senderNonce": self.internal_nonce, - "plainTextData": paylaod, - } - ), - ) - - if response.status_code == 200: - data = response.json() - self.key_to_share = data["keyToShare"] - - return { - "public_key": self.key_to_share, - "data": data["encryptedData"], - "nonce": self.internal_nonce, - } - - return None - - def decrypt(self, paylaod): - response = requests.post( - f"{self.server_url}/decrypt", - headers={"Content-Type": "application/json"}, - data=json.dumps( - { - "receiverPrivateKey": self.internal_private_key, - "receiverNonce": self.internal_nonce, - "senderPublicKey": self.external_public_key, - "senderNonce": self.external_nonce, - "encryptedData": paylaod, - } - ), - ) - - if response.status_code == 200: - data = response.json() - - return data["decryptedData"] - - return None diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py deleted file mode 100644 index eb9db58f2e..0000000000 --- a/care/abdm/utils/fhir.py +++ /dev/null @@ -1,1220 +0,0 @@ -import base64 -from datetime import UTC, datetime -from uuid import uuid4 as uuid - -from fhir.resources.address import Address -from fhir.resources.annotation import Annotation -from fhir.resources.attachment import Attachment -from fhir.resources.bundle import Bundle, BundleEntry -from fhir.resources.careplan import CarePlan -from fhir.resources.codeableconcept import CodeableConcept -from fhir.resources.coding import Coding -from fhir.resources.composition import Composition, CompositionSection -from fhir.resources.condition import Condition -from fhir.resources.contactpoint import ContactPoint -from fhir.resources.diagnosticreport import DiagnosticReport -from fhir.resources.documentreference import DocumentReference, DocumentReferenceContent -from fhir.resources.dosage import Dosage -from fhir.resources.encounter import Encounter, EncounterDiagnosis -from fhir.resources.humanname import HumanName -from fhir.resources.identifier import Identifier -from fhir.resources.immunization import Immunization, ImmunizationProtocolApplied -from fhir.resources.medication import Medication -from fhir.resources.medicationrequest import MedicationRequest -from fhir.resources.meta import Meta -from fhir.resources.observation import Observation, ObservationComponent -from fhir.resources.organization import Organization -from fhir.resources.patient import Patient -from fhir.resources.period import Period -from fhir.resources.practitioner import Practitioner -from fhir.resources.procedure import Procedure -from fhir.resources.quantity import Quantity -from fhir.resources.reference import Reference - -from care.facility.models.file_upload import FileUpload -from care.facility.models.icd11_diagnosis import REVERSE_CONDITION_VERIFICATION_STATUSES -from care.facility.models.patient_investigation import InvestigationValue -from care.facility.static_data.icd11 import get_icd11_diagnosis_object_by_id - - -class Fhir: - def __init__(self, consultation): - self.consultation = consultation - - self._patient_profile = None - self._practitioner_profile = None - self._organization_profile = None - self._encounter_profile = None - self._careplan_profile = None - self._diagnostic_report_profile = None - self._immunization_profile = None - self._medication_profiles = [] - self._medication_request_profiles = [] - self._observation_profiles = [] - self._document_reference_profiles = [] - self._condition_profiles = [] - self._procedure_profiles = [] - - def _reference_url(self, resource=None): - if resource is None: - return "" - - return f"{resource.resource_type}/{resource.id}" - - def _reference(self, resource=None): - if resource is None: - return None - - return Reference(reference=self._reference_url(resource)) - - def _patient(self): - if self._patient_profile is not None: - return self._patient_profile - - id = str(self.consultation.patient.external_id) - name = self.consultation.patient.name - gender = self.consultation.patient.gender - self._patient_profile = Patient( - id=id, - identifier=[Identifier(value=id)], - name=[HumanName(text=name)], - gender="male" if gender == 1 else "female" if gender == 2 else "other", - ) - - return self._patient_profile - - def _practioner(self): - if self._practitioner_profile is not None: - return self._practitioner_profile - - id = str(uuid()) - name = ( - ( - self.consultation.treating_physician - and f"{self.consultation.treating_physician.first_name} {self.consultation.treating_physician.last_name}" - ) - or self.consultation.deprecated_verified_by - or f"{self.consultation.created_by.first_name} {self.consultation.created_by.last_name}" - ) - self._practitioner_profile = Practitioner( - id=id, - identifier=[Identifier(value=id)], - name=[HumanName(text=name)], - ) - - return self._practitioner_profile - - def _organization(self): - if self._organization_profile is not None: - return self._organization_profile - - id = str(self.consultation.facility.external_id) - hip_id = "IN3210000017" # TODO: make it dynamic - name = self.consultation.facility.name - phone = self.consultation.facility.phone_number - address = self.consultation.facility.address - local_body = self.consultation.facility.local_body.name - district = self.consultation.facility.district.name - state = self.consultation.facility.state.name - pincode = self.consultation.facility.pincode - self._organization_profile = Organization( - id=id, - identifier=[ - Identifier(system="https://facilitysbx.ndhm.gov.in", value=hip_id) - ], - name=name, - telecom=[ContactPoint(system="phone", value=phone)], - address=[ - Address( - line=[address, local_body], - district=district, - state=state, - postalCode=pincode, - country="INDIA", - ) - ], - ) - - return self._organization_profile - - def _condition(self, diagnosis_id, verification_status): - diagnosis = get_icd11_diagnosis_object_by_id(diagnosis_id) - [code, label] = diagnosis.label.split(" ", 1) - condition_profile = Condition( - id=diagnosis_id, - identifier=[Identifier(value=diagnosis_id)], - category=[ - CodeableConcept( - coding=[ - Coding( - system="http://terminology.hl7.org/CodeSystem/condition-category", - code="encounter-diagnosis", - display="Encounter Diagnosis", - ) - ], - text="Encounter Diagnosis", - ) - ], - verificationStatus=CodeableConcept( - coding=[ - Coding( - system="http://terminology.hl7.org/CodeSystem/condition-ver-status", - code=verification_status, - display=REVERSE_CONDITION_VERIFICATION_STATUSES[ - verification_status - ], - ) - ] - ), - code=CodeableConcept( - coding=[ - Coding( - system="http://id.who.int/icd/release/11/mms", - code=code, - display=label, - ) - ], - text=diagnosis.label, - ), - subject=self._reference(self._patient()), - ) - - self._condition_profiles.append(condition_profile) - return condition_profile - - def _procedure(self, procedure): - procedure_profile = Procedure( - id=str(uuid()), - status="completed", - code=CodeableConcept( - text=procedure["procedure"], - ), - subject=self._reference(self._patient()), - performedDateTime=( - f"{procedure['time']}:00+05:30" if not procedure["repetitive"] else None - ), - performedString=( - f"Every {procedure['frequency']}" if procedure["repetitive"] else None - ), - ) - - self._procedure_profiles.append(procedure_profile) - return procedure_profile - - def _careplan(self): - if self._careplan_profile: - return self._careplan_profile - - self._careplan_profile = CarePlan( - id=str(uuid()), - status="completed", - intent="plan", - title="Care Plan", - description="This includes Treatment Summary, Prescribed Medication, General Notes and Special Instructions", - period=Period( - start=self.consultation.encounter_date.isoformat(), - end=( - self.consultation.discharge_date.isoformat() - if self.consultation.discharge_date - else None - ), - ), - note=[ - Annotation(text=self.consultation.treatment_plan), - Annotation(text=self.consultation.consultation_notes), - Annotation(text=self.consultation.special_instruction), - ], - subject=self._reference(self._patient()), - ) - - return self._careplan_profile - - def _diagnostic_report(self): - if self._diagnostic_report_profile: - return self._diagnostic_report_profile - - self._diagnostic_report_profile = DiagnosticReport( - id=str(uuid()), - status="final", - code=CodeableConcept(text="Investigation/Test Results"), - result=list( - map( - lambda investigation: self._reference( - self._observation( - title=investigation.investigation.name, - value={ - "value": investigation.value, - "unit": investigation.investigation.unit, - }, - id=str(investigation.external_id), - date=investigation.created_date.isoformat(), - ) - ), - InvestigationValue.objects.filter(consultation=self.consultation), - ) - ), - subject=self._reference(self._patient()), - performer=[self._reference(self._organization())], - resultsInterpreter=[self._reference(self._organization())], - conclusion="Refer to Doctor. To be correlated with further study.", - ) - - return self._diagnostic_report_profile - - def _observation(self, title, value, id, date): - if not value or (isinstance(value, dict) and not value["value"]): - return None - - return Observation( - id=( - f"{id}.{title.replace(' ', '').replace('_', '-')}" - if id and title - else str(uuid()) - ), - status="final", - effectiveDateTime=date if date else None, - code=CodeableConcept(text=title), - valueQuantity=( - Quantity(value=str(value["value"]), unit=value["unit"]) - if isinstance(value, dict) - else None - ), - valueString=value if isinstance(value, str) else None, - component=( - list( - map( - lambda component: ObservationComponent( - code=CodeableConcept(text=component["title"]), - valueQuantity=( - Quantity( - value=component["value"], unit=component["unit"] - ) - if isinstance(component, dict) - else None - ), - valueString=( - component if isinstance(component, str) else None - ), - ), - value, - ) - ) - if isinstance(value, list) - else None - ), - ) - - def _observations_from_daily_round(self, daily_round): - id = str(daily_round.external_id) - date = daily_round.created_date.isoformat() - observation_profiles = [ - self._observation( - "Temperature", - {"value": daily_round.temperature, "unit": "F"}, - id, - date, - ), - self._observation( - "SpO2", - {"value": daily_round.ventilator_spo2, "unit": "%"}, - id, - date, - ), - self._observation( - "Pulse", - {"value": daily_round.pulse, "unit": "bpm"}, - id, - date, - ), - self._observation( - "Resp", - {"value": daily_round.resp, "unit": "bpm"}, - id, - date, - ), - self._observation( - "Blood Pressure", - ( - [ - { - "title": "Systolic Blood Pressure", - "value": daily_round.bp["systolic"], - "unit": "mmHg", - }, - { - "title": "Diastolic Blood Pressure", - "value": daily_round.bp["diastolic"], - "unit": "mmHg", - }, - ] - if "systolic" in daily_round.bp and "diastolic" in daily_round.bp - else None - ), - id, - date, - ), - ] - - # TODO: do it for other fields like bp, pulse, spo2, ... - - observation_profiles = list( - filter(lambda profile: profile is not None, observation_profiles) - ) - self._observation_profiles.extend(observation_profiles) - return observation_profiles - - def _encounter(self, include_diagnosis=False): - if self._encounter_profile is not None: - return self._encounter_profile - - id = str(self.consultation.external_id) - status = "finished" if self.consultation.discharge_date else "in-progress" - period_start = self.consultation.encounter_date.isoformat() - period_end = ( - self.consultation.discharge_date.isoformat() - if self.consultation.discharge_date - else None - ) - self._encounter_profile = Encounter( - **{ - "id": id, - "identifier": [Identifier(value=id)], - "status": status, - "class": Coding(code="IMP", display="Inpatient Encounter"), - "subject": self._reference(self._patient()), - "period": Period(start=period_start, end=period_end), - "diagnosis": ( - list( - map( - lambda consultation_diagnosis: EncounterDiagnosis( - condition=self._reference( - self._condition( - consultation_diagnosis.diagnosis_id, - consultation_diagnosis.verification_status, - ), - ) - ), - self.consultation.diagnoses.all(), - ) - ) - if include_diagnosis - else None - ), - } - ) - - return self._encounter_profile - - def _immunization(self): - if self._immunization_profile: - return self._immunization_profile - - if not self.consultation.patient.is_vaccinated: - return None - - self._immunization_profile = Immunization( - id=str(uuid()), - status="completed", - identifier=[ - Identifier( - type=CodeableConcept(text="Covin Id"), - value=self.consultation.patient.covin_id, - ) - ], - vaccineCode=CodeableConcept( - coding=[ - Coding( - system="http://snomed.info/sct", - code="1119305005", - display="COVID-19 antigen vaccine", - ) - ], - text=self.consultation.patient.vaccine_name, - ), - patient=self._reference(self._patient()), - route=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="47625008", - display="Intravenous route", - ) - ] - ), - occurrenceDateTime=self.consultation.patient.last_vaccinated_date.isoformat(), - protocolApplied=[ - ImmunizationProtocolApplied( - doseNumberPositiveInt=self.consultation.patient.number_of_doses - ) - ], - ) - - def _document_reference(self, file): - id = str(file.external_id) - content_type, content = file.file_contents() - document_reference_profile = DocumentReference( - id=id, - identifier=[Identifier(value=id)], - status="current", - type=CodeableConcept(text=file.internal_name.split(".")[0]), - content=[ - DocumentReferenceContent( - attachment=Attachment( - contentType=content_type, data=base64.b64encode(content) - ) - ) - ], - author=[self._reference(self._organization())], - ) - - self._document_reference_profiles.append(document_reference_profile) - return document_reference_profile - - def _medication(self, name): - medication_profile = Medication(id=str(uuid()), code=CodeableConcept(text=name)) - - self._medication_profiles.append(medication_profile) - return medication_profile - - def _medication_request(self, medicine): - id = str(uuid()) - prescription_date = ( - self.consultation.encounter_date.isoformat() - ) # TODO: change to the time of prescription - status = "unknown" # TODO: get correct status active | on-hold | cancelled | completed | entered-in-error | stopped | draft | unknown - dosage_text = f"{medicine['dosage_new']} / {medicine['dosage']} for {medicine['days']} days" - - medication_profile = self._medication(medicine["medicine"]) - medication_request_profile = MedicationRequest( - id=id, - identifier=[Identifier(value=id)], - status=status, - intent="order", - authoredOn=prescription_date, - dosageInstruction=[Dosage(text=dosage_text)], - medicationReference=self._reference(medication_profile), - subject=self._reference(self._patient()), - requester=self._reference(self._practioner()), - ) - - self._medication_request_profiles.append(medication_request_profile) - return medication_profile, medication_request_profile - - def _prescription_composition(self): - id = str(uuid()) # TODO: use identifiable id - return Composition( - id=id, - identifier=Identifier(value=id), - status="final", # TODO: use appropriate one - type=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="440545006", - display="Prescription record", - ) - ] - ), - title="Prescription", - date=datetime.now(UTC).isoformat(), - section=[ - CompositionSection( - title="In Patient Prescriptions", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="440545006", - display="Prescription record", - ) - ] - ), - entry=list( - map( - lambda medicine: self._reference( - self._medication_request(medicine)[1] - ), - self.consultation.discharge_advice, - ) - ), - ) - ], - subject=self._reference(self._patient()), - encounter=self._reference(self._encounter()), - author=[self._reference(self._organization())], - ) - - def _health_document_composition(self): - id = str(uuid()) # TODO: use identifiable id - return Composition( - id=id, - identifier=Identifier(value=id), - status="final", # TODO: use appropriate one - type=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="419891008", - display="Record artifact", - ) - ] - ), - title="Health Document Record", - date=datetime.now(UTC).isoformat(), - section=[ - CompositionSection( - title="Health Document Record", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="419891008", - display="Record artifact", - ) - ] - ), - entry=list( - map( - lambda file: self._reference( - self._document_reference(file) - ), - FileUpload.objects.filter( - associating_id=self.consultation.id - ), - ) - ), - ) - ], - subject=self._reference(self._patient()), - encounter=self._reference(self._encounter()), - author=[self._reference(self._organization())], - ) - - def _wellness_composition(self): - id = str(uuid()) # TODO: use identifiable id - return Composition( - id=id, - identifier=Identifier(value=id), - status="final", # TODO: use appropriate one - type=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - display="Wellness Record", - ) - ] - ), - title="Wellness Record", - date=datetime.now(UTC).isoformat(), - section=list( - map( - lambda daily_round: CompositionSection( - title=f"Daily Round - {daily_round.created_date}", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - display="Wellness Record", - ) - ] - ), - entry=list( - map( - lambda observation_profile: self._reference( - observation_profile - ), - self._observations_from_daily_round(daily_round), - ) - ), - ), - self.consultation.daily_rounds.all(), - ) - ), - subject=self._reference(self._patient()), - encounter=self._reference(self._encounter()), - author=[self._reference(self._organization())], - ) - - def _immunization_composition(self): - id = str(uuid()) # TODO: use identifiable id - return Composition( - id=id, - identifier=Identifier(value=id), - status="final", # TODO: use appropriate one - type=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="41000179103", - display="Immunization Record", - ), - ], - ), - title="Immunization", - date=datetime.now(UTC).isoformat(), - section=[ - CompositionSection( - title="IPD Immunization", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="41000179103", - display="Immunization Record", - ), - ], - ), - entry=[ - *( - [self._reference(self._immunization())] - if self._immunization() - else [] - ) - ], - emptyReason=( - None - if self._immunization() - else CodeableConcept( - coding=[Coding(code="notasked", display="Not Asked")] - ) - ), - ), - ], - subject=self._reference(self._patient()), - encounter=self._reference(self._encounter()), - author=[self._reference(self._organization())], - ) - - def _diagnostic_report_composition(self): - id = str(uuid()) # TODO: use identifiable id - return Composition( - id=id, - identifier=Identifier(value=id), - status="final", # TODO: use appropriate one - type=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="721981007", - display="Diagnostic Report", - ), - ], - ), - title="Diagnostic Report", - date=datetime.now(UTC).isoformat(), - section=[ - CompositionSection( - title="Investigation Report", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="721981007", - display="Diagnostic Report", - ), - ], - ), - entry=[self._reference(self._diagnostic_report())], - ), - ], - subject=self._reference(self._patient()), - encounter=self._reference(self._encounter()), - author=[self._reference(self._organization())], - ) - - def _discharge_summary_composition(self): - id = str(uuid()) # TODO: use identifiable id - return Composition( - id=id, - identifier=Identifier(value=id), - status="final", # TODO: use appropriate one - type=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="373942005", - display="Discharge Summary Record", - ) - ] - ), - title="Discharge Summary Document", - date=datetime.now(UTC).isoformat(), - section=[ - CompositionSection( - title="Prescribed medications", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="440545006", - display="Prescription", - ) - ] - ), - entry=list( - map( - lambda medicine: self._reference( - self._medication_request(medicine)[1] - ), - self.consultation.discharge_advice, - ) - ), - ), - CompositionSection( - title="Health Documents", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="419891008", - display="Record", - ) - ] - ), - entry=list( - map( - lambda file: self._reference( - self._document_reference(file) - ), - FileUpload.objects.filter( - associating_id=self.consultation.id - ), - ) - ), - ), - *list( - map( - lambda daily_round: CompositionSection( - title=f"Daily Round - {daily_round.created_date}", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - display="Wellness Record", - ) - ] - ), - entry=list( - map( - lambda observation_profile: self._reference( - observation_profile - ), - self._observations_from_daily_round(daily_round), - ) - ), - ), - self.consultation.daily_rounds.all(), - ) - ), - CompositionSection( - title="Procedures", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="371525003", - display="Clinical procedure report", - ) - ] - ), - entry=list( - map( - lambda procedure: self._reference( - self._procedure(procedure) - ), - self.consultation.procedure, - ) - ), - ), - CompositionSection( - title="Care Plan", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="734163000", - display="Care Plan", - ) - ] - ), - entry=[self._reference(self._careplan())], - ), - ], - subject=self._reference(self._patient()), - encounter=self._reference(self._encounter(include_diagnosis=True)), - author=[self._reference(self._organization())], - ) - - def _op_consultation_composition(self): - id = str(uuid()) # TODO: use identifiable id - return Composition( - id=id, - identifier=Identifier(value=id), - status="final", # TODO: use appropriate one - type=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="371530004", - display="Clinical consultation report", - ) - ] - ), - title="OP Consultation Document", - date=datetime.now(UTC).isoformat(), - section=[ - CompositionSection( - title="Prescribed medications", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="440545006", - display="Prescription", - ) - ] - ), - entry=list( - map( - lambda medicine: self._reference( - self._medication_request(medicine)[1] - ), - self.consultation.discharge_advice, - ) - ), - ), - CompositionSection( - title="Health Documents", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="419891008", - display="Record", - ) - ] - ), - entry=list( - map( - lambda file: self._reference( - self._document_reference(file) - ), - FileUpload.objects.filter( - associating_id=self.consultation.id - ), - ) - ), - ), - *list( - map( - lambda daily_round: CompositionSection( - title=f"Daily Round - {daily_round.created_date}", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - display="Wellness Record", - ) - ] - ), - entry=list( - map( - lambda observation_profile: self._reference( - observation_profile - ), - self._observations_from_daily_round(daily_round), - ) - ), - ), - self.consultation.daily_rounds.all(), - ) - ), - CompositionSection( - title="Procedures", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="371525003", - display="Clinical procedure report", - ) - ] - ), - entry=list( - map( - lambda procedure: self._reference( - self._procedure(procedure) - ), - self.consultation.procedure, - ) - ), - ), - CompositionSection( - title="Care Plan", - code=CodeableConcept( - coding=[ - Coding( - system="https://projecteka.in/sct", - code="734163000", - display="Care Plan", - ) - ] - ), - entry=[self._reference(self._careplan())], - ), - ], - subject=self._reference(self._patient()), - encounter=self._reference(self._encounter(include_diagnosis=True)), - author=[self._reference(self._organization())], - ) - - def _bundle_entry(self, resource): - return BundleEntry(fullUrl=self._reference_url(resource), resource=resource) - - def create_prescription_record(self): - id = str(uuid()) - now = datetime.now(UTC).isoformat() - return Bundle( - id=id, - identifier=Identifier(value=id), - type="document", - meta=Meta(lastUpdated=now), - timestamp=now, - entry=[ - self._bundle_entry(self._prescription_composition()), - self._bundle_entry(self._practioner()), - self._bundle_entry(self._patient()), - self._bundle_entry(self._organization()), - self._bundle_entry(self._encounter()), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._medication_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._medication_request_profiles, - ) - ), - ], - ).json() - - def create_wellness_record(self): - id = str(uuid()) - now = datetime.now(UTC).isoformat() - return Bundle( - id=id, - identifier=Identifier(value=id), - type="document", - meta=Meta(lastUpdated=now), - timestamp=now, - entry=[ - self._bundle_entry(self._wellness_composition()), - self._bundle_entry(self._practioner()), - self._bundle_entry(self._patient()), - self._bundle_entry(self._organization()), - self._bundle_entry(self._encounter()), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._observation_profiles, - ) - ), - ], - ).json() - - def create_immunization_record(self): - id = str(uuid()) - now = datetime.now(UTC).isoformat() - return Bundle( - id=id, - identifier=Identifier(value=id), - type="document", - meta=Meta(lastUpdated=now), - timestamp=now, - entry=[ - self._bundle_entry(self._immunization_composition()), - self._bundle_entry(self._practioner()), - self._bundle_entry(self._patient()), - self._bundle_entry(self._organization()), - self._bundle_entry(self._encounter()), - self._bundle_entry(self._immunization()), - ], - ).json() - - def create_diagnostic_report_record(self): - id = str(uuid()) - now = datetime.now(UTC).isoformat() - return Bundle( - id=id, - identifier=Identifier(value=id), - type="document", - meta=Meta(lastUpdated=now), - timestamp=now, - entry=[ - self._bundle_entry(self._diagnostic_report_composition()), - self._bundle_entry(self._practioner()), - self._bundle_entry(self._patient()), - self._bundle_entry(self._organization()), - self._bundle_entry(self._encounter()), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._observation_profiles, - ) - ), - ], - ).json() - - def create_health_document_record(self): - id = str(uuid()) - now = datetime.now(UTC).isoformat() - return Bundle( - id=id, - identifier=Identifier(value=id), - type="document", - meta=Meta(lastUpdated=now), - timestamp=now, - entry=[ - self._bundle_entry(self._health_document_composition()), - self._bundle_entry(self._practioner()), - self._bundle_entry(self._patient()), - self._bundle_entry(self._organization()), - self._bundle_entry(self._encounter()), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._document_reference_profiles, - ) - ), - ], - ).json() - - def create_discharge_summary_record(self): - id = str(uuid()) - now = datetime.now(UTC).isoformat() - return Bundle( - id=id, - identifier=Identifier(value=id), - type="document", - meta=Meta(lastUpdated=now), - timestamp=now, - entry=[ - self._bundle_entry(self._discharge_summary_composition()), - self._bundle_entry(self._practioner()), - self._bundle_entry(self._patient()), - self._bundle_entry(self._organization()), - self._bundle_entry(self._encounter()), - self._bundle_entry(self._careplan()), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._medication_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._medication_request_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._condition_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._procedure_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._document_reference_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._observation_profiles, - ) - ), - ], - ).json() - - def create_op_consultation_record(self): - id = str(uuid()) - now = datetime.now(UTC).isoformat() - return Bundle( - id=id, - identifier=Identifier(value=id), - type="document", - meta=Meta(lastUpdated=now), - timestamp=now, - entry=[ - self._bundle_entry(self._op_consultation_composition()), - self._bundle_entry(self._practioner()), - self._bundle_entry(self._patient()), - self._bundle_entry(self._organization()), - self._bundle_entry(self._encounter()), - self._bundle_entry(self._careplan()), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._medication_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._medication_request_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._condition_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._procedure_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._document_reference_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._observation_profiles, - ) - ), - ], - ).json() - - def create_record(self, record_type): - if record_type == "Prescription": - return self.create_prescription_record() - if record_type == "WellnessRecord": - return self.create_wellness_record() - if record_type == "ImmunizationRecord": - return self.create_immunization_record() - if record_type == "HealthDocumentRecord": - return self.create_health_document_record() - if record_type == "DiagnosticReport": - return self.create_diagnostic_report_record() - if record_type == "DischargeSummary": - return self.create_discharge_summary_record() - if record_type == "OPConsultation": - return self.create_op_consultation_record() - return self.create_discharge_summary_record() diff --git a/care/abdm/views.py b/care/abdm/views.py deleted file mode 100644 index 60f00ef0ef..0000000000 --- a/care/abdm/views.py +++ /dev/null @@ -1 +0,0 @@ -# Create your views here. diff --git a/care/facility/api/serializers/bed.py b/care/facility/api/serializers/bed.py index 41597e186d..031d2a68c1 100644 --- a/care/facility/api/serializers/bed.py +++ b/care/facility/api/serializers/bed.py @@ -111,6 +111,10 @@ def validate(self, attrs): not facilities.filter(id=asset.current_location.facility.id).exists() ) or (not facilities.filter(id=bed.facility.id).exists()): raise PermissionError + if AssetBed.objects.filter(asset=asset, bed=bed).exists(): + raise ValidationError( + {"non_field_errors": "Asset is already linked to bed"} + ) if asset.asset_class not in [ AssetClasses.HL7MONITOR.name, AssetClasses.ONVIF.name, @@ -127,11 +131,11 @@ def validate(self, attrs): and AssetBed.objects.filter( bed=bed, asset__asset_class=asset.asset_class ).exists() - ): + ) and AssetBed.objects.filter( + bed=bed, asset__asset_class=asset.asset_class + ).exists(): raise ValidationError( - { - "asset": "Bed is already in use by another asset of the same class" - } + {"asset": "Another HL7 Monitor is already linked to this bed."} ) else: raise ValidationError( diff --git a/care/facility/api/serializers/camera_preset.py b/care/facility/api/serializers/camera_preset.py new file mode 100644 index 0000000000..7157b5245a --- /dev/null +++ b/care/facility/api/serializers/camera_preset.py @@ -0,0 +1,49 @@ +from rest_framework import serializers +from rest_framework.exceptions import ValidationError + +from care.facility.api.serializers.bed import AssetBedSerializer +from care.facility.models import CameraPreset +from care.users.api.serializers.user import UserBaseMinimumSerializer + + +class CameraPresetSerializer(serializers.ModelSerializer): + id = serializers.UUIDField(source="external_id", read_only=True) + created_by = UserBaseMinimumSerializer(read_only=True) + updated_by = UserBaseMinimumSerializer(read_only=True) + asset_bed = AssetBedSerializer(read_only=True) + + class Meta: + model = CameraPreset + exclude = ( + "external_id", + "deleted", + ) + read_only_fields = ( + "created_date", + "modified_date", + "is_migrated", + "created_by", + "updated_by", + ) + + def get_asset_bed_obj(self): + return ( + self.instance.asset_bed if self.instance else self.context.get("asset_bed") + ) + + def validate_name(self, value): + if CameraPreset.objects.filter( + asset_bed__bed_id=self.get_asset_bed_obj().bed_id, name=value + ).exists(): + msg = "Name should be unique. Another preset related to this bed already uses the same name." + raise ValidationError(msg) + return value + + def create(self, validated_data): + validated_data["created_by"] = self.context["request"].user + validated_data["asset_bed"] = self.get_asset_bed_obj() + return super().create(validated_data) + + def update(self, instance, validated_data): + validated_data["updated_by"] = self.context["request"].user + return super().update(instance, validated_data) diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index 15dd00e2aa..8b24bebb51 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -58,10 +58,12 @@ AvailabilityRecord, StatusChoices, ) +from care.facility.models.bed import AssetBed, ConsultationBed from care.users.models import User from care.utils.assetintegration.asset_classes import AssetClasses from care.utils.cache.cache_allowed_facilities import get_accessible_facilities from care.utils.filters.choicefilter import CareChoiceFilter, inverse_choices +from care.utils.queryset.asset_bed import get_asset_queryset from care.utils.queryset.asset_location import get_asset_location_queryset from care.utils.queryset.facility import get_facility_queryset from config.authentication import MiddlewareAuthentication @@ -83,6 +85,27 @@ def delete_asset_cache(sender, instance, created, **kwargs): cache.delete("asset:qr:" + str(instance.id)) +class AssetLocationFilter(filters.FilterSet): + bed_is_occupied = filters.BooleanFilter(method="filter_bed_is_occupied") + + def filter_bed_is_occupied(self, queryset, name, value): + asset_locations = ( + AssetBed.objects.select_related("asset", "bed") + .filter(asset__asset_class=AssetClasses.HL7MONITOR.name) + .values_list("bed__location_id", "bed__id") + ) + if value: + asset_locations = asset_locations.filter( + bed__id__in=Subquery( + ConsultationBed.objects.filter( + bed__id=OuterRef("bed__id"), end_date__isnull=value + ).values("bed__id") + ) + ) + asset_locations = asset_locations.values_list("bed__location_id", flat=True) + return queryset.filter(id__in=asset_locations) + + class AssetLocationViewSet( ListModelMixin, RetrieveModelMixin, @@ -100,8 +123,9 @@ class AssetLocationViewSet( ) serializer_class = AssetLocationSerializer lookup_field = "external_id" - filter_backends = (drf_filters.SearchFilter,) + filter_backends = (filters.DjangoFilterBackend, drf_filters.SearchFilter) search_fields = ["name"] + filterset_class = AssetLocationFilter def get_serializer_context(self): facility = self.get_facility() @@ -290,21 +314,7 @@ class AssetViewSet( filterset_class = AssetFilter def get_queryset(self): - user = self.request.user - queryset = self.queryset - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(current_location__facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter( - current_location__facility__district=user.district - ) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter( - current_location__facility__id__in=allowed_facilities - ) + queryset = get_asset_queryset(user=self.request.user, queryset=self.queryset) return queryset.annotate( latest_status=Subquery( AvailabilityRecord.objects.filter( diff --git a/care/facility/api/viewsets/bed.py b/care/facility/api/viewsets/bed.py index 336b5f83c2..db9dd6652f 100644 --- a/care/facility/api/viewsets/bed.py +++ b/care/facility/api/viewsets/bed.py @@ -30,6 +30,7 @@ from care.users.models import User from care.utils.cache.cache_allowed_facilities import get_accessible_facilities from care.utils.filters.choicefilter import CareChoiceFilter, inverse_choices +from care.utils.queryset.asset_bed import get_asset_bed_queryset, get_bed_queryset inverse_bed_type = inverse_choices(BedTypeChoices) @@ -76,27 +77,14 @@ class BedViewSet( filterset_class = BedFilter def get_queryset(self): - user = self.request.user - queryset = self.queryset - - queryset = queryset.annotate( + queryset = self.queryset.annotate( is_occupied=Exists( ConsultationBed.objects.filter( bed__id=OuterRef("id"), end_date__isnull=True ) ) ) - - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter(facility__district=user.district) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter(facility__id__in=allowed_facilities) - return queryset + return get_bed_queryset(user=self.request.user, queryset=queryset) @transaction.atomic def create(self, request, *args, **kwargs): @@ -168,18 +156,7 @@ class AssetBedViewSet( lookup_field = "external_id" def get_queryset(self): - user = self.request.user - queryset = self.queryset - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(bed__facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter(bed__facility__district=user.district) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter(bed__facility__id__in=allowed_facilities) - return queryset + return get_asset_bed_queryset(user=self.request.user, queryset=self.queryset) class PatientAssetBedFilter(filters.FilterSet): @@ -212,20 +189,9 @@ class PatientAssetBedViewSet(ListModelMixin, GenericViewSet): ] def get_queryset(self): - user = self.request.user - queryset = self.queryset - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(bed__facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter(bed__facility__district=user.district) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter(bed__facility__id__in=allowed_facilities) - return queryset.filter( - bed__facility__external_id=self.kwargs["facility_external_id"] - ) + return get_asset_bed_queryset( + user=self.request.user, queryset=self.queryset + ).filter(bed__facility__external_id=self.kwargs["facility_external_id"]) class ConsultationBedFilter(filters.FilterSet): diff --git a/care/facility/api/viewsets/camera_preset.py b/care/facility/api/viewsets/camera_preset.py new file mode 100644 index 0000000000..bfb168834b --- /dev/null +++ b/care/facility/api/viewsets/camera_preset.py @@ -0,0 +1,63 @@ +from django.shortcuts import get_object_or_404 +from rest_framework.exceptions import NotFound +from rest_framework.mixins import ListModelMixin +from rest_framework.permissions import IsAuthenticated +from rest_framework.viewsets import GenericViewSet, ModelViewSet + +from care.facility.api.serializers.camera_preset import CameraPresetSerializer +from care.facility.models import CameraPreset +from care.utils.queryset.asset_bed import ( + get_asset_bed_queryset, + get_asset_queryset, + get_bed_queryset, +) + + +class AssetBedCameraPresetViewSet(ModelViewSet): + serializer_class = CameraPresetSerializer + queryset = CameraPreset.objects.all().select_related( + "asset_bed", "created_by", "updated_by" + ) + lookup_field = "external_id" + permission_classes = (IsAuthenticated,) + + def get_asset_bed_obj(self): + queryset = get_asset_bed_queryset(self.request.user).filter( + external_id=self.kwargs["assetbed_external_id"] + ) + return get_object_or_404(queryset) + + def get_queryset(self): + return super().get_queryset().filter(asset_bed=self.get_asset_bed_obj()) + + def get_serializer_context(self): + context = super().get_serializer_context() + context["asset_bed"] = self.get_asset_bed_obj() + return context + + +class CameraPresetViewSet(GenericViewSet, ListModelMixin): + serializer_class = CameraPresetSerializer + queryset = CameraPreset.objects.all().select_related( + "asset_bed", "created_by", "updated_by" + ) + lookup_field = "external_id" + permission_classes = (IsAuthenticated,) + + def get_bed_obj(self, external_id: str): + queryset = get_bed_queryset(self.request.user).filter(external_id=external_id) + return get_object_or_404(queryset) + + def get_asset_obj(self, external_id: str): + queryset = get_asset_queryset(self.request.user).filter(external_id=external_id) + return get_object_or_404(queryset) + + def get_queryset(self): + queryset = super().get_queryset() + if asset_external_id := self.kwargs.get("asset_external_id"): + return queryset.filter( + asset_bed__asset=self.get_asset_obj(asset_external_id) + ) + if bed_external_id := self.kwargs.get("bed_external_id"): + return queryset.filter(asset_bed__bed=self.get_bed_obj(bed_external_id)) + raise NotFound diff --git a/care/facility/api/viewsets/facility.py b/care/facility/api/viewsets/facility.py index dd8221c0cb..1f0ac69442 100644 --- a/care/facility/api/viewsets/facility.py +++ b/care/facility/api/viewsets/facility.py @@ -204,3 +204,19 @@ def get_serializer_context(self): context = super().get_serializer_context() context["facility"] = facility return context + + +class FacilityHubsViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + queryset = FacilityHubSpoke.objects.all().select_related("spoke", "hub") + serializer_class = FacilitySpokeSerializer + permission_classes = (IsAuthenticated,) + lookup_field = "external_id" + + def get_queryset(self): + return self.queryset.filter(spoke=self.get_facility()) + + def get_facility(self): + facilities = get_facility_queryset(self.request.user) + return get_object_or_404( + facilities.filter(external_id=self.kwargs["facility_external_id"]) + ) diff --git a/care/facility/management/commands/load_dummy_data.py b/care/facility/management/commands/load_dummy_data.py index 4a0633801c..ea57bf17a6 100644 --- a/care/facility/management/commands/load_dummy_data.py +++ b/care/facility/management/commands/load_dummy_data.py @@ -2,6 +2,13 @@ from django.core import management from django.core.management import BaseCommand, CommandError +from django.db.models.signals import ( + m2m_changed, + post_delete, + post_save, + pre_delete, + pre_save, +) class Command(BaseCommand): @@ -20,6 +27,20 @@ def handle(self, *args, **options): msg = "This command is not intended to be run in production environment." raise CommandError(msg) + # Disconnecting signals temporarily to avoid conflicts + signals_to_disconnect = [ + post_save, + post_delete, + pre_save, + pre_delete, + m2m_changed, + ] + original_receivers = {} + + for signal in signals_to_disconnect: + original_receivers[signal] = signal.receivers + signal.receivers = [] + try: management.call_command("loaddata", self.BASE_URL + "states.json") management.call_command("load_skill_data") @@ -32,3 +53,7 @@ def handle(self, *args, **options): management.call_command("populate_investigations") except Exception as e: raise CommandError(e) from e + finally: + # Reconnect original signals + for signal in signals_to_disconnect: + signal.receivers = original_receivers[signal] diff --git a/care/facility/management/commands/load_redis_index.py b/care/facility/management/commands/load_redis_index.py index 736f482836..e24175a8bf 100644 --- a/care/facility/management/commands/load_redis_index.py +++ b/care/facility/management/commands/load_redis_index.py @@ -17,6 +17,17 @@ class Command(BaseCommand): help = "Loads static data to redis" def handle(self, *args, **options): + try: + deleted_count = cache.delete_pattern("care_static_data*", itersize=25_000) + self.stdout.write( + f"Deleted {deleted_count} keys with prefix 'care_static_data'" + ) + except Exception as e: + self.stdout.write( + f"Failed to delete keys with prefix 'care_static_data': {e}" + ) + return + if cache.get("redis_index_loading"): self.stdout.write("Redis Index already loading, skipping") return diff --git a/care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py b/care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py index 304bebb163..194458e274 100644 --- a/care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py +++ b/care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ("abdm", "0001_initial_squashed_0007_alter_abhanumber_id"), + # ("abdm", "0001_initial_squashed_0007_alter_abhanumber_id"), ("facility", "0373_remove_patientconsultation_hba1c"), ] @@ -22,23 +22,17 @@ class Migration(migrations.Migration): migrations.AddField( model_name="historicalpatientregistration", name="abha_number", - field=models.ForeignKey( + field=models.IntegerField( blank=True, - db_constraint=False, null=True, - on_delete=django.db.models.deletion.DO_NOTHING, - related_name="+", - to="abdm.abhanumber", ), ), migrations.AddField( model_name="patientregistration", name="abha_number", - field=models.OneToOneField( + field=models.IntegerField( blank=True, null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="abdm.abhanumber", ), ), ] diff --git a/care/facility/migrations/0454_remove_historicalpatientregistration_abha_number_and_more.py b/care/facility/migrations/0454_remove_historicalpatientregistration_abha_number_and_more.py index 4dc7fd5054..9e10ce90d1 100644 --- a/care/facility/migrations/0454_remove_historicalpatientregistration_abha_number_and_more.py +++ b/care/facility/migrations/0454_remove_historicalpatientregistration_abha_number_and_more.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ ("facility", "0453_merge_20240824_2040"), - ("abdm", "0013_abhanumber_patient"), + # ("abdm", "0013_abhanumber_patient"), ] operations = [ diff --git a/care/facility/migrations/0466_camera_presets.py b/care/facility/migrations/0466_camera_presets.py new file mode 100644 index 0000000000..8ee6942342 --- /dev/null +++ b/care/facility/migrations/0466_camera_presets.py @@ -0,0 +1,169 @@ +# Generated by Django 4.2.8 on 2024-05-30 06:56 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.core.paginator import Paginator +from django.db import migrations, models +from django.db.models import F, Window +from django.db.models.functions import RowNumber + +import care.utils.models.validators + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("facility", "0465_merge_20240923_1045"), + ] + + def delete_asset_beds_without_asset_class(apps, schema_editor): + AssetBed = apps.get_model("facility", "AssetBed") + AssetBed.objects.filter(asset__asset_class__isnull=True).delete() + + def backfill_camera_presets(apps, schema_editor): + AssetBed = apps.get_model("facility", "AssetBed") + CameraPreset = apps.get_model("facility", "CameraPreset") + + paginator = Paginator( + AssetBed.objects.annotate( + row_number=Window( + expression=RowNumber(), + partition_by=[F("asset"), F("bed")], + order_by=F("id").asc(), + ) + ) + .filter(deleted=False, asset__asset_class="ONVIF") + .order_by("asset", "bed", "id"), + 1000, + ) + + for page_number in paginator.page_range: + assetbeds_to_delete = [] + presets_to_create = [] + + for asset_bed in paginator.page(page_number).object_list: + name = asset_bed.meta.get("preset_name") + + if position := asset_bed.meta.get("position"): + try: + presets_to_create.append( + CameraPreset( + name=name, + asset_bed=AssetBed.objects.filter( + asset=asset_bed.asset, bed=asset_bed.bed + ).order_by("id")[0], + position={ + "x": float(position["x"]), + "y": float(position["y"]), + "zoom": float(position["zoom"]), + }, + is_migrated=True, + ) + ) + except: + pass + if asset_bed.row_number != 1: + assetbeds_to_delete.append(asset_bed.id) + else: + assetbeds_to_delete.append(asset_bed.id) + + CameraPreset.objects.bulk_create(presets_to_create) + AssetBed.objects.filter(id__in=assetbeds_to_delete).update(deleted=True) + + operations = [ + migrations.CreateModel( + name="CameraPreset", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "external_id", + models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + ( + "created_date", + models.DateTimeField(auto_now_add=True, db_index=True, null=True), + ), + ( + "modified_date", + models.DateTimeField(auto_now=True, db_index=True, null=True), + ), + ("deleted", models.BooleanField(db_index=True, default=False)), + ("name", models.CharField(max_length=255, null=True)), + ( + "position", + models.JSONField( + validators=[ + care.utils.models.validators.JSONFieldSchemaValidator( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": False, + "properties": { + "x": {"type": "number"}, + "y": {"type": "number"}, + "zoom": {"type": "number"}, + }, + "required": ["x", "y", "zoom"], + "type": "object", + } + ) + ], + ), + ), + ("is_migrated", models.BooleanField(default=False)), + ( + "asset_bed", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="camera_presets", + to="facility.assetbed", + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.RunPython( + delete_asset_beds_without_asset_class, + migrations.RunPython.noop, + ), + migrations.RunPython( + backfill_camera_presets, + migrations.RunPython.noop, + ), + migrations.AddConstraint( + model_name="assetbed", + constraint=models.UniqueConstraint( + condition=models.Q(("deleted", False)), + fields=("asset", "bed"), + name="unique_together_asset_bed", + ), + ), + ] diff --git a/care/facility/migrations/0467_alter_hospitaldoctors_area.py b/care/facility/migrations/0467_alter_hospitaldoctors_area.py new file mode 100644 index 0000000000..d1507f0334 --- /dev/null +++ b/care/facility/migrations/0467_alter_hospitaldoctors_area.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.1 on 2024-10-28 13:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('facility', '0466_camera_presets'), + ] + + operations = [ + migrations.AlterField( + model_name='hospitaldoctors', + name='area', + field=models.IntegerField(choices=[(1, 'General Medicine'), (2, 'Pulmonology'), (3, 'Intensivist'), (4, 'Pediatrician'), (5, 'Others'), (6, 'Anesthesiologist'), (7, 'Cardiac Surgeon'), (8, 'Cardiologist'), (9, 'Dentist'), (10, 'Dermatologist'), (11, 'Diabetologist'), (12, 'Emergency Medicine Physician'), (13, 'Endocrinologist'), (14, 'Family Physician'), (15, 'Gastroenterologist'), (16, 'General Surgeon'), (17, 'Geriatrician'), (18, 'Hematologist'), (19, 'Immunologist'), (20, 'Infectious Disease Specialist'), (21, 'MBBS doctor'), (22, 'Medical Officer'), (23, 'Nephrologist'), (24, 'Neuro Surgeon'), (25, 'Neurologist'), (26, 'Obstetrician/Gynecologist (OB/GYN)'), (27, 'Oncologist'), (28, 'Oncology Surgeon'), (29, 'Ophthalmologist'), (30, 'Oral and Maxillofacial Surgeon'), (31, 'Orthopedic'), (32, 'Orthopedic Surgeon'), (33, 'Otolaryngologist (ENT)'), (34, 'Palliative care Physician'), (35, 'Pathologist'), (36, 'Pediatric Surgeon'), (37, 'Physician'), (38, 'Plastic Surgeon'), (39, 'Psychiatrist'), (40, 'Pulmonologist'), (41, 'Radio technician'), (42, 'Radiologist'), (43, 'Rheumatologist'), (44, 'Sports Medicine Specialist'), (45, 'Thoraco-Vascular Surgeon'), (46, 'Transfusion Medicine Specialist'), (47, 'Urologist'), (48, 'Nurse'), (49, 'Allergist/Immunologist'), (50, 'Cardiothoracic Surgeon'), (51, 'Gynecologic Oncologist'), (52, 'Hepatologist'), (53, 'Internist'), (54, 'Neonatologist'), (55, 'Pain Management Specialist'), (56, 'Physiatrist (Physical Medicine and Rehabilitation)'), (57, 'Podiatrist'), (58, 'Preventive Medicine Specialist'), (59, 'Radiation Oncologist'), (60, 'Sleep Medicine Specialist'), (61, 'Transplant Surgeon'), (62, 'Trauma Surgeon'), (63, 'Vascular Surgeon'), (64, 'Critical Care Physician')]), + ), + ] diff --git a/care/facility/models/__init__.py b/care/facility/models/__init__.py index df41476768..d6d63cacca 100644 --- a/care/facility/models/__init__.py +++ b/care/facility/models/__init__.py @@ -4,6 +4,7 @@ from .ambulance import * # noqa from .asset import * # noqa from .bed import * # noqa +from .camera_preset import * # noqa from .daily_round import * # noqa from .encounter_symptom import * # noqa from .events import * # noqa diff --git a/care/facility/models/bed.py b/care/facility/models/bed.py index a06db2729c..992f36ac74 100644 --- a/care/facility/models/bed.py +++ b/care/facility/models/bed.py @@ -68,9 +68,22 @@ class AssetBed(BaseModel): bed = models.ForeignKey(Bed, on_delete=models.PROTECT, null=False, blank=False) meta = JSONField(default=dict, blank=True) + class Meta: + constraints = [ + models.UniqueConstraint( + name="unique_together_asset_bed", + fields=("asset", "bed"), + condition=models.Q(deleted=False), + ), + ] + def __str__(self): return f"{self.asset.name} - {self.bed.name}" + def delete(self, *args): + self.camera_presets.update(deleted=True) + return super().delete(*args) + class ConsultationBed(BaseModel): consultation = models.ForeignKey( diff --git a/care/facility/models/camera_preset.py b/care/facility/models/camera_preset.py new file mode 100644 index 0000000000..b1128f8817 --- /dev/null +++ b/care/facility/models/camera_preset.py @@ -0,0 +1,33 @@ +from django.db import models + +from care.utils.models.base import BaseModel +from care.utils.models.validators import JSONFieldSchemaValidator + +CAMERA_PRESET_POSITION_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "x": {"type": "number"}, + "y": {"type": "number"}, + "zoom": {"type": "number"}, + }, + "required": ["x", "y", "zoom"], + "additionalProperties": False, +} + + +class CameraPreset(BaseModel): + name = models.CharField(max_length=255, null=True) + asset_bed = models.ForeignKey( + "facility.AssetBed", on_delete=models.PROTECT, related_name="camera_presets" + ) + position = models.JSONField( + validators=[JSONFieldSchemaValidator(CAMERA_PRESET_POSITION_SCHEMA)] + ) + created_by = models.ForeignKey( + "users.User", null=True, blank=True, on_delete=models.PROTECT, related_name="+" + ) + updated_by = models.ForeignKey( + "users.User", null=True, blank=True, on_delete=models.PROTECT, related_name="+" + ) + is_migrated = models.BooleanField(default=False) diff --git a/care/facility/models/facility.py b/care/facility/models/facility.py index e3593871b7..01c0102b10 100644 --- a/care/facility/models/facility.py +++ b/care/facility/models/facility.py @@ -148,7 +148,7 @@ class FacilityFeature(models.IntegerChoices): (16, "General Surgeon"), (17, "Geriatrician"), (18, "Hematologist"), - (29, "Immunologist"), + (19, "Immunologist"), (20, "Infectious Disease Specialist"), (21, "MBBS doctor"), (22, "Medical Officer"), diff --git a/care/facility/tasks/cleanup.py b/care/facility/tasks/cleanup.py index 3f913142cc..4fca52eddc 100644 --- a/care/facility/tasks/cleanup.py +++ b/care/facility/tasks/cleanup.py @@ -1,6 +1,7 @@ from datetime import timedelta from celery import shared_task +from django.conf import settings from django.utils import timezone from care.facility.models.notification import Notification @@ -8,5 +9,7 @@ @shared_task def delete_old_notifications(): - ninety_days_ago = timezone.now() - timedelta(days=90) - Notification.objects.filter(created_date__lte=ninety_days_ago).delete() + retention_days = settings.NOTIFICATION_RETENTION_DAYS + + threshold_date = timezone.now() - timedelta(days=retention_days) + Notification.objects.filter(created_date__lte=threshold_date).delete() diff --git a/care/facility/tasks/redis_index.py b/care/facility/tasks/redis_index.py index 306fc1352c..901c1be22d 100644 --- a/care/facility/tasks/redis_index.py +++ b/care/facility/tasks/redis_index.py @@ -15,6 +15,13 @@ @shared_task def load_redis_index(): + try: + deleted_count = cache.delete_pattern("care_static_data*", itersize=25_000) + logger.info("Deleted %s keys with prefix 'care_static_data'", deleted_count) + except Exception as e: + logger.error("Failed to delete keys with prefix 'care_static_data': %s", e) + return + if cache.get("redis_index_loading"): logger.info("Redis Index already loading, skipping") return @@ -37,9 +44,9 @@ def load_redis_index(): if load_static_data: load_static_data() except ModuleNotFoundError: - logger.info("Module %s not found", module_path) + logger.debug("Module %s not found", module_path) except Exception as e: - logger.info("Error loading static data for %s: %s", plug.name, e) + logger.error("Error loading static data for %s: %s", plug.name, e) cache.delete("redis_index_loading") logger.info("Redis Index Loaded") diff --git a/care/facility/tests/test_asset_bed_api.py b/care/facility/tests/test_asset_bed_api.py new file mode 100644 index 0000000000..4ed81a36b8 --- /dev/null +++ b/care/facility/tests/test_asset_bed_api.py @@ -0,0 +1,219 @@ +from rest_framework import status +from rest_framework.test import APITestCase + +from care.users.models import User +from care.utils.assetintegration.asset_classes import AssetClasses +from care.utils.tests.test_utils import TestUtils + + +class AssetBedViewSetTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls): + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.user = cls.create_user( + User.TYPE_VALUE_MAP["DistrictAdmin"], + cls.district, + home_facility=cls.facility, + ) + cls.asset_location = cls.create_asset_location(cls.facility) + cls.asset = cls.create_asset(cls.asset_location) + cls.monitor_asset_1 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.HL7MONITOR.name + ) + cls.monitor_asset_2 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.HL7MONITOR.name + ) + cls.camera_asset = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name + ) + cls.camera_asset_1 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name, name="Camera 1" + ) + cls.camera_asset_2 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name, name="Camera 2" + ) + cls.bed = cls.create_bed(cls.facility, cls.asset_location) + + def test_link_disallowed_asset_class_asset_to_bed(self): + data = { + "asset": self.asset.external_id, + "bed": self.bed.external_id, + } + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_link_asset_to_bed_and_attempt_duplicate_linking(self): + data = { + "asset": self.camera_asset.external_id, + "bed": self.bed.external_id, + } + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + # Attempt linking same camera to the same bed again. + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + # List asset beds filtered by asset and bed ID and check only 1 result exists + res = self.client.get("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertEqual(res.data["count"], 1) + + def test_linking_multiple_cameras_to_a_bed(self): + data = { + "asset": self.camera_asset_1.external_id, + "bed": self.bed.external_id, + } + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + # Attempt linking another camera to same bed. + data["asset"] = self.camera_asset_2.external_id + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + + def test_linking_multiple_hl7_monitors_to_a_bed(self): + data = { + "asset": self.monitor_asset_1.external_id, + "bed": self.bed.external_id, + } + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + # Attempt linking another hl7 monitor to same bed. + data["asset"] = self.monitor_asset_2.external_id + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + +class AssetBedCameraPresetViewSetTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls): + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.user = cls.create_user( + User.TYPE_VALUE_MAP["DistrictAdmin"], + cls.district, + home_facility=cls.facility, + ) + cls.asset_location = cls.create_asset_location(cls.facility) + cls.asset1 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name + ) + cls.asset2 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name + ) + cls.bed = cls.create_bed(cls.facility, cls.asset_location) + cls.asset_bed1 = cls.create_asset_bed(cls.asset1, cls.bed) + cls.asset_bed2 = cls.create_asset_bed(cls.asset2, cls.bed) + + def get_base_url(self, asset_bed_id=None): + return f"/api/v1/assetbed/{asset_bed_id or self.asset_bed1.external_id}/camera_presets/" + + def test_create_camera_preset_without_position(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset without position", + "position": {}, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_missing_required_keys_in_position(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": {"key": "value"}, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_position_not_number(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": { + "x": "not a number", + "y": 1, + "zoom": 1, + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_position_values_as_string(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": { + "x": "1", + "y": "1", + "zoom": "1", + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_and_presence_in_various_preset_list_apis(self): + asset_bed = self.asset_bed1 + res = self.client.post( + self.get_base_url(asset_bed.external_id), + { + "name": "Preset with proper position", + "position": { + "x": 1.0, + "y": 1.0, + "zoom": 1.0, + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + preset_external_id = res.data["id"] + + # Check if preset in asset-bed preset list + res = self.client.get(self.get_base_url(asset_bed.external_id)) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + # Check if preset in asset preset list + res = self.client.get( + f"/api/v1/asset/{asset_bed.asset.external_id}/camera_presets/" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + # Check if preset in bed preset list + res = self.client.get( + f"/api/v1/bed/{asset_bed.bed.external_id}/camera_presets/" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + def test_create_camera_preset_with_same_name_in_same_bed(self): + data = { + "name": "Duplicate Preset Name", + "position": { + "x": 1.0, + "y": 1.0, + "zoom": 1.0, + }, + } + self.client.post( + self.get_base_url(self.asset_bed1.external_id), data, format="json" + ) + res = self.client.post( + self.get_base_url(self.asset_bed2.external_id), data, format="json" + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/care/facility/tests/test_asset_location_api.py b/care/facility/tests/test_asset_location_api.py index c2f95b8940..9e8280d617 100644 --- a/care/facility/tests/test_asset_location_api.py +++ b/care/facility/tests/test_asset_location_api.py @@ -1,6 +1,7 @@ from rest_framework import status from rest_framework.test import APITestCase +from care.utils.assetintegration.asset_classes import AssetClasses from care.utils.tests.test_utils import TestUtils @@ -15,8 +16,12 @@ def setUpTestData(cls) -> None: cls.asset_location = cls.create_asset_location(cls.facility) cls.asset_location_with_linked_bed = cls.create_asset_location(cls.facility) cls.asset_location_with_linked_asset = cls.create_asset_location(cls.facility) - cls.asset = cls.create_asset(cls.asset_location_with_linked_asset) + cls.asset = cls.create_asset( + cls.asset_location_with_linked_asset, + asset_class=AssetClasses.HL7MONITOR.name, + ) cls.bed = cls.create_bed(cls.facility, cls.asset_location_with_linked_bed) + cls.asset_bed = cls.create_asset_bed(cls.asset, cls.bed) cls.patient = cls.create_patient(cls.district, cls.facility) cls.consultation = cls.create_consultation(cls.patient, cls.facility) cls.consultation_bed = cls.create_consultation_bed(cls.consultation, cls.bed) @@ -24,6 +29,16 @@ def setUpTestData(cls) -> None: cls.deleted_asset = cls.create_asset(cls.asset_location) cls.deleted_asset.deleted = True cls.deleted_asset.save() + cls.asset_second_location = cls.create_asset_location( + cls.facility, name="asset2 location" + ) + cls.asset_second = cls.create_asset( + cls.asset_second_location, asset_class=AssetClasses.HL7MONITOR.name + ) + cls.asset_bed_second = cls.create_bed(cls.facility, cls.asset_second_location) + cls.assetbed_second = cls.create_asset_bed( + cls.asset_second, cls.asset_bed_second + ) def test_list_asset_locations(self): response = self.client.get( @@ -31,6 +46,32 @@ def test_list_asset_locations(self): ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertContains(response, self.asset_location.external_id) + self.assertContains(response, self.asset_second_location.external_id) + + def test_asset_locations_get_monitors_all(self): + response = self.client.get( + f"/api/v1/facility/{self.facility.external_id}/asset_location/?bed_is_occupied=false" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertContains(response, self.asset_location_with_linked_bed.external_id) + self.assertContains(response, self.asset_second_location.external_id) + + def test_asset_locations_get_monitors_only_consultation_bed(self): + response = self.client.get( + f"/api/v1/facility/{self.facility.external_id}/asset_location/?bed_is_occupied=true" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertContains(response, self.asset_location_with_linked_bed.external_id) + + def test_asset_locations_get_only_monitors(self): + self.asset.asset_class = AssetClasses.VENTILATOR.name + self.asset.save() + response = self.client.get( + f"/api/v1/facility/{self.facility.external_id}/asset_location/?bed_is_occupied=false" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertContains(response, self.asset_second_location.external_id) + self.assertEqual(len(response.data["results"]), 1) def test_retrieve_asset_location(self): response = self.client.get( diff --git a/care/facility/tests/test_delete_older_notifications_task.py b/care/facility/tests/test_delete_older_notifications_task.py index 9f860770b2..70ded42e71 100644 --- a/care/facility/tests/test_delete_older_notifications_task.py +++ b/care/facility/tests/test_delete_older_notifications_task.py @@ -1,5 +1,6 @@ from datetime import timedelta +from django.conf import settings from django.test import TestCase from django.utils import timezone from freezegun import freeze_time @@ -10,8 +11,10 @@ class DeleteOldNotificationsTest(TestCase): def test_delete_old_notifications(self): - # notifications created 90 days ago - with freeze_time(timezone.now() - timedelta(days=90)): + retention_days = settings.NOTIFICATION_RETENTION_DAYS + + # notifications created at the threshold of retention + with freeze_time(timezone.now() - timedelta(days=retention_days)): notification1 = Notification.objects.create() notification2 = Notification.objects.create() diff --git a/care/facility/tests/test_facility_api.py b/care/facility/tests/test_facility_api.py index 800f45fb8e..b86cb0db86 100644 --- a/care/facility/tests/test_facility_api.py +++ b/care/facility/tests/test_facility_api.py @@ -229,6 +229,25 @@ def test_spoke_is_not_ancestor(self): ) self.assertIs(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_hubs_list(self): + facility_a = self.create_facility( + self.super_user, self.district, self.local_body + ) + facility_b = self.create_facility( + self.super_user, self.district, self.local_body + ) + + FacilityHubSpoke.objects.create(hub=facility_a, spoke=facility_b) + + self.client.force_authenticate(user=self.super_user) + response = self.client.get(f"/api/v1/facility/{facility_b.external_id}/hubs/") + self.assertIs(response.status_code, status.HTTP_200_OK) + data = response.json() + self.assertEqual(data["count"], 1) + self.assertEqual( + data["results"][0]["hub_object"]["id"], str(facility_a.external_id) + ) + class FacilityCoverImageTests(TestUtils, APITestCase): @classmethod diff --git a/care/abdm/__init__.py b/care/security/__init__.py similarity index 100% rename from care/abdm/__init__.py rename to care/security/__init__.py diff --git a/care/security/apps.py b/care/security/apps.py new file mode 100644 index 0000000000..9f29b9bd7a --- /dev/null +++ b/care/security/apps.py @@ -0,0 +1,11 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class SecurityConfig(AppConfig): + name = "care.security" + verbose_name = _("Security Management") + + def ready(self): + # import care.security.signals # noqa F401 + pass diff --git a/care/abdm/api/__init__.py b/care/security/authorization/__init__.py similarity index 100% rename from care/abdm/api/__init__.py rename to care/security/authorization/__init__.py diff --git a/care/security/authorization/base.py b/care/security/authorization/base.py new file mode 100644 index 0000000000..36db9c022e --- /dev/null +++ b/care/security/authorization/base.py @@ -0,0 +1,88 @@ +from care.security.permissions.base import PermissionController + + +class PermissionDeniedError(Exception): + pass + + +class AuthorizationHandler: + """ + This is the base class for Authorization Handlers + Authorization handler must define a list of actions that can be performed and define the methods that + actually perform the authorization action. + + All Authz methods would be of the signature ( user, obj , **kwargs ) + obj refers to the obj which the user is seeking permission to. obj can also be a string or any datatype as long + as the logic can handle the type. + + Queries are actions that return a queryset as the response. + """ + + actions = [] + queries = [] + + def check_permission(self, user, obj): + if not PermissionController.has_permission(user, obj): + raise PermissionDeniedError + + return PermissionController.has_permission(user, obj) + + +class AuthorizationController: + """ + This class abstracts all security related operations in care + This includes Checking if A has access to resource X, + Filtering query-sets for list based operations and so on. + Security Controller implicitly caches all cachable operations and expects it to be invalidated. + + SecurityController maintains a list of override Classes, When present, + The override classes are invoked first and then the predefined classes. + The overridden classes can choose to call the next function in the hierarchy if needed. + """ + + override_authz_controllers: list[ + AuthorizationHandler + ] = [] # The order is important + # Override Security Controllers will be defined from plugs + internal_authz_controllers: list[AuthorizationHandler] = [] + + cache = {} + + @classmethod + def build_cache(cls): + for controller in ( + cls.internal_authz_controllers + cls.override_authz_controllers + ): + for action in controller.actions: + if "actions" not in cls.cache: + cls.cache["actions"] = {} + cls.cache["actions"][action] = [ + *cls.cache["actions"].get(action, []), + controller, + ] + + @classmethod + def get_action_controllers(cls, action): + return cls.cache["actions"].get(action, []) + + @classmethod + def check_action_permission(cls, action, user, obj): + """ + TODO: Add Caching and capability to remove cache at both user and obj level + """ + if not cls.cache: + cls.build_cache() + controllers = cls.get_action_controllers(action) + for controller in controllers: + permission_fn = getattr(controller, action) + result, _continue = permission_fn(user, obj) + if not _continue: + return result + if not result: + return result + return True + + @classmethod + def register_internal_controller(cls, controller: AuthorizationHandler): + # TODO : Do some deduplication Logic + cls.internal_authz_controllers.append(controller) diff --git a/care/security/authorization/facility.py b/care/security/authorization/facility.py new file mode 100644 index 0000000000..11de91f2b7 --- /dev/null +++ b/care/security/authorization/facility.py @@ -0,0 +1,22 @@ +from care.abdm.utils.api_call import Facility +from care.facility.models import FacilityUser +from care.security.authorization.base import ( + AuthorizationHandler, + PermissionDeniedError, +) + + +class FacilityAccess(AuthorizationHandler): + actions = ["can_read_facility"] + queries = ["allowed_facilities"] + + def can_read_facility(self, user, facility_id): + self.check_permission(user, facility_id) + # Since the old method relied on a facility-user relationship, check that + # This can be removed when the migrations have been completed + if not FacilityUser.objects.filter(facility_id=facility_id, user=user).exists(): + raise PermissionDeniedError + return True, True + + def allowed_facilities(self, user): + return Facility.objects.filter(users__id__exact=user.id) diff --git a/care/abdm/migrations/__init__.py b/care/security/management/__init__.py similarity index 100% rename from care/abdm/migrations/__init__.py rename to care/security/management/__init__.py diff --git a/care/security/management/commands/__init__.py b/care/security/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/security/management/commands/sync_permissions_roles.py b/care/security/management/commands/sync_permissions_roles.py new file mode 100644 index 0000000000..5d5808e7e7 --- /dev/null +++ b/care/security/management/commands/sync_permissions_roles.py @@ -0,0 +1,65 @@ +from django.core.management import BaseCommand +from django.db import transaction + +from care.security.models import PermissionModel, RoleModel, RolePermission +from care.security.permissions.base import PermissionController +from care.security.roles.role import RoleController +from care.utils.lock import Lock + + +class Command(BaseCommand): + """ + This command syncs roles, permissions and role-permission mapping to the database. + This command should be run after all deployments and plug changes. + This command is idempotent, multiple instances running the same command is automatically blocked with redis. + """ + + help = "Syncs permissions and roles to database" + + def handle(self, *args, **options): + permissions = PermissionController.get_permissions() + roles = RoleController.get_roles() + with transaction.atomic(), Lock("sync_permissions_roles", 900): + # Create, update permissions and delete old permissions + PermissionModel.objects.all().update(temp_deleted=True) + for permission, metadata in permissions.items(): + permission_obj = PermissionModel.objects.filter(slug=permission).first() + if not permission_obj: + permission_obj = PermissionModel(slug=permission) + permission_obj.name = metadata.name + permission_obj.description = metadata.description + permission_obj.context = metadata.context.value + permission_obj.temp_deleted = False + permission_obj.save() + PermissionModel.objects.filter(temp_deleted=True).delete() + # Create, update roles and delete old roles + RoleModel.objects.all().update(temp_deleted=True) + for role in roles: + role_obj = RoleModel.objects.filter( + name=role.name, context=role.context.value + ).first() + if not role_obj: + role_obj = RoleModel(name=role.name, context=role.context.value) + role_obj.description = role.description + role_obj.is_system = True + role_obj.temp_deleted = False + role_obj.save() + RoleModel.objects.filter(temp_deleted=True).delete() + # Sync permissions to role + RolePermission.objects.all().update(temp_deleted=True) + role_cache = {} + for permission, metadata in permissions.items(): + permission_obj = PermissionModel.objects.filter(slug=permission).first() + for role in metadata.roles: + if role.name not in role_cache: + role_cache[role.name] = RoleModel.objects.get(name=role.name) + obj = RolePermission.objects.filter( + role=role_cache[role.name], permission=permission_obj + ).first() + if not obj: + obj = RolePermission( + role=role_cache[role.name], permission=permission_obj + ) + obj.temp_deleted = False + obj.save() + RolePermission.objects.filter(temp_deleted=True).delete() diff --git a/care/security/migrations/0001_initial.py b/care/security/migrations/0001_initial.py new file mode 100644 index 0000000000..37363e6084 --- /dev/null +++ b/care/security/migrations/0001_initial.py @@ -0,0 +1,88 @@ +# Generated by Django 5.1.2 on 2024-10-30 10:00 + +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='PermissionModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), + ('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), + ('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), + ('deleted', models.BooleanField(db_index=True, default=False)), + ('slug', models.CharField(db_index=True, max_length=1024, unique=True)), + ('name', models.CharField(max_length=1024)), + ('description', models.TextField(default='')), + ('context', models.CharField(max_length=1024)), + ('temp_deleted', models.BooleanField(default=False)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='RoleModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), + ('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), + ('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), + ('deleted', models.BooleanField(db_index=True, default=False)), + ('name', models.CharField(max_length=1024)), + ('description', models.TextField(default='')), + ('context', models.CharField(max_length=1024)), + ('is_system', models.BooleanField(default=False)), + ('temp_deleted', models.BooleanField(default=False)), + ], + options={ + 'constraints': [models.UniqueConstraint(fields=('name', 'context'), name='unique_order')], + }, + ), + migrations.CreateModel( + name='RoleAssociation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), + ('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), + ('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), + ('deleted', models.BooleanField(db_index=True, default=False)), + ('context', models.CharField(max_length=1024)), + ('context_id', models.BigIntegerField()), + ('expiry', models.DateTimeField(blank=True, null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='security.rolemodel')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='RolePermission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), + ('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), + ('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), + ('deleted', models.BooleanField(db_index=True, default=False)), + ('temp_deleted', models.BooleanField(default=False)), + ('permission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='security.permissionmodel')), + ('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='security.rolemodel')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/care/security/migrations/__init__.py b/care/security/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/security/models/__init__.py b/care/security/models/__init__.py new file mode 100644 index 0000000000..edaf462c2c --- /dev/null +++ b/care/security/models/__init__.py @@ -0,0 +1,3 @@ +from .permission import * # noqa F403 +from .permission_association import * # noqa F403 +from .role import * # noqa F403 diff --git a/care/security/models/permission.py b/care/security/models/permission.py new file mode 100644 index 0000000000..0faed59c6d --- /dev/null +++ b/care/security/models/permission.py @@ -0,0 +1,18 @@ +from django.db import models + +from care.utils.models.base import BaseModel + + +class PermissionModel(BaseModel): + """ + This model represents a permission in the security system. + A permission allows a certain action to be performed by the user for a given context. + """ + + slug = models.CharField(max_length=1024, unique=True, db_index=True) + name = models.CharField(max_length=1024) + description = models.TextField(default="") + context = models.CharField( + max_length=1024 + ) # We can add choices here as well if needed + temp_deleted = models.BooleanField(default=False) diff --git a/care/security/models/permission_association.py b/care/security/models/permission_association.py new file mode 100644 index 0000000000..b8e85fd3b8 --- /dev/null +++ b/care/security/models/permission_association.py @@ -0,0 +1,22 @@ +from django.db import models + +from care.security.models.role import RoleModel +from care.users.models import User +from care.utils.models.base import BaseModel + + +class RoleAssociation(BaseModel): + """ + This model connects roles to users via contexts + Expiry can be used to expire the role allocation after a certain period + """ + + user = models.ForeignKey(User, on_delete=models.CASCADE, null=False, blank=False) + context = models.CharField(max_length=1024) + context_id = models.BigIntegerField() # Store integer id of the context here + role = models.ForeignKey( + RoleModel, on_delete=models.CASCADE, null=False, blank=False + ) + expiry = models.DateTimeField(null=True, blank=True) + + # TODO : Index user, context and context_id diff --git a/care/security/models/role.py b/care/security/models/role.py new file mode 100644 index 0000000000..471aa44d35 --- /dev/null +++ b/care/security/models/role.py @@ -0,0 +1,45 @@ +from django.db import models +from django.db.models import UniqueConstraint + +from care.security.models.permission import PermissionModel +from care.utils.models.base import BaseModel + + +class RoleModel(BaseModel): + """ + This model represents a role in the security system. + A role comprises multiple permissions on the same type. + A role can only be made for a single context. eg, A role can be FacilityAdmin with Facility related permission items + Another role is to be created for other contexts, eg. Asset Admin should only contain Asset related permission items + Roles can be created on the fly, System roles cannot be deleted, but user created roles can be deleted by users + with the permission to delete roles + """ + + name = models.CharField(max_length=1024) + description = models.TextField(default="") + context = models.CharField( + max_length=1024 + ) # We can add choices here as well if needed + is_system = models.BooleanField( + default=False + ) # Denotes if role was created on the fly + temp_deleted = models.BooleanField(default=False) + + class Meta: + constraints = [ + UniqueConstraint(name="unique_order", fields=["name", "context"]) + ] + + +class RolePermission(BaseModel): + """ + Connects a role to a list of permissions + """ + + role = models.ForeignKey( + RoleModel, on_delete=models.CASCADE, null=False, blank=False + ) + permission = models.ForeignKey( + PermissionModel, on_delete=models.CASCADE, null=False, blank=False + ) + temp_deleted = models.BooleanField(default=False) diff --git a/care/security/permissions/__init__.py b/care/security/permissions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/security/permissions/base.py b/care/security/permissions/base.py new file mode 100644 index 0000000000..00028e6b39 --- /dev/null +++ b/care/security/permissions/base.py @@ -0,0 +1,83 @@ +import enum +from dataclasses import dataclass + +from care.security.models import RoleAssociation, RolePermission + + +class PermissionContext(enum.Enum): + GENERIC = "GENERIC" + FACILITY = "FACILITY" + ASSET = "ASSET" + LOCATION = "LOCATION" + + +@dataclass +class Permission: + """ + This class abstracts a permission + """ + + name: str + description: str + context: PermissionContext + roles: list + + +class PermissionHandler: + pass + + +from care.security.permissions.facility import FacilityPermissions # noqa: E402 + + +class PermissionController: + """ + This class defines all permissions used within care. + This class is used to abstract all operations related to permissions + """ + + override_permission_handlers = [] + # Override Permission Controllers will be defined from plugs + internal_permission_handlers = [FacilityPermissions] + + cache = {} + + @classmethod + def build_cache(cls): + """ + Iterate through the entire permission library and create a list of permissions and associated Metadata + """ + for handler in ( + cls.internal_permission_handlers + cls.override_permission_handlers + ): + for permission in handler: + cls.cache[permission.name] = permission.value + + @classmethod + def has_permission(cls, user, permission, context, context_id): + # TODO : Cache permissions and invalidate when they change + # TODO : Fetch the user role from the previous role management implementation as well. + # Need to maintain some sort of mapping from previous generation to new generation of roles + from care.security.roles.role import RoleController + + mapped_role = RoleController.map_old_role_to_new(user.role) + permission_roles = RolePermission.objects.filter( + permission__slug=permission, permission__context=context + ).values("role_id") + if RoleAssociation.objects.filter( + context_id=context_id, context=context, role__in=permission_roles, user=user + ).exists(): + return True + # Check for old cases + return RolePermission.objects.filter( + permission__slug=permission, + permission__context=context, + role__name=mapped_role.name, + role__context=mapped_role.context.value, + ).exists() + + @classmethod + def get_permissions(cls): + if not cls.cache: + cls.build_cache() + return cls.cache diff --git a/care/security/permissions/facility.py b/care/security/permissions/facility.py new file mode 100644 index 0000000000..ee1cafbd0e --- /dev/null +++ b/care/security/permissions/facility.py @@ -0,0 +1,19 @@ +import enum + +from care.security.permissions.base import Permission, PermissionContext +from care.security.roles.role import DOCTOR_ROLE, STAFF_ROLE + + +class FacilityPermissions(enum.Enum): + can_read_facility = Permission( + "Can Read on Facility", + "Something Here", + PermissionContext.FACILITY, + [STAFF_ROLE, DOCTOR_ROLE], + ) + can_update_facility = Permission( + "Can Update on Facility", + "Something Here", + PermissionContext.FACILITY, + [STAFF_ROLE], + ) diff --git a/care/security/roles/__init__.py b/care/security/roles/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/security/roles/role.py b/care/security/roles/role.py new file mode 100644 index 0000000000..dac2d8455e --- /dev/null +++ b/care/security/roles/role.py @@ -0,0 +1,69 @@ +from dataclasses import dataclass + +from care.security.permissions.base import PermissionContext + + +@dataclass +class Role: + """ + This class can be inherited for role classes that are created by default + """ + + name: str + description: str + context: PermissionContext + + +DOCTOR_ROLE = Role( + name="Doctor", + description="Some Description Here", + context=PermissionContext.FACILITY, +) # TODO : Clean description +STAFF_ROLE = Role( + name="Staff", + description="Some Description Here", + context=PermissionContext.FACILITY, +) # TODO : Clean description +ADMIN_ROLE = Role( + name="Facility Admin", + description="Some Description Here", + context=PermissionContext.FACILITY, +) # TODO : Clean description + + +class RoleController: + override_roles = [] + # Override Permission Controllers will be defined from plugs + internal_roles = [DOCTOR_ROLE, STAFF_ROLE] + + @classmethod + def get_roles(cls): + return cls.internal_roles + cls.override_roles + + @classmethod + def map_old_role_to_new(cls, old_role): + mapping = { + "Transportation": STAFF_ROLE, + "Pharmacist": STAFF_ROLE, + "Volunteer": STAFF_ROLE, + "StaffReadOnly": STAFF_ROLE, + "Staff": STAFF_ROLE, + "NurseReadOnly": STAFF_ROLE, + "Nurse": STAFF_ROLE, + "Doctor": DOCTOR_ROLE, + "Reserved": DOCTOR_ROLE, + "WardAdmin": STAFF_ROLE, + "LocalBodyAdmin": ADMIN_ROLE, + "DistrictLabAdmin": ADMIN_ROLE, + "DistrictReadOnlyAdmin": ADMIN_ROLE, + "DistrictAdmin": ADMIN_ROLE, + "StateLabAdmin": ADMIN_ROLE, + "StateReadOnlyAdmin": ADMIN_ROLE, + "StateAdmin": ADMIN_ROLE, + } + return mapping[old_role] + + @classmethod + def register_role(cls, role: Role): + # TODO : Do some deduplication Logic + cls.override_roles.append(role) diff --git a/care/users/admin.py b/care/users/admin.py index ba40369025..3f0a7f9f5c 100644 --- a/care/users/admin.py +++ b/care/users/admin.py @@ -58,6 +58,13 @@ class UserAdmin(auth_admin.UserAdmin, ExportCsvMixin): list_display = ["username", "is_superuser"] search_fields = ["first_name", "last_name"] + def get_queryset(self, request): + # use the base manager to avoid filtering out soft deleted objects + qs = self.model._base_manager.get_queryset() # noqa: SLF001 + if ordering := self.get_ordering(request): + qs = qs.order_by(*ordering) + return qs + @admin.register(State) class StateAdmin(admin.ModelAdmin): diff --git a/care/users/api/serializers/plug_config.py b/care/users/api/serializers/plug_config.py new file mode 100644 index 0000000000..6ac59adb21 --- /dev/null +++ b/care/users/api/serializers/plug_config.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from care.users.models import PlugConfig + + +class PLugConfigSerializer(serializers.ModelSerializer): + class Meta: + model = PlugConfig + exclude = ("id",) diff --git a/care/users/api/viewsets/plug_config.py b/care/users/api/viewsets/plug_config.py new file mode 100644 index 0000000000..faeb475dcc --- /dev/null +++ b/care/users/api/viewsets/plug_config.py @@ -0,0 +1,43 @@ +from django.core.cache import cache +from rest_framework.permissions import IsAdminUser +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet, ModelViewSet + +from care.users.api.serializers.plug_config import PLugConfigSerializer +from care.users.models import PlugConfig + + +class PlugConfigViewset( + ModelViewSet, + GenericViewSet, +): + lookup_field = "slug" + serializer_class = PLugConfigSerializer + queryset = PlugConfig.objects.all().order_by("slug") + cache_key = "care_plug_viewset_list" + + def list(self, request, *args, **kwargs): + # Cache data and return + response = cache.get(self.cache_key) + if not response: + serializer = self.get_serializer(self.queryset, many=True) + response = serializer.data + cache.set(self.cache_key, response) + return Response({"configs": response}) + + def perform_create(self, serializer): + cache.delete(self.cache_key) + serializer.save() + + def perform_update(self, serializer): + cache.delete(self.cache_key) + serializer.save() + + def perform_destroy(self, instance): + cache.delete(self.cache_key) + instance.delete() + + def get_permissions(self): + if self.action in ["list", "retrieve"]: + return [] + return [IsAdminUser()] diff --git a/care/users/migrations/0020_plugconfig.py b/care/users/migrations/0020_plugconfig.py new file mode 100644 index 0000000000..8f0b3977a1 --- /dev/null +++ b/care/users/migrations/0020_plugconfig.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.1 on 2024-10-29 19:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0019_rename_doctor_qualification_user_qualification'), + ] + + operations = [ + migrations.CreateModel( + name='PlugConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('slug', models.CharField(max_length=255, unique=True)), + ('meta', models.JSONField(default=dict)), + ], + ), + ] diff --git a/care/users/models.py b/care/users/models.py index 0d66392279..bf0d47c284 100644 --- a/care/users/models.py +++ b/care/users/models.py @@ -130,7 +130,7 @@ def __str__(self): class CustomUserManager(UserManager): def get_queryset(self): qs = super().get_queryset() - return qs.filter(deleted=False, is_active=True).select_related( + return qs.filter(deleted=False).select_related( "local_body", "district", "state" ) @@ -443,6 +443,14 @@ def __str__(self): return self.facility.name +class PlugConfig(models.Model): + slug = models.CharField(max_length=255, unique=True) + meta = models.JSONField(default=dict) + + def __str__(self): + return self.slug + + class UserFlag(BaseFlag): user = models.ForeignKey(User, on_delete=models.CASCADE, null=False, blank=False) diff --git a/care/users/reset_password_views.py b/care/users/reset_password_views.py index 89f67ae087..e204ab0719 100644 --- a/care/users/reset_password_views.py +++ b/care/users/reset_password_views.py @@ -208,6 +208,20 @@ def post(self, request, *args, **kwargs): status=status.HTTP_429_TOO_MANY_REQUESTS, ) + if settings.IS_PRODUCTION and ( + not settings.EMAIL_HOST + or not settings.EMAIL_HOST_USER + or not settings.EMAIL_HOST_PASSWORD + ): + raise exceptions.ValidationError( + { + "detail": [ + _( + "There was a problem resetting your password. Please contact the administrator." + ) + ] + } + ) # before we continue, delete all existing expired tokens password_reset_token_validation_time = get_password_reset_token_expiry_time() diff --git a/care/users/tests/test_auth.py b/care/users/tests/test_auth.py index 695e105564..912e5da010 100644 --- a/care/users/tests/test_auth.py +++ b/care/users/tests/test_auth.py @@ -1,5 +1,6 @@ from datetime import timedelta +from django.core import mail from django.test import override_settings from django.utils.timezone import now from django_rest_passwordreset.models import ResetPasswordToken @@ -99,7 +100,7 @@ def test_auth_verify_with_invalid_token(self): self.assertEqual(response.data["detail"], "Token is invalid or expired") -@override_settings(DISABLE_RATELIMIT=True) +@override_settings(DISABLE_RATELIMIT=True, IS_PRODUCTION=False) class TestPasswordReset(TestUtils, APITestCase): @classmethod def setUpTestData(cls) -> None: @@ -118,13 +119,55 @@ def create_reset_password_token( token.save() return token + @override_settings( + EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", + ) def test_forgot_password_with_valid_input(self): + mail.outbox = [] + response = self.client.post( + "/api/v1/password_reset/", + {"username": self.user.username}, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual("Password Reset for Care", mail.outbox[0].subject) + self.assertEqual(mail.outbox[0].to, [self.user.email]) + self.assertTrue(ResetPasswordToken.objects.filter(user=self.user).exists()) + self.assertTrue(ResetPasswordToken.objects.filter(user=self.user).exists()) + + @override_settings(IS_PRODUCTION=True) + def test_forgot_password_without_email_configration(self): + response = self.client.post( + "/api/v1/password_reset/", + {"username": self.user.username}, + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + response.json()["detail"][0], + "There was a problem resetting your password. Please contact the administrator.", + ) + + @override_settings( + IS_PRODUCTION=True, + EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", + EMAIL_HOST="dummy.smtp.server", + EMAIL_HOST_USER="dummy-email@example.com", + EMAIL_HOST_PASSWORD="dummy-password", + ) + def test_forgot_password_with_email_configuration(self): + mail.outbox = [] + response = self.client.post( "/api/v1/password_reset/", {"username": self.user.username}, ) self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual("Password Reset for Care", mail.outbox[0].subject) + self.assertEqual(mail.outbox[0].to, [self.user.email]) self.assertTrue(ResetPasswordToken.objects.filter(user=self.user).exists()) def test_forgot_password_with_missing_fields(self): diff --git a/care/utils/queryset/asset_bed.py b/care/utils/queryset/asset_bed.py new file mode 100644 index 0000000000..f9fe8f925e --- /dev/null +++ b/care/utils/queryset/asset_bed.py @@ -0,0 +1,47 @@ +from care.facility.models import Asset, AssetBed, Bed +from care.users.models import User +from care.utils.cache.cache_allowed_facilities import get_accessible_facilities + + +def get_asset_bed_queryset(user, queryset=None): + queryset = AssetBed.objects.all() if queryset is None else queryset + if user.is_superuser: + pass + elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + queryset = queryset.filter(bed__facility__state=user.state) + elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + queryset = queryset.filter(bed__facility__district=user.district) + else: + allowed_facilities = get_accessible_facilities(user) + queryset = queryset.filter(bed__facility__id__in=allowed_facilities) + return queryset + + +def get_bed_queryset(user, queryset=None): + queryset = Bed.objects.all() if queryset is None else queryset + if user.is_superuser: + pass + elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + queryset = queryset.filter(facility__state=user.state) + elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + queryset = queryset.filter(facility__district=user.district) + else: + allowed_facilities = get_accessible_facilities(user) + queryset = queryset.filter(facility__id__in=allowed_facilities) + return queryset + + +def get_asset_queryset(user, queryset=None): + queryset = Asset.objects.all() if queryset is None else queryset + if user.is_superuser: + pass + elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + queryset = queryset.filter(current_location__facility__state=user.state) + elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + queryset = queryset.filter(current_location__facility__district=user.district) + else: + allowed_facilities = get_accessible_facilities(user) + queryset = queryset.filter( + current_location__facility__id__in=allowed_facilities + ) + return queryset diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index 1f858c7258..91d4ac8d67 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -38,7 +38,7 @@ Ward, ) from care.facility.models.asset import Asset, AssetLocation -from care.facility.models.bed import Bed, ConsultationBed +from care.facility.models.bed import AssetBed, Bed, ConsultationBed from care.facility.models.facility import FacilityUser from care.facility.models.icd11_diagnosis import ( ConditionVerificationStatus, @@ -446,6 +446,12 @@ def create_bed(cls, facility: Facility, location: AssetLocation, **kwargs): data.update(kwargs) return Bed.objects.create(**data) + @classmethod + def create_asset_bed(cls, asset: Asset, bed: Bed, **kwargs): + data = {"asset": asset, "bed": bed} + data.update(kwargs) + return AssetBed.objects.create(**data) + @classmethod def create_consultation_bed( cls, diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..e00ce3d698 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +github_checks: + annotations: false diff --git a/config/api_router.py b/config/api_router.py index 917b187395..94b18f61de 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -3,12 +3,6 @@ from rest_framework.routers import DefaultRouter, SimpleRouter from rest_framework_nested.routers import NestedSimpleRouter -from care.abdm.api.viewsets.abha_number import AbhaNumberViewSet -from care.abdm.api.viewsets.consent import ConsentViewSet -from care.abdm.api.viewsets.health_facility import HealthFacilityViewSet -from care.abdm.api.viewsets.health_information import HealthInformationViewSet -from care.abdm.api.viewsets.healthid import ABDMHealthIDViewSet -from care.abdm.api.viewsets.patients import PatientsViewSet from care.facility.api.viewsets.ambulance import AmbulanceViewSet from care.facility.api.viewsets.asset import ( AssetLocationViewSet, @@ -26,6 +20,10 @@ ConsultationBedViewSet, PatientAssetBedViewSet, ) +from care.facility.api.viewsets.camera_preset import ( + AssetBedCameraPresetViewSet, + CameraPresetViewSet, +) from care.facility.api.viewsets.consultation_diagnosis import ( ConsultationDiagnosisViewSet, ) @@ -37,6 +35,7 @@ ) from care.facility.api.viewsets.facility import ( AllFacilityViewSet, + FacilityHubsViewSet, FacilitySpokesViewSet, FacilityViewSet, ) @@ -100,6 +99,7 @@ StateViewSet, WardViewSet, ) +from care.users.api.viewsets.plug_config import PlugConfigViewset from care.users.api.viewsets.skill import SkillViewSet from care.users.api.viewsets.users import UserViewSet from care.users.api.viewsets.userskill import UserSkillViewSet @@ -107,6 +107,9 @@ router = DefaultRouter() if settings.DEBUG else SimpleRouter() router.register("users", UserViewSet, basename="users") + +router.register("plug_config", PlugConfigViewset, basename="plug_configs") + user_nested_router = NestedSimpleRouter(router, r"users", lookup="users") user_nested_router.register("skill", UserSkillViewSet, basename="users-skill") @@ -218,9 +221,13 @@ facility_nested_router.register( r"spokes", FacilitySpokesViewSet, basename="facility-spokes" ) +facility_nested_router.register(r"hubs", FacilityHubsViewSet, basename="facility-hubs") router.register("asset", AssetViewSet, basename="asset") asset_nested_router = NestedSimpleRouter(router, r"asset", lookup="asset") +asset_nested_router.register( + r"camera_presets", CameraPresetViewSet, basename="asset-camera-presets" +) asset_nested_router.register( r"availability", AvailabilityViewSet, basename="asset-availability" ) @@ -232,8 +239,17 @@ router.register("asset_transaction", AssetTransactionViewSet) router.register("bed", BedViewSet, basename="bed") +bed_nested_router = NestedSimpleRouter(router, r"bed", lookup="bed") +bed_nested_router.register( + r"camera_presets", CameraPresetViewSet, basename="bed-camera-presets" +) + router.register("assetbed", AssetBedViewSet, basename="asset-bed") router.register("consultationbed", ConsultationBedViewSet, basename="consultation-bed") +assetbed_nested_router = NestedSimpleRouter(router, r"assetbed", lookup="assetbed") +assetbed_nested_router.register( + r"camera_presets", AssetBedCameraPresetViewSet, basename="assetbed-camera-presets" +) router.register("patient/search", PatientSearchViewSet, basename="patient-search") router.register("patient", PatientViewSet, basename="patient") @@ -303,23 +319,6 @@ router.register("public/asset", AssetPublicViewSet, basename="public-asset") router.register("public/asset_qr", AssetPublicQRViewSet, basename="public-asset-qr") -# ABDM endpoints -if settings.ENABLE_ABDM: - router.register("abdm/healthid", ABDMHealthIDViewSet, basename="abdm-healthid") - router.register("abdm/consent", ConsentViewSet, basename="abdm-consent") - router.register( - "abdm/health_information", - HealthInformationViewSet, - basename="abdm-healthinformation", - ) - router.register("abdm/patients", PatientsViewSet, basename="abdm-patients") - router.register("abdm/abha_numbers", AbhaNumberViewSet, basename="abdm-abhanumber") - -router.register( - "abdm/health_facility", HealthFacilityViewSet, basename="abdm-healthfacility" -) - - app_name = "api" urlpatterns = [ path("", include(router.urls)), @@ -327,6 +326,8 @@ path("", include(facility_nested_router.urls)), path("", include(facility_location_nested_router.urls)), path("", include(asset_nested_router.urls)), + path("", include(bed_nested_router.urls)), + path("", include(assetbed_nested_router.urls)), path("", include(patient_nested_router.urls)), path("", include(patient_notes_nested_router.urls)), path("", include(consultation_nested_router.urls)), diff --git a/config/authentication.py b/config/authentication.py index 086348b1bc..619ae9ab22 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -1,9 +1,7 @@ -import json import logging import jwt import requests -from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.core.cache import cache from django.core.exceptions import ValidationError @@ -199,57 +197,6 @@ def get_user(self, validated_token, facility): return asset_user -class ABDMAuthentication(JWTAuthentication): - def open_id_authenticate(self, url, token): - public_key = requests.get(url, timeout=OPENID_REQUEST_TIMEOUT) - jwk = public_key.json()["keys"][0] - public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk)) - return jwt.decode( - token, key=public_key, audience="account", algorithms=["RS256"] - ) - - def authenticate_header(self, request): - return "Bearer" - - def authenticate(self, request): - jwt_token = self.get_header(request) - if jwt_token is None: - return None - jwt_token = self.get_jwt_token(jwt_token) - - abdm_cert_url = f"{settings.ABDM_URL}/gateway/v0.5/certs" - validated_token = self.get_validated_token(abdm_cert_url, jwt_token) - - return self.get_user(validated_token), validated_token - - def get_jwt_token(self, token): - return token.replace("Bearer", "").replace(" ", "") - - def get_validated_token(self, url, token): - try: - return self.open_id_authenticate(url, token) - except Exception as e: - logger.info(e, "Token: ", token) - raise InvalidToken({"detail": "Invalid Authorization token"}) from e - - def get_user(self, validated_token): - user = User.objects.filter(username=settings.ABDM_USERNAME).first() - if not user: - password = User.objects.make_random_password() - user = User( - username=settings.ABDM_USERNAME, - email="hcx@ohc.network", - password=f"{password}xyz", - gender=3, - phone_number="917777777777", - user_type=User.TYPE_VALUE_MAP["Volunteer"], - verified=True, - date_of_birth=timezone.now().date(), - ) - user.save() - return user - - class CustomJWTAuthenticationScheme(OpenApiAuthenticationExtension): target_class = "config.authentication.CustomJWTAuthentication" name = "jwtAuth" diff --git a/config/settings/base.py b/config/settings/base.py index 5bf8ddd3b6..6983433f86 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -124,8 +124,8 @@ "healthy_django", ] LOCAL_APPS = [ + "care.security", "care.facility", - "care.abdm", "care.users", "care.audit_log", ] @@ -530,6 +530,7 @@ "VAPID_PRIVATE_KEY", default="7mf3OFreFsgFF4jd8A71ZGdVaj8kpJdOto4cFbfAS-s" ) SEND_SMS_NOTIFICATION = False +NOTIFICATION_RETENTION_DAYS = env.int("NOTIFICATION_RETENTION_DAYS", default=30) # Cloud and Buckets # ------------------------------------------------------------------------------ @@ -633,21 +634,6 @@ APP_VERSION = env("APP_VERSION", default="unknown") -# ABDM -ENABLE_ABDM = env.bool("ENABLE_ABDM", default=False) -ABDM_CLIENT_ID = env("ABDM_CLIENT_ID", default="") -ABDM_CLIENT_SECRET = env("ABDM_CLIENT_SECRET", default="") -ABDM_URL = env("ABDM_URL", default="https://dev.abdm.gov.in") -HEALTH_SERVICE_API_URL = env( - "HEALTH_SERVICE_API_URL", default="https://healthidsbx.abdm.gov.in/api" -) -ABDM_FACILITY_URL = env("ABDM_FACILITY_URL", default="https://facilitysbx.abdm.gov.in") -HIP_NAME_PREFIX = env("HIP_NAME_PREFIX", default="") -HIP_NAME_SUFFIX = env("HIP_NAME_SUFFIX", default="") -ABDM_USERNAME = env("ABDM_USERNAME", default="abdm_user_internal") -X_CM_ID = env("X_CM_ID", default="sbx") -FIDELIUS_URL = env("FIDELIUS_URL", default="http://fidelius:8090") - IS_PRODUCTION = False PLAUSIBLE_HOST = env("PLAUSIBLE_HOST", default="") diff --git a/config/urls.py b/config/urls.py index c90bd2adc2..904af4b3f3 100644 --- a/config/urls.py +++ b/config/urls.py @@ -9,7 +9,6 @@ SpectacularSwaggerView, ) -from care.abdm.urls import abdm_urlpatterns from care.facility.api.viewsets.open_id import PublicJWKsView from care.facility.api.viewsets.patient_consultation import ( dev_preview_discharge_summary, @@ -34,7 +33,7 @@ path("ping/", ping, name="ping"), path("app_version/", app_version, name="app_version"), # Django Admin, use {% url 'admin:index' %} - path(f"{settings.ADMIN_URL.rstrip("/")}/", admin.site.urls), + path(f"{settings.ADMIN_URL.rstrip('/')}/", admin.site.urls), # Rest API path("api/v1/auth/login/", TokenObtainPairView.as_view(), name="token_obtain_pair"), path( @@ -75,9 +74,6 @@ *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT), ] -if settings.ENABLE_ABDM: - urlpatterns += abdm_urlpatterns - if settings.DEBUG: # This allows the error pages to be debugged during development, just visit # these url in browser to see how these error pages look like. diff --git a/data/dummy/facility.json b/data/dummy/facility.json index cea1986085..17d98574ff 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -12,14 +12,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], + "features": "[\"1\", \"2\", \"3\", \"4\", \"5\", \"6\"]", "longitude": null, "latitude": null, "pincode": 670000, @@ -56,10 +49,7 @@ "verified": false, "facility_type": 1300, "kasp_empanelled": false, - "features": [ - 1, - 6 - ], + "features": "[\"1\", \"6\"]", "longitude": null, "latitude": null, "pincode": 670112, @@ -96,11 +86,7 @@ "verified": false, "facility_type": 1500, "kasp_empanelled": false, - "features": [ - 1, - 4, - 6 - ], + "features": "[\"1\", \"4\", \"6\"]", "longitude": "78.6757364624373000", "latitude": "21.4009146842158660", "pincode": 670000, @@ -137,11 +123,7 @@ "verified": false, "facility_type": 1510, "kasp_empanelled": false, - "features": [ - 1, - 3, - 5 - ], + "features": "[\"1\", \"3\", \"5\"]", "longitude": "75.2139014820876600", "latitude": "18.2774285038890340", "pincode": 670000, @@ -178,7 +160,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -215,7 +197,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -252,7 +234,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -289,7 +271,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -326,7 +308,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -363,7 +345,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -400,7 +382,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -437,7 +419,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -474,7 +456,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -511,7 +493,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -548,7 +530,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -585,7 +567,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -622,7 +604,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -659,7 +641,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -696,7 +678,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -733,7 +715,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -1055,6 +1037,15 @@ "created_by": 2 } }, + { + "model": "facility.facilityuser", + "pk": 25, + "fields": { + "facility": 1, + "user": 25, + "created_by": 2 + } + }, { "model": "facility.assetlocation", "pk": 1, @@ -1984,6 +1975,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-01T08:35:00Z", @@ -2011,7 +2003,8 @@ "weight": 0.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2043,6 +2036,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:33:03.700Z", @@ -2070,7 +2064,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2102,6 +2097,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:39:29.394Z", @@ -2129,7 +2125,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2161,6 +2158,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:42:10.532Z", @@ -2188,7 +2186,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2220,6 +2219,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:42:33.614Z", @@ -2247,7 +2247,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2279,6 +2280,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:42:56.180Z", @@ -2306,7 +2308,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2338,6 +2341,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:43:18.480Z", @@ -2365,7 +2369,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2397,6 +2402,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:43:41.540Z", @@ -2424,7 +2430,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2456,6 +2463,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:44:05.398Z", @@ -2483,7 +2491,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2515,6 +2524,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:44:28.550Z", @@ -2542,7 +2552,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2574,6 +2585,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:44:51.239Z", @@ -2601,7 +2613,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2633,6 +2646,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:45:13.721Z", @@ -2660,7 +2674,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2692,6 +2707,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:45:37.972Z", @@ -2719,7 +2735,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2751,6 +2768,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:46:00.645Z", @@ -2778,7 +2796,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2810,6 +2829,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:46:23.492Z", @@ -2837,7 +2857,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2869,6 +2890,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:46:46.028Z", @@ -2896,7 +2918,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2928,6 +2951,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:47:11.141Z", @@ -2955,7 +2979,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2987,6 +3012,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:47:34.395Z", @@ -3014,7 +3040,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3046,6 +3073,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-07T08:47:53.746Z", @@ -3073,7 +3101,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3105,6 +3134,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-15T08:47:53.746Z", @@ -3132,7 +3162,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3140,8 +3171,8 @@ "pk": 21, "fields": { "external_id": "40fa5cc6-6199-48cd-bc2a-dd9e73b920f9", - "created_date": "2024-1-30T08:47:53.746Z", - "modified_date": "2024-1-30T08:47:53.746Z", + "created_date": "2024-01-30T08:47:53.746Z", + "modified_date": "2024-01-30T08:47:53.746Z", "deleted": false, "patient": 18, "patient_no": "IP0010", @@ -3164,9 +3195,10 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, - "encounter_date": "2024-1-30T08:47:53.746Z", + "encounter_date": "2024-01-30T08:47:53.746Z", "icu_admission_date": null, "discharge_date": null, "discharge_reason": null, @@ -3191,7 +3223,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3199,8 +3232,8 @@ "pk": 22, "fields": { "external_id": "40faecc6-6199-48cd-bc2a-6d9e73b920f9", - "created_date": "2024-2-28T08:47:53.746Z", - "modified_date": "2024-2-28T08:47:53.746Z", + "created_date": "2024-02-28T08:47:53.746Z", + "modified_date": "2024-02-28T08:47:53.746Z", "deleted": false, "patient": 18, "patient_no": "IP011", @@ -3223,9 +3256,10 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, - "encounter_date": "2024-2-28T08:47:53.746Z", + "encounter_date": "2024-02-28T08:47:53.746Z", "icu_admission_date": null, "discharge_date": null, "discharge_reason": null, @@ -3250,7 +3284,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3282,6 +3317,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2024-04-01T08:47:53.746Z", @@ -3309,7 +3345,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3341,6 +3378,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2022-05-06T08:47:53.746Z", @@ -3368,7 +3406,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3400,6 +3439,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2022-02-06T08:47:53.746Z", @@ -3427,7 +3467,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3459,6 +3500,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:47:34.395Z", @@ -3486,7 +3528,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3585,6 +3628,54 @@ "location": 1 } }, + { + "model": "facility.bed", + "pk": 9, + "fields": { + "external_id": "a9ea05d2-4f58-4fc2-bc24-2979a3502dc4", + "created_date": "2024-10-14T07:56:52.641Z", + "modified_date": "2024-10-14T07:56:52.641Z", + "deleted": false, + "name": "Dummy Bed 7", + "description": "", + "bed_type": 2, + "facility": 1, + "meta": {}, + "location": 2 + } + }, + { + "model": "facility.bed", + "pk": 10, + "fields": { + "external_id": "aff5a088-c278-4075-9a4c-64453f50f216", + "created_date": "2024-10-14T07:56:59.896Z", + "modified_date": "2024-10-14T07:56:59.896Z", + "deleted": false, + "name": "Dummy Bed 8", + "description": "", + "bed_type": 6, + "facility": 1, + "meta": {}, + "location": 2 + } + }, + { + "model": "facility.bed", + "pk": 11, + "fields": { + "external_id": "882ce4de-4e2c-4afa-b3e6-29fceec30730", + "created_date": "2024-10-14T07:57:06.701Z", + "modified_date": "2024-10-14T07:57:06.701Z", + "deleted": false, + "name": "Dummy Bed 9", + "description": "", + "bed_type": 2, + "facility": 1, + "meta": {}, + "location": 2 + } + }, { "model": "facility.consultationdiagnosis", "pk": 1, @@ -3985,14 +4076,8 @@ "default_unit": 1, "description": "", "min_quantity": 150.0, - "allowed_units": [ - 1, - 2 - ], - "tags": [ - 1, - 2 - ] + "allowed_units": [1, 2], + "tags": [1, 2] } }, { @@ -4003,13 +4088,8 @@ "default_unit": 1, "description": "", "min_quantity": 2.0, - "allowed_units": [ - 1, - 2 - ], - "tags": [ - 2 - ] + "allowed_units": [1, 2], + "tags": [2] } }, { @@ -4020,12 +4100,8 @@ "default_unit": 7, "description": "", "min_quantity": 10.0, - "allowed_units": [ - 7 - ], - "tags": [ - 2 - ] + "allowed_units": [7], + "tags": [2] } }, { @@ -4036,9 +4112,7 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [ - 4 - ], + "allowed_units": [4], "tags": [] } }, @@ -4050,9 +4124,7 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [ - 4 - ], + "allowed_units": [4], "tags": [] } }, @@ -4064,9 +4136,7 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [ - 4 - ], + "allowed_units": [4], "tags": [] } }, @@ -4078,12 +4148,8 @@ "default_unit": 7, "description": "", "min_quantity": 10.0, - "allowed_units": [ - 7 - ], - "tags": [ - 2 - ] + "allowed_units": [7], + "tags": [2] } }, { @@ -4107,8 +4173,10 @@ "pincode": 600115, "date_of_birth": "2005-10-16", "year_of_birth": 2005, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4123,6 +4191,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 5896, "local_body": 95, @@ -4186,8 +4256,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4202,6 +4274,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4265,8 +4339,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4281,6 +4357,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4344,8 +4422,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4360,6 +4440,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4423,8 +4505,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4439,6 +4523,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4502,8 +4588,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4518,6 +4606,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4581,8 +4671,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4597,6 +4689,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4660,8 +4754,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4676,6 +4772,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4739,8 +4837,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4755,6 +4855,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4818,8 +4920,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4834,6 +4938,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4897,8 +5003,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4913,6 +5021,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4976,8 +5086,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4992,6 +5104,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5055,8 +5169,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5071,6 +5187,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5134,8 +5252,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5150,6 +5270,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5213,8 +5335,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5229,6 +5353,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5292,8 +5418,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5308,6 +5436,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5371,8 +5501,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5387,6 +5519,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5450,8 +5584,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5466,6 +5602,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -7122,6 +7260,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7151,6 +7291,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7180,6 +7322,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7209,6 +7353,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7238,6 +7384,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7267,6 +7415,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7296,6 +7446,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7325,6 +7477,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7354,6 +7508,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7383,6 +7539,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7412,6 +7570,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7441,6 +7601,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7470,6 +7632,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7499,6 +7663,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7528,6 +7694,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7557,6 +7725,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7586,6 +7756,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7877,8 +8049,8 @@ "pk": 9, "fields": { "external_id": "09876543-210e-4567-a890-1234567890ab", - "created_date": "2023-12-06T09:00:00.000Z", - "modified_date": "2023-12-06T09:00:00.000Z", + "created_date": "2023-12-06T09:00:00Z", + "modified_date": "2023-12-06T09:00:00Z", "deleted": false, "origin_facility": 1, "shifting_approving_facility": 2, @@ -8008,40 +8180,6 @@ "last_edited_by": 1 } }, - { - "model": "facility.shiftingrequest", - "pk": 15, - "fields": { - "external_id": "98765432-10e2-40f1-a0b9-876543210abc", - "created_date": "2023-09-05T22:50:10.221Z", - "modified_date": "2023-12-04T14:30:45.501Z", - "deleted": false, - "origin_facility": 1, - "shifting_approving_facility": 2, - "assigned_facility_type": 2, - "assigned_facility": null, - "assigned_facility_external": null, - "patient": 15, - "emergency": true, - "is_up_shift": true, - "reason": "Test", - "vehicle_preference": "", - "preferred_vehicle_choice": 10, - "comments": "", - "refering_facility_contact_name": "Someone at Facility", - "refering_facility_contact_number": "+914455666777", - "is_kasp": false, - "status": 100, - "breathlessness_level": 30, - "is_assigned_to_user": false, - "assigned_to": null, - "ambulance_driver_name": "", - "ambulance_phone_number": "", - "ambulance_number": "", - "created_by": 2, - "last_edited_by": 2 - } - }, { "model": "facility.shiftingrequest", "pk": 14, @@ -8114,6 +8252,10 @@ "model": "facility.resourcerequest", "pk": 1, "fields": { + "external_id": "067ad8fc-2551-4267-8a09-6facaebf0e1f", + "created_date": "2023-09-05T22:50:10.221Z", + "modified_date": "2023-09-05T22:50:10.221Z", + "deleted": false, "origin_facility": 1, "approving_facility": 2, "assigned_facility": 3, @@ -8131,15 +8273,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 1, - "last_edited_by": 1, - "created_date": "2023-09-05T22:50:10.221Z", - "modified_date": "2023-09-05T22:50:10.221Z" + "last_edited_by": 1 } }, { "model": "facility.resourcerequest", "pk": 2, "fields": { + "external_id": "0c77c84a-3b91-419c-807c-65e53f11a74f", + "created_date": "2023-09-06T22:50:10.221Z", + "modified_date": "2023-09-06T22:50:10.221Z", + "deleted": false, "origin_facility": 1, "approving_facility": 5, "assigned_facility": 8, @@ -8157,15 +8301,17 @@ "is_assigned_to_user": true, "assigned_to": 3, "created_by": 2, - "last_edited_by": 2, - "created_date": "2023-09-06T22:50:10.221Z", - "modified_date": "2023-09-06T22:50:10.221Z" + "last_edited_by": 2 } }, { "model": "facility.resourcerequest", "pk": 3, "fields": { + "external_id": "b08312cc-c301-46c0-8347-c521cbae8c54", + "created_date": "2023-09-07T22:50:10.221Z", + "modified_date": "2023-09-07T22:50:10.221Z", + "deleted": false, "origin_facility": 7, "approving_facility": 2, "assigned_facility": 3, @@ -8183,15 +8329,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 3, - "last_edited_by": 3, - "created_date": "2023-09-07T22:50:10.221Z", - "modified_date": "2023-09-07T22:50:10.221Z" + "last_edited_by": 3 } }, { "model": "facility.resourcerequest", "pk": 4, "fields": { + "external_id": "e9664dcf-224f-4e1e-a003-c4ff0d3699d3", + "created_date": "2023-09-08T22:50:10.221Z", + "modified_date": "2023-09-08T22:50:10.221Z", + "deleted": false, "origin_facility": 9, "approving_facility": 1, "assigned_facility": 4, @@ -8209,15 +8357,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 4, - "last_edited_by": 4, - "created_date": "2023-09-08T22:50:10.221Z", - "modified_date": "2023-09-08T22:50:10.221Z" + "last_edited_by": 4 } }, { "model": "facility.resourcerequest", "pk": 5, "fields": { + "external_id": "ce300e4d-fae6-4ee4-8bf9-f70b98abd7be", + "created_date": "2023-09-09T22:50:10.221Z", + "modified_date": "2023-09-09T22:50:10.221Z", + "deleted": false, "origin_facility": 5, "approving_facility": 6, "assigned_facility": 7, @@ -8235,15 +8385,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 5, - "last_edited_by": 5, - "created_date": "2023-09-09T22:50:10.221Z", - "modified_date": "2023-09-09T22:50:10.221Z" + "last_edited_by": 5 } }, { "model": "facility.resourcerequest", "pk": 6, "fields": { + "external_id": "79b90b87-1cac-449b-95d2-656f0c543652", + "created_date": "2023-09-10T22:50:10.221Z", + "modified_date": "2023-09-10T22:50:10.221Z", + "deleted": false, "origin_facility": 12, "approving_facility": 9, "assigned_facility": 1, @@ -8261,15 +8413,17 @@ "is_assigned_to_user": true, "assigned_to": 6, "created_by": 6, - "last_edited_by": 6, - "created_date": "2023-09-10T22:50:10.221Z", - "modified_date": "2023-09-10T22:50:10.221Z" + "last_edited_by": 6 } }, { "model": "facility.resourcerequest", "pk": 7, "fields": { + "external_id": "fb5fcb52-8640-4a34-95f3-8adaa7c34f6d", + "created_date": "2023-09-11T22:50:10.221Z", + "modified_date": "2023-09-11T22:50:10.221Z", + "deleted": false, "origin_facility": 10, "approving_facility": 11, "assigned_facility": 9, @@ -8287,15 +8441,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 7, - "last_edited_by": 7, - "created_date": "2023-09-11T22:50:10.221Z", - "modified_date": "2023-09-11T22:50:10.221Z" + "last_edited_by": 7 } }, { "model": "facility.resourcerequest", "pk": 8, "fields": { + "external_id": "94ac6a65-cf45-4a01-aa5a-11238ae7dca5", + "created_date": "2023-09-12T22:50:10.221Z", + "modified_date": "2023-09-12T22:50:10.221Z", + "deleted": false, "origin_facility": 15, "approving_facility": 7, "assigned_facility": 6, @@ -8313,15 +8469,17 @@ "is_assigned_to_user": true, "assigned_to": 8, "created_by": 8, - "last_edited_by": 8, - "created_date": "2023-09-12T22:50:10.221Z", - "modified_date": "2023-09-12T22:50:10.221Z" + "last_edited_by": 8 } }, { "model": "facility.resourcerequest", "pk": 9, "fields": { + "external_id": "b8137245-82de-48d8-add1-132d8cf4458a", + "created_date": "2023-09-13T22:50:10.221Z", + "modified_date": "2023-09-13T22:50:10.221Z", + "deleted": false, "origin_facility": 3, "approving_facility": 9, "assigned_facility": 1, @@ -8339,15 +8497,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 9, - "last_edited_by": 9, - "created_date": "2023-09-13T22:50:10.221Z", - "modified_date": "2023-09-13T22:50:10.221Z" + "last_edited_by": 9 } }, { "model": "facility.resourcerequest", "pk": 10, "fields": { + "external_id": "996d4aa0-3694-4b05-bb75-53a194d65158", + "created_date": "2023-09-14T22:50:10.221Z", + "modified_date": "2023-09-14T22:50:10.221Z", + "deleted": false, "origin_facility": 10, "approving_facility": 1, "assigned_facility": 5, @@ -8365,9 +8525,7 @@ "is_assigned_to_user": true, "assigned_to": 10, "created_by": 10, - "last_edited_by": 10, - "created_date": "2023-09-14T22:50:10.221Z", - "modified_date": "2023-09-14T22:50:10.221Z" + "last_edited_by": 10 } } ] diff --git a/data/dummy/users.json b/data/dummy/users.json index fc55f68670..e7b0115614 100644 --- a/data/dummy/users.json +++ b/data/dummy/users.json @@ -886,5 +886,47 @@ "groups": [], "user_permissions": [] } + }, + { + "model": "users.user", + "pk": 25, + "fields": { + "password": "argon2$argon2id$v=19$m=102400,t=2,p=8$bUNTR1MwejJYNXdXd2VUYjJHMmN5bw$alS6S9Ay3bvIHe9U18luyn7LyVaArgrgHIt+vh4ta48", + "last_login": null, + "is_superuser": false, + "first_name": "Dev", + "last_name": "Doctor Two", + "email": "devdoctor1@test.com", + "is_staff": false, + "is_active": true, + "date_joined": "2024-10-14T07:53:32.400Z", + "external_id": "009c4fc2-f7af-4a02-9383-6fbb4af2fdbb", + "username": "devdoctor1", + "user_type": 15, + "created_by": 2, + "ward": null, + "local_body": null, + "district": 7, + "state": 1, + "phone_number": "+917644536346", + "alt_phone_number": "+917644536346", + "video_connect_link": null, + "gender": 1, + "date_of_birth": "2005-01-01", + "profile_picture_url": null, + "home_facility": null, + "weekly_working_hours": null, + "qualification": "MBBS", + "doctor_experience_commenced_on": "2020-10-14", + "doctor_medical_council_registration": "23532093", + "verified": true, + "deleted": false, + "pf_endpoint": null, + "pf_p256dh": null, + "pf_auth": null, + "asset": null, + "groups": [], + "user_permissions": [] + } } ] diff --git a/docker-compose.yaml b/docker-compose.yaml index 166297a383..b361cbdd8b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -37,12 +37,6 @@ services: ports: - "4566:4566" - fidelius: - image: khavinshankar/fidelius:latest - restart: unless-stopped - ports: - - "8092:8090" - volumes: postgres-data: redis-data: diff --git a/docker/dev.Dockerfile b/docker/dev.Dockerfile index f83f059d2a..198f40d824 100644 --- a/docker/dev.Dockerfile +++ b/docker/dev.Dockerfile @@ -1,11 +1,11 @@ -FROM python:3.12-slim-bookworm +FROM python:3.13-slim-bookworm ARG TYPST_VERSION=0.11.0 ENV PATH=/venv/bin:$PATH RUN apt-get update && apt-get install --no-install-recommends -y \ - build-essential libjpeg-dev zlib1g-dev \ + build-essential libjpeg-dev zlib1g-dev libgmp-dev \ libpq-dev gettext wget curl gnupg git \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* @@ -28,7 +28,7 @@ RUN ARCH=$(dpkg --print-architecture) && \ # use pipenv to manage virtualenv RUN python -m venv /venv -RUN pip install pipenv +RUN pip install pipenv==2024.2.0 COPY Pipfile Pipfile.lock ./ RUN pipenv install --system --categories "packages dev-packages" diff --git a/docker/prod.Dockerfile b/docker/prod.Dockerfile index 877478c3a9..2aef7be6d8 100644 --- a/docker/prod.Dockerfile +++ b/docker/prod.Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12-slim-bookworm AS base +FROM python:3.13-slim-bookworm AS base ARG APP_HOME=/app ARG TYPST_VERSION=0.11.0 @@ -20,7 +20,7 @@ WORKDIR $APP_HOME FROM base AS builder RUN apt-get update && apt-get install --no-install-recommends -y \ - build-essential libjpeg-dev zlib1g-dev libpq-dev git wget \ + build-essential libjpeg-dev zlib1g-dev libgmp-dev libpq-dev git wget \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* @@ -41,7 +41,7 @@ RUN ARCH=$(dpkg --print-architecture) && \ # use pipenv to manage virtualenv RUN python -m venv /venv -RUN pip install pipenv +RUN pip install pipenv==2024.2.0 COPY Pipfile Pipfile.lock $APP_HOME RUN pipenv sync --system --categories "packages" @@ -54,7 +54,7 @@ RUN python3 $APP_HOME/install_plugins.py FROM base AS runtime RUN apt-get update && apt-get install --no-install-recommends -y \ - libpq-dev gettext wget curl gnupg \ + libpq-dev libgmp-dev gettext wget curl gnupg \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* diff --git a/install_plugins.py b/install_plugins.py index b320f0caaf..8324ff795b 100644 --- a/install_plugins.py +++ b/install_plugins.py @@ -1,18 +1,3 @@ -import json -import logging -import os - from plug_config import manager -from plugs.plug import Plug - -logger = logging.getLogger(__name__) - - -if ADDITIONAL_PLUGS := os.getenv("ADDITIONAL_PLUGS"): - try: - for plug in json.loads(ADDITIONAL_PLUGS): - manager.add_plug(Plug(**plug)) - except json.JSONDecodeError: - logger.error("ADDITIONAL_PLUGS is not a valid JSON") manager.install() diff --git a/plug_config.py b/plug_config.py index c177c62cde..27be9de162 100644 --- a/plug_config.py +++ b/plug_config.py @@ -1,6 +1,13 @@ from plugs.manager import PlugManager from plugs.plug import Plug +abdm_plugin = Plug( + name="abdm", + package_name="git+https://github.com/ohcnetwork/care_abdm.git", + version="@main", + configs={}, +) + hcx_plugin = Plug( name="hcx", package_name="git+https://github.com/ohcnetwork/care_hcx.git", @@ -8,6 +15,6 @@ configs={}, ) -plugs = [hcx_plugin] +plugs = [hcx_plugin, abdm_plugin] manager = PlugManager(plugs) diff --git a/plugs/manager.py b/plugs/manager.py index dfdac9fa93..2e4516ebb6 100644 --- a/plugs/manager.py +++ b/plugs/manager.py @@ -1,9 +1,14 @@ +import json +import logging +import os import subprocess import sys from collections import defaultdict from plugs.plug import Plug +logger = logging.getLogger(__name__) + class PlugManager: """ @@ -13,6 +18,14 @@ class PlugManager: def __init__(self, plugs: list[Plug]): self.plugs: list[Plug] = plugs + # load additional plugs from environment variable + if additional_plugs := os.getenv("ADDITIONAL_PLUGS"): + try: + for plug in json.loads(additional_plugs): + self.add_plug(Plug(**plug)) + except json.JSONDecodeError: + logger.error("ADDITIONAL_PLUGS is not a valid JSON") + def install(self) -> None: packages: list[str] = [f"{x.package_name}{x.version}" for x in self.plugs] if packages: