From da5de1a4019eaae0ce23789fa520b06802f99f7a Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 6 Aug 2024 13:53:09 +0100 Subject: [PATCH 01/66] build: strip out unecessary backend requirements --- src/backend/pdm.lock | 1862 +++++++++++----------------------- src/backend/pyproject.toml | 15 +- src/backend/requirements.txt | 11 - 3 files changed, 570 insertions(+), 1318 deletions(-) delete mode 100644 src/backend/requirements.txt diff --git a/src/backend/pdm.lock b/src/backend/pdm.lock index 3afe890b..ec66c351 100644 --- a/src/backend/pdm.lock +++ b/src/backend/pdm.lock @@ -1,11 +1,21 @@ # This file is @generated by PDM. # It is not intended for manual editing. +[metadata] +groups = ["default"] +strategy = ["cross_platform"] +lock_version = "4.4.1" +content_hash = "sha256:ac15441fa1ce71b998872b835deef9897bb7dfaa906832d70c7aa32180555ccc" + [[package]] name = "aiosmtplib" version = "3.0.2" requires_python = ">=3.8" summary = "asyncio SMTP client" +files = [ + {file = "aiosmtplib-3.0.2-py3-none-any.whl", hash = "sha256:8783059603a34834c7c90ca51103c3aa129d5922003b5ce98dbaa6d4440f10fc"}, + {file = "aiosmtplib-3.0.2.tar.gz", hash = "sha256:08fd840f9dbc23258025dca229e8a8f04d2ccf3ecb1319585615bfc7933f7f47"}, +] [[package]] name = "alembic" @@ -17,12 +27,20 @@ dependencies = [ "SQLAlchemy>=1.3.0", "typing-extensions>=4", ] +files = [ + {file = "alembic-1.13.2-py3-none-any.whl", hash = "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953"}, + {file = "alembic-1.13.2.tar.gz", hash = "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef"}, +] [[package]] name = "annotated-types" version = "0.7.0" requires_python = ">=3.8" summary = "Reusable constraint types to use with typing.Annotated" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] [[package]] name = "anyio" @@ -33,12 +51,10 @@ dependencies = [ "idna>=2.8", "sniffio>=1.1", ] - -[[package]] -name = "appnope" -version = "0.1.4" -requires_python = ">=3.6" -summary = "Disable App Nap on macOS >= 10.9" +files = [ + {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, + {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, +] [[package]] name = "argon2-cffi" @@ -48,6 +64,10 @@ summary = "Argon2 for Python" dependencies = [ "argon2-cffi-bindings", ] +files = [ + {file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, + {file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, +] [[package]] name = "argon2-cffi-bindings" @@ -57,46 +77,68 @@ summary = "Low-level CFFI bindings for Argon2" dependencies = [ "cffi>=1.0.1", ] - -[[package]] -name = "asttokens" -version = "2.4.1" -summary = "Annotate AST trees with source code positions" -dependencies = [ - "six>=1.12.0", +files = [ + {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, + {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, + {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, + {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, + {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, ] -[[package]] -name = "async-timeout" -version = "4.0.3" -requires_python = ">=3.7" -summary = "Timeout context manager for asyncio programs" - -[[package]] -name = "asyncpg" -version = "0.29.0" -requires_python = ">=3.8.0" -summary = "An asyncio PostgreSQL driver" -dependencies = [ - "async-timeout>=4.0.3; python_version < \"3.12.0\"", -] - -[[package]] -name = "backcall" -version = "0.2.0" -summary = "Specifications for callback functions passed in to an API" - [[package]] name = "bcrypt" version = "4.0.1" requires_python = ">=3.6" summary = "Modern password hashing for your software and your servers" +files = [ + {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, + {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, + {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, + {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, + {file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, + {file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, + {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, + {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, + {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, + {file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, +] [[package]] name = "certifi" version = "2024.7.4" requires_python = ">=3.6" summary = "Python package for providing Mozilla's CA Bundle." +files = [ + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, +] [[package]] name = "cffi" @@ -106,12 +148,70 @@ summary = "Foreign Function Interface for Python calling C code." dependencies = [ "pycparser", ] +files = [ + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] [[package]] name = "charset-normalizer" version = "3.3.2" requires_python = ">=3.7.0" summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] [[package]] name = "click" @@ -121,34 +221,21 @@ summary = "Composable command line interface toolkit" dependencies = [ "colorama; platform_system == \"Windows\"", ] +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] [[package]] name = "colorama" version = "0.4.6" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" summary = "Cross-platform colored terminal text." - -[[package]] -name = "databases" -version = "0.9.0" -requires_python = ">=3.8" -summary = "Async database support for Python." -dependencies = [ - "sqlalchemy>=2.0.7", +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "decorator" -version = "5.1.1" -requires_python = ">=3.5" -summary = "Decorators for Humans" - -[[package]] -name = "dnspython" -version = "2.6.1" -requires_python = ">=3.8" -summary = "DNS toolkit" - [[package]] name = "drone-flightplan" version = "0.2.1" @@ -159,23 +246,11 @@ dependencies = [ "pyproj>=3.6.1", "shapely==2.0.2", ] - -[[package]] -name = "email-validator" -version = "2.2.0" -requires_python = ">=3.8" -summary = "A robust email address syntax and deliverability validation library." -dependencies = [ - "dnspython>=2.0.0", - "idna>=2.0.0", +files = [ + {file = "drone_flightplan-0.2.1-py3-none-any.whl", hash = "sha256:e5257eb43d706fe1d44a08a75476ed511d1bd5f5bc27d4c5129158f45d504a4d"}, + {file = "drone_flightplan-0.2.1.tar.gz", hash = "sha256:e937961a5ac226603d374fe7b651cc7ebcb931dc35530ea8180b73fc77ac4a14"}, ] -[[package]] -name = "executing" -version = "2.0.1" -requires_python = ">=3.5" -summary = "Get the currently executing AST node of a frame, and other information" - [[package]] name = "fastapi" version = "0.104.1" @@ -187,11 +262,18 @@ dependencies = [ "starlette<0.28.0,>=0.27.0", "typing-extensions>=4.8.0", ] +files = [ + {file = "fastapi-0.104.1-py3-none-any.whl", hash = "sha256:752dc31160cdbd0436bb93bad51560b57e525cbb1d4bbf6f4904ceee75548241"}, + {file = "fastapi-0.104.1.tar.gz", hash = "sha256:e5e4540a7c5e1dcfbbcf5b903c234feddcdcd881f191977a1c5dfd917487e7ae"}, +] [[package]] name = "flatdict" version = "4.0.1" summary = "Python module for interacting with nested dicts as a single level dict with delimited keys." +files = [ + {file = "flatdict-4.0.1.tar.gz", hash = "sha256:cd32f08fd31ed21eb09ebc76f06b6bd12046a24f77beb1fd0281917e47f26742"}, +] [[package]] name = "fmtm-splitter" @@ -205,12 +287,19 @@ dependencies = [ "psycopg2>=2.9.1", "shapely>=1.8.1", ] +files = [ + {file = "fmtm-splitter-1.2.2.tar.gz", hash = "sha256:9384dbf00c0e53e24e1f13046ae6693e13567ff3dc0f59f29f4a96ac4a54105e"}, + {file = "fmtm_splitter-1.2.2-py3-none-any.whl", hash = "sha256:bbef78cf0e1f2b67f8c8aeaadb7fd2927bfd333d216927059a12abbbb04a5742"}, +] [[package]] name = "gdal" version = "3.6.2" requires_python = ">=3.6.0" -summary = "GDAL: Geospatial Data Abstraction Library" +summary = "" +files = [ + {file = "GDAL-3.6.2.tar.gz", hash = "sha256:a167cde1813707d91a938dad1a22f280f5ad28c45980d42e948fb8c59f890f5a"}, +] [[package]] name = "geoalchemy2" @@ -221,12 +310,20 @@ dependencies = [ "SQLAlchemy>=1.4", "packaging", ] +files = [ + {file = "GeoAlchemy2-0.14.2-py3-none-any.whl", hash = "sha256:ca81c2d924c0724458102bac93f68f3e3c337a65fcb811af5e504ce7c5d56ac2"}, + {file = "GeoAlchemy2-0.14.2.tar.gz", hash = "sha256:8ca023dcb9a36c6d312f3b4aee631d66385264e2fc9feb0ab0f446eb5609407d"}, +] [[package]] name = "geojson" version = "3.1.0" requires_python = ">=3.7" summary = "Python bindings and utilities for GeoJSON" +files = [ + {file = "geojson-3.1.0-py3-none-any.whl", hash = "sha256:68a9771827237adb8c0c71f8527509c8f5bef61733aa434cefc9c9d4f0ebe8f3"}, + {file = "geojson-3.1.0.tar.gz", hash = "sha256:58a7fa40727ea058efc28b0e9ff0099eadf6d0965e04690830208d3ef571adac"}, +] [[package]] name = "geojson-pydantic" @@ -236,43 +333,56 @@ summary = "Pydantic data models for the GeoJSON spec." dependencies = [ "pydantic~=2.0", ] +files = [ + {file = "geojson_pydantic-1.0.1-py3-none-any.whl", hash = "sha256:da8c15f15a0a9fc3e0af0253f0c2bb8a948f95ece9a0356f43d4738fa2be5107"}, + {file = "geojson_pydantic-1.0.1.tar.gz", hash = "sha256:a996ffccd5a016d3acb4a0c6aac941d2c569e3c6163d5ce6a04b61ee131c8f94"}, +] [[package]] name = "greenlet" version = "3.0.3" requires_python = ">=3.7" summary = "Lightweight in-process concurrent programming" +files = [ + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, +] [[package]] name = "h11" version = "0.14.0" requires_python = ">=3.7" summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] [[package]] name = "idna" version = "3.7" requires_python = ">=3.5" summary = "Internationalized Domain Names in Applications (IDNA)" - -[[package]] -name = "ipython" -version = "8.14.0" -requires_python = ">=3.9" -summary = "IPython: Productive Interactive Computing" -dependencies = [ - "appnope; sys_platform == \"darwin\"", - "backcall", - "colorama; sys_platform == \"win32\"", - "decorator", - "jedi>=0.16", - "matplotlib-inline", - "pexpect>4.3; sys_platform != \"win32\"", - "pickleshare", - "prompt-toolkit!=3.0.37,<3.1.0,>=3.0.30", - "pygments>=2.4.0", - "stack-data", - "traitlets>=5", +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] [[package]] @@ -280,14 +390,9 @@ name = "itsdangerous" version = "2.2.0" requires_python = ">=3.8" summary = "Safely pass data to untrusted environments and back." - -[[package]] -name = "jedi" -version = "0.19.1" -requires_python = ">=3.6" -summary = "An autocompletion tool for Python that can be used for text editors." -dependencies = [ - "parso<0.9.0,>=0.8.3", +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, ] [[package]] @@ -298,6 +403,10 @@ summary = "A very fast and expressive template engine." dependencies = [ "MarkupSafe>=2.0", ] +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] [[package]] name = "loguru" @@ -308,6 +417,10 @@ dependencies = [ "colorama>=0.3.4; sys_platform == \"win32\"", "win32-setctime>=1.0.0; sys_platform == \"win32\"", ] +files = [ + {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, + {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, +] [[package]] name = "mako" @@ -317,20 +430,38 @@ summary = "A super-fast templating language that borrows the best ideas from the dependencies = [ "MarkupSafe>=0.9.2", ] +files = [ + {file = "Mako-1.3.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a"}, + {file = "Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"}, +] [[package]] name = "markupsafe" version = "2.1.5" requires_python = ">=3.7" summary = "Safely add untrusted strings to HTML/XML markup." - -[[package]] -name = "matplotlib-inline" -version = "0.1.7" -requires_python = ">=3.8" -summary = "Inline Matplotlib backend for Jupyter" -dependencies = [ - "traitlets", +files = [ + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] @@ -344,18 +475,48 @@ dependencies = [ "typing-extensions", "urllib3", ] +files = [ + {file = "minio-7.2.7-py3-none-any.whl", hash = "sha256:59d1f255d852fe7104018db75b3bebbd987e538690e680f7c5de835e422de837"}, + {file = "minio-7.2.7.tar.gz", hash = "sha256:473d5d53d79f340f3cd632054d0c82d2f93177ce1af2eac34a235bea55708d98"}, +] [[package]] name = "numpy" version = "1.26.4" requires_python = ">=3.9" summary = "Fundamental package for array computing in Python" +files = [ + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] [[package]] name = "oauthlib" version = "3.2.2" requires_python = ">=3.6" summary = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +files = [ + {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, + {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, +] [[package]] name = "osm-rawdata" @@ -374,23 +535,29 @@ dependencies = [ "shapely>=1.8.1", "sqlalchemy>=2.0.0", ] +files = [ + {file = "osm-rawdata-0.3.1.tar.gz", hash = "sha256:8714eebc2a774b8ab366caecfce608f30a18a11d3601db19a5d3c10dddcd4514"}, + {file = "osm_rawdata-0.3.1-py3-none-any.whl", hash = "sha256:21ef255381610c05ff6628a2d30bd2e84c4538c7d7a7355530f706b2dbeeab9c"}, +] [[package]] name = "packaging" version = "24.1" requires_python = ">=3.8" summary = "Core utilities for Python packages" - -[[package]] -name = "parso" -version = "0.8.4" -requires_python = ">=3.6" -summary = "A Python Parser" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] [[package]] name = "passlib" version = "1.7.4" summary = "comprehensive password hashing framework supporting over 30 schemes" +files = [ + {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, + {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, +] [[package]] name = "passlib" @@ -401,27 +568,36 @@ dependencies = [ "bcrypt>=3.1.0", "passlib==1.7.4", ] +files = [ + {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, + {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, +] [[package]] -name = "pexpect" -version = "4.9.0" -summary = "Pexpect allows easy control of interactive console applications." +name = "psycopg" +version = "3.2.1" +requires_python = ">=3.8" +summary = "PostgreSQL database adapter for Python" dependencies = [ - "ptyprocess>=0.5", + "typing-extensions>=4.4", + "tzdata; sys_platform == \"win32\"", +] +files = [ + {file = "psycopg-3.2.1-py3-none-any.whl", hash = "sha256:ece385fb413a37db332f97c49208b36cf030ff02b199d7635ed2fbd378724175"}, + {file = "psycopg-3.2.1.tar.gz", hash = "sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7"}, ] [[package]] -name = "pickleshare" -version = "0.7.5" -summary = "Tiny 'shelve'-like database with concurrency support" - -[[package]] -name = "prompt-toolkit" -version = "3.0.47" -requires_python = ">=3.7.0" -summary = "Library for building powerful interactive command lines in Python" +name = "psycopg-pool" +version = "3.2.2" +requires_python = ">=3.8" +summary = "Connection Pool for Psycopg" dependencies = [ - "wcwidth", + "typing-extensions>=4.4", +] +files = [ + {file = "psycopg_pool-3.2.2-py3-none-any.whl", hash = "sha256:273081d0fbfaced4f35e69200c89cb8fbddfe277c38cc86c235b90a2ec2c8153"}, + {file = "psycopg_pool-3.2.2.tar.gz", hash = "sha256:9e22c370045f6d7f2666a5ad1b0caf345f9f1912195b0b25d0d3bcc4f3a7389c"}, ] [[package]] @@ -429,22 +605,28 @@ name = "psycopg2" version = "2.9.9" requires_python = ">=3.7" summary = "psycopg2 - Python-PostgreSQL Database Adapter" +files = [ + {file = "psycopg2-2.9.9-cp311-cp311-win32.whl", hash = "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372"}, + {file = "psycopg2-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981"}, + {file = "psycopg2-2.9.9-cp312-cp312-win32.whl", hash = "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024"}, + {file = "psycopg2-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693"}, + {file = "psycopg2-2.9.9.tar.gz", hash = "sha256:d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156"}, +] [[package]] -name = "psycopg2-binary" -version = "2.9.9" -requires_python = ">=3.7" -summary = "psycopg2 - Python-PostgreSQL Database Adapter" - -[[package]] -name = "ptyprocess" -version = "0.7.0" -summary = "Run a subprocess in a pseudo terminal" - -[[package]] -name = "pure-eval" -version = "0.2.3" -summary = "Safely evaluate AST nodes without side effects" +name = "psycopg" +version = "3.2.1" +extras = ["pool"] +requires_python = ">=3.8" +summary = "PostgreSQL database adapter for Python" +dependencies = [ + "psycopg-pool", + "psycopg==3.2.1", +] +files = [ + {file = "psycopg-3.2.1-py3-none-any.whl", hash = "sha256:ece385fb413a37db332f97c49208b36cf030ff02b199d7635ed2fbd378724175"}, + {file = "psycopg-3.2.1.tar.gz", hash = "sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7"}, +] [[package]] name = "pyarrow" @@ -454,18 +636,62 @@ summary = "Python library for Apache Arrow" dependencies = [ "numpy>=1.16.6", ] +files = [ + {file = "pyarrow-17.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977"}, + {file = "pyarrow-17.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3"}, + {file = "pyarrow-17.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15"}, + {file = "pyarrow-17.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597"}, + {file = "pyarrow-17.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420"}, + {file = "pyarrow-17.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4"}, + {file = "pyarrow-17.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03"}, + {file = "pyarrow-17.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22"}, + {file = "pyarrow-17.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053"}, + {file = "pyarrow-17.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a"}, + {file = "pyarrow-17.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc"}, + {file = "pyarrow-17.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a"}, + {file = "pyarrow-17.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b"}, + {file = "pyarrow-17.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7"}, + {file = "pyarrow-17.0.0.tar.gz", hash = "sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28"}, +] [[package]] name = "pycparser" version = "2.22" requires_python = ">=3.8" summary = "C parser in Python" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] [[package]] name = "pycryptodome" version = "3.20.0" requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" summary = "Cryptographic library for Python" +files = [ + {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044"}, + {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a"}, + {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2"}, + {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c"}, + {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25"}, + {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128"}, + {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c"}, + {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4"}, + {file = "pycryptodome-3.20.0-cp35-abi3-win32.whl", hash = "sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72"}, + {file = "pycryptodome-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9"}, + {file = "pycryptodome-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a"}, + {file = "pycryptodome-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e"}, + {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04"}, + {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3"}, + {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea"}, + {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b"}, + {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a60fedd2b37b4cb11ccb5d0399efe26db9e0dd149016c1cc6c8161974ceac2d6"}, + {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:405002eafad114a2f9a930f5db65feef7b53c4784495dd8758069b89baf68eab"}, + {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab6ab0cb755154ad14e507d1df72de9897e99fd2d4922851a276ccc14f4f1a5"}, + {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acf6e43fa75aca2d33e93409f2dafe386fe051818ee79ee8a3e21de9caa2ac9e"}, + {file = "pycryptodome-3.20.0.tar.gz", hash = "sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7"}, +] [[package]] name = "pydantic" @@ -477,6 +703,10 @@ dependencies = [ "pydantic-core==2.14.5", "typing-extensions>=4.6.1", ] +files = [ + {file = "pydantic-2.5.2-py3-none-any.whl", hash = "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0"}, + {file = "pydantic-2.5.2.tar.gz", hash = "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd"}, +] [[package]] name = "pydantic-core" @@ -486,6 +716,65 @@ summary = "" dependencies = [ "typing-extensions!=4.7.0,>=4.6.0", ] +files = [ + {file = "pydantic_core-2.14.5-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459"}, + {file = "pydantic_core-2.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4"}, + {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada"}, + {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda"}, + {file = "pydantic_core-2.14.5-cp311-none-win32.whl", hash = "sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651"}, + {file = "pydantic_core-2.14.5-cp311-none-win_amd64.whl", hash = "sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077"}, + {file = "pydantic_core-2.14.5-cp311-none-win_arm64.whl", hash = "sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf"}, + {file = "pydantic_core-2.14.5-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093"}, + {file = "pydantic_core-2.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e"}, + {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69"}, + {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d"}, + {file = "pydantic_core-2.14.5-cp312-none-win32.whl", hash = "sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260"}, + {file = "pydantic_core-2.14.5-cp312-none-win_amd64.whl", hash = "sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36"}, + {file = "pydantic_core-2.14.5-cp312-none-win_arm64.whl", hash = "sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3"}, + {file = "pydantic_core-2.14.5.tar.gz", hash = "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71"}, +] [[package]] name = "pydantic-settings" @@ -496,18 +785,20 @@ dependencies = [ "pydantic>=2.3.0", "python-dotenv>=0.21.0", ] - -[[package]] -name = "pygments" -version = "2.18.0" -requires_python = ">=3.8" -summary = "Pygments is a syntax highlighting package written in Python." +files = [ + {file = "pydantic_settings-2.1.0-py3-none-any.whl", hash = "sha256:7621c0cb5d90d1140d2f0ef557bdf03573aac7035948109adf2574770b77605a"}, + {file = "pydantic_settings-2.1.0.tar.gz", hash = "sha256:26b1492e0a24755626ac5e6d715e9077ab7ad4fb5f19a8b7ed7011d52f36141c"}, +] [[package]] name = "pyjwt" version = "2.9.0" requires_python = ">=3.8" summary = "JSON Web Token implementation in Python" +files = [ + {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, + {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, +] [[package]] name = "pyproj" @@ -517,18 +808,43 @@ summary = "Python interface to PROJ (cartographic projections and coordinate tra dependencies = [ "certifi", ] +files = [ + {file = "pyproj-3.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ebfbdbd0936e178091309f6cd4fcb4decd9eab12aa513cdd9add89efa3ec2882"}, + {file = "pyproj-3.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:447db19c7efad70ff161e5e46a54ab9cc2399acebb656b6ccf63e4bc4a04b97a"}, + {file = "pyproj-3.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7e13c40183884ec7f94eb8e0f622f08f1d5716150b8d7a134de48c6110fee85"}, + {file = "pyproj-3.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65ad699e0c830e2b8565afe42bd58cc972b47d829b2e0e48ad9638386d994915"}, + {file = "pyproj-3.6.1-cp311-cp311-win32.whl", hash = "sha256:8b8acc31fb8702c54625f4d5a2a6543557bec3c28a0ef638778b7ab1d1772132"}, + {file = "pyproj-3.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:38a3361941eb72b82bd9a18f60c78b0df8408416f9340521df442cebfc4306e2"}, + {file = "pyproj-3.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1e9fbaf920f0f9b4ee62aab832be3ae3968f33f24e2e3f7fbb8c6728ef1d9746"}, + {file = "pyproj-3.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d227a865356f225591b6732430b1d1781e946893789a609bb34f59d09b8b0f8"}, + {file = "pyproj-3.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83039e5ae04e5afc974f7d25ee0870a80a6bd6b7957c3aca5613ccbe0d3e72bf"}, + {file = "pyproj-3.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb059ba3bced6f6725961ba758649261d85ed6ce670d3e3b0a26e81cf1aa8d"}, + {file = "pyproj-3.6.1-cp312-cp312-win32.whl", hash = "sha256:2d6ff73cc6dbbce3766b6c0bce70ce070193105d8de17aa2470009463682a8eb"}, + {file = "pyproj-3.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:7a27151ddad8e1439ba70c9b4b2b617b290c39395fa9ddb7411ebb0eb86d6fb0"}, + {file = "pyproj-3.6.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd93c1a0c6c4aedc77c0fe275a9f2aba4d59b8acf88cebfc19fe3c430cfabf4f"}, + {file = "pyproj-3.6.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6420ea8e7d2a88cb148b124429fba8cd2e0fae700a2d96eab7083c0928a85110"}, + {file = "pyproj-3.6.1.tar.gz", hash = "sha256:44aa7c704c2b7d8fb3d483bbf75af6cb2350d30a63b144279a09b75fead501bf"}, +] [[package]] name = "python-dotenv" version = "1.0.0" requires_python = ">=3.8" summary = "Read key-value pairs from a .env file and set them as environment variables" +files = [ + {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, + {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, +] [[package]] name = "python-multipart" version = "0.0.9" requires_python = ">=3.8" summary = "A streaming multipart parser for Python" +files = [ + {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, + {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, +] [[package]] name = "python-slugify" @@ -538,12 +854,34 @@ summary = "A Python slugify application that also handles Unicode" dependencies = [ "text-unidecode>=1.3", ] +files = [ + {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, + {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, +] [[package]] name = "pyyaml" version = "6.0.1" requires_python = ">=3.6" summary = "YAML parser and emitter for Python" +files = [ + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] [[package]] name = "requests" @@ -556,6 +894,10 @@ dependencies = [ "idna<4,>=2.5", "urllib3<3,>=1.21.1", ] +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] [[package]] name = "requests-oauthlib" @@ -566,6 +908,10 @@ dependencies = [ "oauthlib>=3.0.0", "requests>=2.0.0", ] +files = [ + {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, + {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, +] [[package]] name = "shapely" @@ -575,18 +921,33 @@ summary = "Manipulation and analysis of geometric objects" dependencies = [ "numpy>=1.14", ] - -[[package]] -name = "six" -version = "1.16.0" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -summary = "Python 2 and 3 compatibility utilities" +files = [ + {file = "shapely-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b0c052709c8a257c93b0d4943b0b7a3035f87e2d6a8ac9407b6a992d206422f"}, + {file = "shapely-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2d217e56ae067e87b4e1731d0dc62eebe887ced729ba5c2d4590e9e3e9fdbd88"}, + {file = "shapely-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94ac128ae2ab4edd0bffcd4e566411ea7bdc738aeaf92c32a8a836abad725f9f"}, + {file = "shapely-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa3ee28f5e63a130ec5af4dc3c4cb9c21c5788bb13c15e89190d163b14f9fb89"}, + {file = "shapely-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:737dba15011e5a9b54a8302f1748b62daa207c9bc06f820cd0ad32a041f1c6f2"}, + {file = "shapely-2.0.2-cp311-cp311-win32.whl", hash = "sha256:45ac6906cff0765455a7b49c1670af6e230c419507c13e2f75db638c8fc6f3bd"}, + {file = "shapely-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:dc9342fc82e374130db86a955c3c4525bfbf315a248af8277a913f30911bed9e"}, + {file = "shapely-2.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:06f193091a7c6112fc08dfd195a1e3846a64306f890b151fa8c63b3e3624202c"}, + {file = "shapely-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:eebe544df5c018134f3c23b6515877f7e4cd72851f88a8d0c18464f414d141a2"}, + {file = "shapely-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7e92e7c255f89f5cdf777690313311f422aa8ada9a3205b187113274e0135cd8"}, + {file = "shapely-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be46d5509b9251dd9087768eaf35a71360de6afac82ce87c636990a0871aa18b"}, + {file = "shapely-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5533a925d8e211d07636ffc2fdd9a7f9f13d54686d00577eeb11d16f00be9c4"}, + {file = "shapely-2.0.2-cp312-cp312-win32.whl", hash = "sha256:084b023dae8ad3d5b98acee9d3bf098fdf688eb0bb9b1401e8b075f6a627b611"}, + {file = "shapely-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:ea84d1cdbcf31e619d672b53c4532f06253894185ee7acb8ceb78f5f33cbe033"}, + {file = "shapely-2.0.2.tar.gz", hash = "sha256:1713cc04c171baffc5b259ba8531c58acc2a301707b7f021d88a15ed090649e7"}, +] [[package]] name = "sniffio" version = "1.3.1" requires_python = ">=3.7" summary = "Sniff out which async library your code is running under" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] [[package]] name = "sqlalchemy" @@ -594,9 +955,29 @@ version = "2.0.23" requires_python = ">=3.7" summary = "Database Abstraction Library" dependencies = [ - "greenlet!=0.4.17; platform_machine == \"aarch64\" or (platform_machine == \"ppc64le\" or (platform_machine == \"x86_64\" or (platform_machine == \"amd64\" or (platform_machine == \"AMD64\" or (platform_machine == \"win32\" or platform_machine == \"WIN32\")))))", + "greenlet!=0.4.17; platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\"", "typing-extensions>=4.2.0", ] +files = [ + {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-win32.whl", hash = "sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855"}, + {file = "SQLAlchemy-2.0.23-cp311-cp311-win_amd64.whl", hash = "sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-win32.whl", hash = "sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846"}, + {file = "SQLAlchemy-2.0.23-cp312-cp312-win_amd64.whl", hash = "sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca"}, + {file = "SQLAlchemy-2.0.23-py3-none-any.whl", hash = "sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d"}, + {file = "SQLAlchemy-2.0.23.tar.gz", hash = "sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69"}, +] [[package]] name = "sqlalchemy-utils" @@ -606,15 +987,9 @@ summary = "Various utility functions for SQLAlchemy." dependencies = [ "SQLAlchemy>=1.3", ] - -[[package]] -name = "stack-data" -version = "0.6.3" -summary = "Extract data from python stack frames and tracebacks for informative displays" -dependencies = [ - "asttokens>=2.1.0", - "executing>=1.2.0", - "pure-eval", +files = [ + {file = "SQLAlchemy-Utils-0.41.1.tar.gz", hash = "sha256:a2181bff01eeb84479e38571d2c0718eb52042f9afd8c194d0d02877e84b7d74"}, + {file = "SQLAlchemy_Utils-0.41.1-py3-none-any.whl", hash = "sha256:6c96b0768ea3f15c0dc56b363d386138c562752b84f647fb8d31a2223aaab801"}, ] [[package]] @@ -625,29 +1000,49 @@ summary = "The little ASGI library that shines." dependencies = [ "anyio<5,>=3.4.0", ] +files = [ + {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, + {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, +] [[package]] name = "text-unidecode" version = "1.3" summary = "The most basic Text::Unidecode port" - -[[package]] -name = "traitlets" -version = "5.14.3" -requires_python = ">=3.8" -summary = "Traitlets Python configuration system" +files = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] [[package]] name = "typing-extensions" version = "4.12.2" requires_python = ">=3.8" summary = "Backported and Experimental Type Hints for Python 3.8+" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "tzdata" +version = "2024.1" +requires_python = ">=2" +summary = "Provider of IANA time zone data" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] [[package]] name = "urllib3" version = "2.2.2" requires_python = ">=3.8" summary = "HTTP library with thread-safe connection pooling, file post, and more." +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] [[package]] name = "uvicorn" @@ -658,19 +1053,9 @@ dependencies = [ "click>=7.0", "h11>=0.8", ] - -[[package]] -name = "wcwidth" -version = "0.2.13" -summary = "Measures the displayed width of unicode strings in a terminal" - -[[package]] -name = "werkzeug" -version = "3.0.1" -requires_python = ">=3.8" -summary = "The comprehensive WSGI web application library." -dependencies = [ - "MarkupSafe>=2.1.1", +files = [ + {file = "uvicorn-0.24.0-py3-none-any.whl", hash = "sha256:3d19f13dfd2c2af1bfe34dd0f7155118ce689425fdf931177abe832ca44b8a04"}, + {file = "uvicorn-0.24.0.tar.gz", hash = "sha256:368d5d81520a51be96431845169c225d771c9dd22a58613e1a181e6c4512ac33"}, ] [[package]] @@ -678,1120 +1063,7 @@ name = "win32-setctime" version = "1.1.0" requires_python = ">=3.5" summary = "A small Python utility to set file creation time on Windows" - -[metadata] -lock_version = "4.2" -cross_platform = true -groups = ["default"] -content_hash = "sha256:96e44ede40b8b6d3ce3559102c612d8b178d8099bf119a0484663968fee74a46" - -[metadata.files] -"aiosmtplib 3.0.2" = [ - {url = "https://files.pythonhosted.org/packages/87/35/441faea7a11159795881a6ec869454f40269e4e3806dced935a35d83a412/aiosmtplib-3.0.2-py3-none-any.whl", hash = "sha256:8783059603a34834c7c90ca51103c3aa129d5922003b5ce98dbaa6d4440f10fc"}, - {url = "https://files.pythonhosted.org/packages/91/2a/812517f8350cd317aad2ba1ce25dfc213c6f1f2e62e1cbf662b4bdc51d34/aiosmtplib-3.0.2.tar.gz", hash = "sha256:08fd840f9dbc23258025dca229e8a8f04d2ccf3ecb1319585615bfc7933f7f47"}, -] -"alembic 1.13.2" = [ - {url = "https://files.pythonhosted.org/packages/66/e2/efa88e86029cada2da5941ec664d50d9a3b2a91f5066405c6f90e5016c16/alembic-1.13.2.tar.gz", hash = "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef"}, - {url = "https://files.pythonhosted.org/packages/df/ed/c884465c33c25451e4a5cd4acad154c29e5341e3214e220e7f3478aa4b0d/alembic-1.13.2-py3-none-any.whl", hash = "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953"}, -] -"annotated-types 0.7.0" = [ - {url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] -"anyio 3.7.1" = [ - {url = "https://files.pythonhosted.org/packages/19/24/44299477fe7dcc9cb58d0a57d5a7588d6af2ff403fdd2d47a246c91a3246/anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, - {url = "https://files.pythonhosted.org/packages/28/99/2dfd53fd55ce9838e6ff2d4dac20ce58263798bd1a0dbe18b3a9af3fcfce/anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, -] -"appnope 0.1.4" = [ - {url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, - {url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, -] -"argon2-cffi 23.1.0" = [ - {url = "https://files.pythonhosted.org/packages/31/fa/57ec2c6d16ecd2ba0cf15f3c7d1c3c2e7b5fcb83555ff56d7ab10888ec8f/argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"}, - {url = "https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"}, -] -"argon2-cffi-bindings 21.2.0" = [ - {url = "https://files.pythonhosted.org/packages/2e/f1/48888db30b6a4a0c78ab7bc7444058a1135b223b6a2a5f2ac7d6780e7443/argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, - {url = "https://files.pythonhosted.org/packages/34/da/d105a3235ae86c1c1a80c1e9c46953e6e53cc8c4c61fb3c5ac8a39bbca48/argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, - {url = "https://files.pythonhosted.org/packages/37/2c/e34e47c7dee97ba6f01a6203e0383e15b60fb85d78ac9a15cd066f6fe28b/argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, - {url = "https://files.pythonhosted.org/packages/43/f3/20bc53a6e50471dfea16a63dc9b69d2a9ec78fd2b9532cc25f8317e121d9/argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, - {url = "https://files.pythonhosted.org/packages/4f/fd/37f86deef67ff57c76f137a67181949c2d408077e2e3dd70c6c42912c9bf/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, - {url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, - {url = "https://files.pythonhosted.org/packages/6f/52/5a60085a3dae8fded8327a4f564223029f5f54b0cb0455a31131b5363a01/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, - {url = "https://files.pythonhosted.org/packages/74/2b/73d767bfdaab25484f7e7901379d5f8793cccbb86c6e0cbc4c1b96f63896/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"}, - {url = "https://files.pythonhosted.org/packages/74/f6/4a34a37a98311ed73bb80efe422fed95f2ac25a4cacc5ae1d7ae6a144505/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"}, - {url = "https://files.pythonhosted.org/packages/8b/95/143cd64feb24a15fa4b189a3e1e7efbaeeb00f39a51e99b26fc62fbacabd/argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, - {url = "https://files.pythonhosted.org/packages/8c/1b/b2abebe25743daf80db3ee3ea37e4d446c8fbcc5abb7c06baf7261f5678d/argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"}, - {url = "https://files.pythonhosted.org/packages/b3/02/f7f7bb6b6af6031edb11037639c697b912e1dea2db94d436e681aea2f495/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"}, - {url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, - {url = "https://files.pythonhosted.org/packages/c5/98/6cdb23d0aeb8612175e2d0fcffe776eb18d22d73e1efe4322f6a9d2bab12/argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"}, - {url = "https://files.pythonhosted.org/packages/d4/13/838ce2620025e9666aa8f686431f67a29052241692a3dd1ae9d3692a89d3/argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"}, - {url = "https://files.pythonhosted.org/packages/dc/46/610263c404f33127878515819217aafd150906814624c31a6ad18a0a40fb/argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"}, - {url = "https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"}, - {url = "https://files.pythonhosted.org/packages/ed/55/f8ba268bc9005d0ca57a862e8f1b55bf1775e97a36bd30b0a8fb568c265c/argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, - {url = "https://files.pythonhosted.org/packages/ee/0f/a2260a207f21ce2ff4cad00a417c31597f08eafb547e00615bcbf403d8ea/argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, - {url = "https://files.pythonhosted.org/packages/f2/c6/e1ea7fc615ac7f9aaa4397e4ace245557d5bb25b4a594b06dccb2d90e05d/argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"}, - {url = "https://files.pythonhosted.org/packages/f4/64/bef937102280c7c92dd47dd9a67b6c76ef6a276f736c419ea538fa86adf8/argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"}, -] -"asttokens 2.4.1" = [ - {url = "https://files.pythonhosted.org/packages/45/1d/f03bcb60c4a3212e15f99a56085d93093a497718adf828d050b9d675da81/asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, - {url = "https://files.pythonhosted.org/packages/45/86/4736ac618d82a20d87d2f92ae19441ebc7ac9e7a581d7e58bbe79233b24a/asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, -] -"async-timeout 4.0.3" = [ - {url = "https://files.pythonhosted.org/packages/87/d6/21b30a550dafea84b1b8eee21b5e23fa16d010ae006011221f33dcd8d7f8/async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, - {url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, -] -"asyncpg 0.29.0" = [ - {url = "https://files.pythonhosted.org/packages/06/df/5cc866069c3a248a67d59a3de495afec34b4d36ed74101da4dfa1f456167/asyncpg-0.29.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169"}, - {url = "https://files.pythonhosted.org/packages/0b/a6/db95467c03b9b2ede7ed1417c2a76608eb204c37dabe79d52263999c2e66/asyncpg-0.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0009a300cae37b8c525e5b449233d59cd9868fd35431abc470a3e364d2b85cb9"}, - {url = "https://files.pythonhosted.org/packages/16/1b/bb42784e9895832bf460ee6643f818bd53e4d6a6308cca5984c581a51845/asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59"}, - {url = "https://files.pythonhosted.org/packages/16/48/e0c950af52a21fe71f0e0ba71830511e78cd455957fe2d25badf89ad12a8/asyncpg-0.29.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48e7c58b516057126b363cec8ca02b804644fd012ef8e6c7e23386b7d5e6ce83"}, - {url = "https://files.pythonhosted.org/packages/1f/fb/e5b798ff0d6aceda7067dad9dbf1a11016ef7c8d0117d75f031a39f5ed1e/asyncpg-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746e80d83ad5d5464cfbf94315eb6744222ab00aa4e522b704322fb182b83610"}, - {url = "https://files.pythonhosted.org/packages/27/25/d140bd503932f99528edc0a1461648973ad3c1c67f5929d11f3e8b5f81f4/asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870"}, - {url = "https://files.pythonhosted.org/packages/38/85/2cf972c94101868d472fc43dacaf0d379dbf3f7c433e47df775f019f4ffa/asyncpg-0.29.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:642a36eb41b6313ffa328e8a5c5c2b5bea6ee138546c9c3cf1bffaad8ee36dd9"}, - {url = "https://files.pythonhosted.org/packages/45/13/a9d65680aef1ba26c2373ad94f9268ac1c358a6d36b6479435a8c5b759ef/asyncpg-0.29.0-cp38-cp38-win32.whl", hash = "sha256:a921372bbd0aa3a5822dd0409da61b4cd50df89ae85150149f8c119f23e8c408"}, - {url = "https://files.pythonhosted.org/packages/49/ac/0396e559e1e7ab23787f790ae96b22affe2d66acebb084d6fc42293d12b8/asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364"}, - {url = "https://files.pythonhosted.org/packages/4a/13/f96284d7014dd06db2e78bea15706443d7895548bf74cf34f0c3ee1863fd/asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac"}, - {url = "https://files.pythonhosted.org/packages/4d/53/0b9e8f09a87cb764fa8e99c3ff3c6286ef7f29a92782e5667a38032e23d1/asyncpg-0.29.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f100d23f273555f4b19b74a96840aa27b85e99ba4b1f18d4ebff0734e78dc090"}, - {url = "https://files.pythonhosted.org/packages/4e/fc/79a886866569a5874b8e276d6113bf67250ff506280efbd4dd6d5742875e/asyncpg-0.29.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:012d01df61e009015944ac7543d6ee30c2dc1eb2f6b10b62a3f598beb6531548"}, - {url = "https://files.pythonhosted.org/packages/5b/89/3ed6e9d235f8aa13aa8ee8dc3a70f754962dbd441bec2dcfdae9f9e0e2e3/asyncpg-0.29.0-cp311-cp311-win32.whl", hash = "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675"}, - {url = "https://files.pythonhosted.org/packages/5b/b4/22cfdc5c41d32b2f04599b740db6053e509443bc326f6cfd5d9c70d9f02f/asyncpg-0.29.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000c996c53c04770798053e1730d34e30cb645ad95a63265aec82da9093d88e7"}, - {url = "https://files.pythonhosted.org/packages/69/28/3e3c4e243778f0361214b9d6e8bc6aa8e8bf55f35a2d2cb8949a6863caab/asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4"}, - {url = "https://files.pythonhosted.org/packages/6b/4d/ee74620647766e67fb6fe88554e49874eb74e34903a2b6c32319cb97e271/asyncpg-0.29.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e17b52c6cf83e170d3d865571ba574577ab8e533e7361a2b8ce6157d02c665d3"}, - {url = "https://files.pythonhosted.org/packages/6d/64/89d970c41baeece88b4123e0b53e13ef855596e33aeb060031cd9b720a40/asyncpg-0.29.0-cp39-cp39-win_amd64.whl", hash = "sha256:cce08a178858b426ae1aa8409b5cc171def45d4293626e7aa6510696d46decd8"}, - {url = "https://files.pythonhosted.org/packages/6d/66/0d26bebcb6794bb49cdd0104deba38cb8deed5d86196afb6f6366c03ee4e/asyncpg-0.29.0-cp310-cp310-win32.whl", hash = "sha256:5bbb7f2cafd8d1fa3e65431833de2642f4b2124be61a449fa064e1a08d27e449"}, - {url = "https://files.pythonhosted.org/packages/71/86/7a18e1a457afb73991e5e5586e2341af09a31c91d8f65cc003f0b4553252/asyncpg-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe"}, - {url = "https://files.pythonhosted.org/packages/74/49/243b77ff7ac7c11ec771ce0d76df20e7af73ea92c0399bd480ef794ce946/asyncpg-0.29.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5340dd515d7e52f4c11ada32171d87c05570479dc01dc66d03ee3e150fb695da"}, - {url = "https://files.pythonhosted.org/packages/7e/ca/aad32992a1d38ff568e11be44d9b45942b48d50d3647f7b421f62fd99ef3/asyncpg-0.29.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97eb024685b1d7e72b1972863de527c11ff87960837919dac6e34754768098eb"}, - {url = "https://files.pythonhosted.org/packages/88/b0/6bebd69ed484055d47b78ea34fd9887c35694b63c9a648a7f02759d3bf73/asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b"}, - {url = "https://files.pythonhosted.org/packages/91/2e/20e024608c57c2099531ba492c761b12fdd80891a67e58c92de44d05d57e/asyncpg-0.29.0-cp312-cp312-win32.whl", hash = "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02"}, - {url = "https://files.pythonhosted.org/packages/99/38/0bfb00e9b828513bd759174860fd2b1c5e36d0b33985c90ff4ed6f96814c/asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106"}, - {url = "https://files.pythonhosted.org/packages/a0/bd/29e0d546a2cd4e22f8dda2fbeaae73978af2058059c93527237a1e6da855/asyncpg-0.29.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cad1324dbb33f3ca0cd2074d5114354ed3be2b94d48ddfd88af75ebda7c43cc"}, - {url = "https://files.pythonhosted.org/packages/a6/05/fed8ceefaef48dda4a24572906b2931b4bf5b20d037d2fc6b6f66f284439/asyncpg-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:76c3ac6530904838a4b650b2880f8e7af938ee049e769ec2fba7cd66469d7772"}, - {url = "https://files.pythonhosted.org/packages/a9/eb/569047f87d6b7ced42352af3771c1b1e6d39584f072e11068e3e3b4bde68/asyncpg-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385"}, - {url = "https://files.pythonhosted.org/packages/b8/38/d399e70fcfc880a70ae02551a68cfb1b3663d59850943f6e711ab19d3648/asyncpg-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e6823a7012be8b68301342ba33b4740e5a166f6bbda0aee32bc01638491a22"}, - {url = "https://files.pythonhosted.org/packages/ba/b7/4cca567ae0d61e56ab53a211e48b576224ba110cf76bcdc9b0c880300570/asyncpg-0.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:103aad2b92d1506700cbf51cd8bb5441e7e72e87a7b3a2ca4e32c840f051a6a3"}, - {url = "https://files.pythonhosted.org/packages/be/a3/d6002935c546829d9d2138d1bc1929a596e489a35c147d7ed68a496c54d3/asyncpg-0.29.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9ea3f24eb4c49a615573724d88a48bd1b7821c890c2effe04f05382ed9e8810"}, - {url = "https://files.pythonhosted.org/packages/c1/11/7a6000244eaeb6b8ed2238bf33477c486515d6133f2c295913aca3ba4a00/asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e"}, - {url = "https://files.pythonhosted.org/packages/c2/95/d22552d03e69ae55dd3a163d71c8724213d5951ba11535349e49fddf35e4/asyncpg-0.29.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0bfe9c4d3429706cf70d3249089de14d6a01192d617e9093a8e941fea8ee775"}, - {url = "https://files.pythonhosted.org/packages/c4/41/a0bdc18f13bdd5f27e7fc1b5de7e1caae19951967c109bca1a2e99cf3331/asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f"}, - {url = "https://files.pythonhosted.org/packages/d5/98/314ccb06cf587656da2c58afb57b4ff3ddd661108db568c16c181af40436/asyncpg-0.29.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ff8e8109cd6a46ff852a5e6bab8b0a047d7ea42fcb7ca5ae6eaae97d8eacf397"}, - {url = "https://files.pythonhosted.org/packages/d5/d1/7ed5169e30e80573c942f5a6f29b2f87d5b8379bdd9bd916f0ed136c874e/asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175"}, - {url = "https://files.pythonhosted.org/packages/db/06/e388331eed46eaea42a73d38b8aa8f624a2e6ca426f03d4a423bd97d823f/asyncpg-0.29.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8d36c7f14a22ec9e928f15f92a48207546ffe68bc412f3be718eedccdf10dc5c"}, - {url = "https://files.pythonhosted.org/packages/dd/29/980c4401aeb7d3ddff24453fae555cc422b84bdb907c8316f7cd07eab924/asyncpg-0.29.0-cp39-cp39-win32.whl", hash = "sha256:797ab8123ebaed304a1fad4d7576d5376c3a006a4100380fb9d517f0b59c1ab2"}, - {url = "https://files.pythonhosted.org/packages/eb/0b/d128b57f7e994a6d71253d0a6a8c949fc50c969785010d46b87d8491be24/asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb"}, - {url = "https://files.pythonhosted.org/packages/f2/1f/1737248d7b1b75d19e7f07a98321bc58cb6fc979754c78544cfebff3359b/asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23"}, - {url = "https://files.pythonhosted.org/packages/f2/39/f7e755b5d5aa59d8385c08be58726aceffc1da9360041031554d664c783f/asyncpg-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3"}, - {url = "https://files.pythonhosted.org/packages/f2/b7/38b7c195f66a5598413c538da499b3f8119ba5764ded6fff620f7eb84c65/asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178"}, -] -"backcall 0.2.0" = [ - {url = "https://files.pythonhosted.org/packages/4c/1c/ff6546b6c12603d8dd1070aa3c3d273ad4c07f5771689a7b69a550e8c951/backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, - {url = "https://files.pythonhosted.org/packages/a2/40/764a663805d84deee23043e1426a9175567db89c8b3287b5c2ad9f71aa93/backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, -] -"bcrypt 4.0.1" = [ - {url = "https://files.pythonhosted.org/packages/13/68/f3184c1f15581ebd936125b4da04cba0995f97ecd5ee8f4262c8ebba2646/bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, - {url = "https://files.pythonhosted.org/packages/28/ed/3c443bfbfdb37cd7c0d055b961311f49049ab4a00f45ba3bfd10d33a9443/bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, - {url = "https://files.pythonhosted.org/packages/2c/be/376341b47e1e3fc424c9df1af60b5aedbd5ab04f73ccdf4107e42d92ef09/bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, - {url = "https://files.pythonhosted.org/packages/41/16/49ff5146fb815742ad58cafb5034907aa7f166b1344d0ddd7fd1c818bd17/bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, - {url = "https://files.pythonhosted.org/packages/41/86/05248719aa42a4fe1ca379d45794198700e992b91d389bfaa69533fc3331/bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, - {url = "https://files.pythonhosted.org/packages/46/81/d8c22cd7e5e1c6a7d48e41a1d1d46c92f17dae70a54d9814f746e6027dec/bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, - {url = "https://files.pythonhosted.org/packages/5e/01/098b798dc6c6984f2d5026269e80d7cad22d6ecacd5989bdf35a9c99a03d/bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, - {url = "https://files.pythonhosted.org/packages/64/fe/da28a5916128d541da0993328dc5cf4b43dfbf6655f2c7a2abe26ca2dc88/bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, - {url = "https://files.pythonhosted.org/packages/77/2c/53c17079898584306eafdc937e0c7cc1bf8e2fe17e9909716ef3f9d6555d/bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, - {url = "https://files.pythonhosted.org/packages/78/d4/3b2657bd58ef02b23a07729b0df26f21af97169dbd0b5797afa9e97ebb49/bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, - {url = "https://files.pythonhosted.org/packages/7d/50/e683d8418974a602ba40899c8a5c38b3decaf5a4d36c32fc65dce454d8a8/bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, - {url = "https://files.pythonhosted.org/packages/87/69/edacb37481d360d06fc947dab5734aaf511acb7d1a1f9e2849454376c0f8/bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, - {url = "https://files.pythonhosted.org/packages/8c/ae/3af7d006aacf513975fd1948a6b4d6f8b4a307f8a244e1a3d3774b297aad/bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, - {url = "https://files.pythonhosted.org/packages/99/a5/ff4aaf2adbefb2c9808d49cec37f65e0572c4ce856b13b194fd87a6cbd14/bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, - {url = "https://files.pythonhosted.org/packages/aa/48/fd2b197a9741fa790ba0b88a9b10b5e88e62ff5cf3e1bc96d8354d7ce613/bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, - {url = "https://files.pythonhosted.org/packages/aa/ca/6a534669890725cbb8c1fb4622019be31813c8edaa7b6d5b62fc9360a17e/bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, - {url = "https://files.pythonhosted.org/packages/d8/f6/43ade4d37a3319baee9aec53f636411e70c18f0e4add9cc44a18f517af5f/bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, - {url = "https://files.pythonhosted.org/packages/dd/4f/3632a69ce344c1551f7c9803196b191a8181c6a1ad2362c225581ef0d383/bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, - {url = "https://files.pythonhosted.org/packages/ec/0a/1582790232fef6c2aa201f345577306b8bfe465c2c665dec04c86a016879/bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, - {url = "https://files.pythonhosted.org/packages/fb/4b/e255df2000c2de4df524740b5f1d0a31157a1f7715b3eaf2e8f9c5c0acbb/bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, - {url = "https://files.pythonhosted.org/packages/fb/a7/ee4561fd9b78ca23c8e5591c150cc58626a5dfb169345ab18e1c2c664ee0/bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, -] -"certifi 2024.7.4" = [ - {url = "https://files.pythonhosted.org/packages/1c/d5/c84e1a17bf61d4df64ca866a1c9a913874b4e9bdc131ec689a0ad013fb36/certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {url = "https://files.pythonhosted.org/packages/c2/02/a95f2b11e207f68bc64d7aae9666fed2e2b3f307748d5123dffb72a1bbea/certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, -] -"cffi 1.16.0" = [ - {url = "https://files.pythonhosted.org/packages/04/a2/55f290ac034bd98c2a63e83be96925729cb2a70c8c42adc391ec5fbbaffd/cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {url = "https://files.pythonhosted.org/packages/09/d4/8759cc3b2222c159add8ce3af0089912203a31610f4be4c36f98e320b4c6/cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {url = "https://files.pythonhosted.org/packages/18/6c/0406611f3d5aadf4c5b08f6c095d874aed8dfc2d3a19892707d72536d5dc/cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {url = "https://files.pythonhosted.org/packages/20/18/76e26bcfa6a7a62f880791122261575b3048ac57dd72f300ba0827629ab8/cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {url = "https://files.pythonhosted.org/packages/20/3b/f95e667064141843843df8ca79dd49ba57bb7a7615d6d7d538531e45f002/cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {url = "https://files.pythonhosted.org/packages/20/f8/5931cfb7a8cc15d224099cead5e5432efe729bd61abce72d9b3e51e5800b/cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {url = "https://files.pythonhosted.org/packages/22/04/1d10d5baf3faaae9b35f6c49bcf25c1be81ea68cc7ee6923206d02be85b0/cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {url = "https://files.pythonhosted.org/packages/22/05/43cfda378da7bb0aa19b3cf34fe54f8867b0d581294216339d87deefd69c/cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {url = "https://files.pythonhosted.org/packages/33/14/8398798ab001523f1abb2b4170a01bf2114588f3f1fa1f984b3f3bef107e/cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {url = "https://files.pythonhosted.org/packages/36/44/124481b75d228467950b9e81d20ec963f33517ca551f08956f2838517ece/cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {url = "https://files.pythonhosted.org/packages/39/44/4381b8d26e9cfa3e220e3c5386f443a10c6313a6ade7acb314b2bcc0a6ce/cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {url = "https://files.pythonhosted.org/packages/40/c9/cfba735d9ed117471e32d7bce435dd49721261ae294277c64aa929ec9c9d/cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {url = "https://files.pythonhosted.org/packages/47/e3/b6832b1b9a1b6170c585ee2c2d30baf64d0a497c17e6623f42cfeb59c114/cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {url = "https://files.pythonhosted.org/packages/4a/56/572f7f728b20e4d51766e63d7de811e45c7cae727dc1f769caad2973fb52/cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {url = "https://files.pythonhosted.org/packages/4a/ac/a4046ab3d72536eff8bc30b39d767f69bd8be715c5e395b71cfca26f03d9/cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {url = "https://files.pythonhosted.org/packages/4c/00/e17e2a8df0ff5aca2edd9eeebd93e095dd2515f2dd8d591d84a3233518f6/cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {url = "https://files.pythonhosted.org/packages/50/bd/17a8f9ac569d328de304e7318d7707fcdb6f028bcc194d80cfc654902007/cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {url = "https://files.pythonhosted.org/packages/54/49/b8875986beef2e74fc668b95f2df010e354f78e009d33d95b375912810c3/cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {url = "https://files.pythonhosted.org/packages/57/3a/c263cf4d5b02880274866968fa2bf196a02c4486248bc164732319b4a4c0/cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {url = "https://files.pythonhosted.org/packages/58/ac/2a3ea436a6cbaa8f75ddcab39010e5e0817a18f26fef5d2fe2e0c7df3425/cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {url = "https://files.pythonhosted.org/packages/5a/c7/694814b3757878b29da39bc2f0cf9d20295f4c1e0a0bde7971708d5f23f8/cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {url = "https://files.pythonhosted.org/packages/68/ce/95b0bae7968c65473e1298efb042e10cafc7bafc14d9e4f154008241c91d/cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, - {url = "https://files.pythonhosted.org/packages/69/46/8882b0405be4ac7db3fefa5a201f221acb54f27c76e584e23e9c62b68819/cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {url = "https://files.pythonhosted.org/packages/73/dd/15c6f32166f0c8f97d8aadee9ac8f096557899f4f21448d2feb74cf4f210/cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {url = "https://files.pythonhosted.org/packages/7f/5a/39e212f99aa73660a1c523f6b7ddeb4e26f906faaa5088e97b617a89c7ae/cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {url = "https://files.pythonhosted.org/packages/85/3e/a4e4857c2aae635195459679ac9daea296630c1d76351259eb3de3c18ed0/cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {url = "https://files.pythonhosted.org/packages/8b/5c/7f9cd1fb80512c9e16c90b29b26fea52977e9ab268321f64b42f4c8488a3/cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {url = "https://files.pythonhosted.org/packages/8c/54/82aa3c014760d5a6ddfde3253602f0ac1937dd504621d4139746f230a7b5/cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {url = "https://files.pythonhosted.org/packages/95/c8/ce05a6cba2bec12d4b28285e66c53cc88dd7385b102dea7231da3b74cfef/cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {url = "https://files.pythonhosted.org/packages/9b/1a/575200306a3dfd9102ce573e7158d459a1bd7e44637e4f22a999c4fd64b1/cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {url = "https://files.pythonhosted.org/packages/9b/89/a31c81e36bbb793581d8bba4406a8aac4ba84b2559301c44eef81f4cf5df/cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {url = "https://files.pythonhosted.org/packages/9d/da/e6dbf22b66899419e66c501ae5f1cf3d69979d4c75ad30da683f60abba94/cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {url = "https://files.pythonhosted.org/packages/a3/81/5f5d61338951afa82ce4f0f777518708893b9420a8b309cc037fbf114e63/cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {url = "https://files.pythonhosted.org/packages/aa/aa/1c43e48a6f361d1529f9e4602d6992659a0107b5f21cae567e2eddcf8d66/cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {url = "https://files.pythonhosted.org/packages/ae/00/831d01e63288d1654ed3084a6ac8b0940de6dc0ada4ba71b830fff7a0088/cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {url = "https://files.pythonhosted.org/packages/b4/5f/c6e7e8d80fbf727909e4b1b5b9352082fc1604a14991b1d536bfaee5a36c/cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {url = "https://files.pythonhosted.org/packages/b4/f6/b28d2bfb5fca9e8f9afc9d05eae245bed9f6ba5c2897fefee7a9abeaf091/cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {url = "https://files.pythonhosted.org/packages/b5/23/ea84dd4985649fcc179ba3a6c9390412e924d20b0244dc71a6545788f5a2/cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {url = "https://files.pythonhosted.org/packages/be/3e/0b197d1bfbf386a90786b251dbf2634a15f2ea3d4e4070e99c7d1c7689cf/cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {url = "https://files.pythonhosted.org/packages/c4/01/f5116266fe80c04d4d1cc96c3d355606943f9fb604a810e0b02228a0ce19/cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {url = "https://files.pythonhosted.org/packages/c9/6e/751437067affe7ac0944b1ad4856ec11650da77f0dd8f305fae1117ef7bb/cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {url = "https://files.pythonhosted.org/packages/c9/7c/43d81bdd5a915923c3bad5bb4bff401ea00ccc8e28433fb6083d2e3bf58e/cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {url = "https://files.pythonhosted.org/packages/e0/80/52b71420d68c4be18873318f6735c742f1172bb3b18d23f0306e6444d410/cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {url = "https://files.pythonhosted.org/packages/e4/9a/7169ae3a67a7bb9caeb2249f0617ac1458df118305c53afa3dec4a9029cd/cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {url = "https://files.pythonhosted.org/packages/e4/c7/c09cc6fd1828ea950e60d44e0ef5ed0b7e3396fbfb856e49ca7d629b1408/cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {url = "https://files.pythonhosted.org/packages/e9/63/e285470a4880a4f36edabe4810057bd4b562c6ddcc165eacf9c3c7210b40/cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {url = "https://files.pythonhosted.org/packages/ea/ac/e9e77bc385729035143e54cc8c4785bd480eaca9df17565963556b0b7a93/cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {url = "https://files.pythonhosted.org/packages/eb/de/4f644fc78a1144a897e1f908abfb2058f7be05a8e8e4fe90b7f41e9de36b/cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {url = "https://files.pythonhosted.org/packages/ee/68/74a2b9f9432b70d97d1184cdabf32d7803124c228adef9481d280864a4a7/cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {url = "https://files.pythonhosted.org/packages/f0/31/a6503a5c4874fb4d4c2053f73f09a957cb427b6943fab5a43b8e156df397/cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {url = "https://files.pythonhosted.org/packages/f1/c9/326611aa83e16b13b6db4dbb73b5455c668159a003c4c2f0c3bcb2ddabaf/cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {url = "https://files.pythonhosted.org/packages/f9/6c/af5f40c66aac38aa70abfa6f26e8296947a79ef373cb81a14c791c3da91d/cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, -] -"charset-normalizer 3.3.2" = [ - {url = "https://files.pythonhosted.org/packages/05/31/e1f51c76db7be1d4aef220d29fbfa5dbb4a99165d9833dcbf166753b6dc0/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {url = "https://files.pythonhosted.org/packages/05/8c/eb854996d5fef5e4f33ad56927ad053d04dc820e4a3d39023f35cad72617/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {url = "https://files.pythonhosted.org/packages/07/07/7e554f2bbce3295e191f7e653ff15d55309a9ca40d0362fcdab36f01063c/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {url = "https://files.pythonhosted.org/packages/13/82/83c188028b6f38d39538442dd127dc794c602ae6d45d66c469f4063a4c30/charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {url = "https://files.pythonhosted.org/packages/13/f8/eefae0629fa9260f83b826ee3363e311bb03cfdd518dad1bd10d57cb2d84/charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {url = "https://files.pythonhosted.org/packages/16/ea/a9e284aa38cccea06b7056d4cbc7adf37670b1f8a668a312864abf1ff7c6/charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {url = "https://files.pythonhosted.org/packages/19/28/573147271fd041d351b438a5665be8223f1dd92f273713cb882ddafe214c/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {url = "https://files.pythonhosted.org/packages/1e/49/7ab74d4ac537ece3bc3334ee08645e231f39f7d6df6347b29a74b0537103/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {url = "https://files.pythonhosted.org/packages/1f/8d/33c860a7032da5b93382cbe2873261f81467e7b37f4ed91e25fed62fd49b/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {url = "https://files.pythonhosted.org/packages/24/9d/2e3ef673dfd5be0154b20363c5cdcc5606f35666544381bee15af3778239/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {url = "https://files.pythonhosted.org/packages/28/76/e6222113b83e3622caa4bb41032d0b1bf785250607392e1b778aca0b8a7d/charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, - {url = "https://files.pythonhosted.org/packages/2a/9d/a6d15bd1e3e2914af5955c8eb15f4071997e7078419328fee93dfd497eb7/charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {url = "https://files.pythonhosted.org/packages/2b/61/095a0aa1a84d1481998b534177c8566fdc50bb1233ea9a0478cd3cc075bd/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {url = "https://files.pythonhosted.org/packages/2d/dc/9dacba68c9ac0ae781d40e1a0c0058e26302ea0660e574ddf6797a0347f7/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {url = "https://files.pythonhosted.org/packages/2e/37/9223632af0872c86d8b851787f0edd3fe66be4a5378f51242b25212f8374/charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {url = "https://files.pythonhosted.org/packages/2e/7d/2259318c202f3d17f3fe6438149b3b9e706d1070fe3fcbb28049730bb25c/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {url = "https://files.pythonhosted.org/packages/2f/0e/d7303ccae9735ff8ff01e36705ad6233ad2002962e8668a970fc000c5e1b/charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {url = "https://files.pythonhosted.org/packages/33/95/ef68482e4a6adf781fae8d183fb48d6f2be8facb414f49c90ba6a5149cd1/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {url = "https://files.pythonhosted.org/packages/33/c3/3b96a435c5109dd5b6adc8a59ba1d678b302a97938f032e3770cc84cd354/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {url = "https://files.pythonhosted.org/packages/34/2a/f392457d45e24a0c9bfc012887ed4f3c54bf5d4d05a5deb970ffec4b7fc0/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {url = "https://files.pythonhosted.org/packages/3a/52/9f9d17c3b54dc238de384c4cb5a2ef0e27985b42a0e5cc8e8a31d918d48d/charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {url = "https://files.pythonhosted.org/packages/3d/09/d82fe4a34c5f0585f9ea1df090e2a71eb9bb1e469723053e1ee9f57c16f3/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {url = "https://files.pythonhosted.org/packages/3d/85/5b7416b349609d20611a64718bed383b9251b5a601044550f0c8983b8900/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {url = "https://files.pythonhosted.org/packages/3e/33/21a875a61057165e92227466e54ee076b73af1e21fe1b31f1e292251aa1e/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {url = "https://files.pythonhosted.org/packages/3f/ba/3f5e7be00b215fa10e13d64b1f6237eb6ebea66676a41b2bcdd09fe74323/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {url = "https://files.pythonhosted.org/packages/40/26/f35951c45070edc957ba40a5b1db3cf60a9dbb1b350c2d5bef03e01e61de/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {url = "https://files.pythonhosted.org/packages/43/05/3bf613e719efe68fb3a77f9c536a389f35b95d75424b96b426a47a45ef1d/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {url = "https://files.pythonhosted.org/packages/44/80/b339237b4ce635b4af1c73742459eee5f97201bd92b2371c53e11958392e/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {url = "https://files.pythonhosted.org/packages/45/59/3d27019d3b447a88fe7e7d004a1e04be220227760264cc41b405e863891b/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {url = "https://files.pythonhosted.org/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {url = "https://files.pythonhosted.org/packages/4f/d1/d547cc26acdb0cc458b152f79b2679d7422f29d41581e6fa907861e88af1/charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {url = "https://files.pythonhosted.org/packages/51/fd/0ee5b1c2860bb3c60236d05b6e4ac240cf702b67471138571dad91bcfed8/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {url = "https://files.pythonhosted.org/packages/53/cd/aa4b8a4d82eeceb872f83237b2d27e43e637cac9ffaef19a1321c3bafb67/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {url = "https://files.pythonhosted.org/packages/54/7f/cad0b328759630814fcf9d804bfabaf47776816ad4ef2e9938b7e1123d04/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {url = "https://files.pythonhosted.org/packages/57/ec/80c8d48ac8b1741d5b963797b7c0c869335619e13d4744ca2f67fc11c6fc/charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {url = "https://files.pythonhosted.org/packages/58/78/a0bc646900994df12e07b4ae5c713f2b3e5998f58b9d3720cce2aa45652f/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {url = "https://files.pythonhosted.org/packages/58/a2/0c63d5d7ffac3104b86631b7f2690058c97bf72d3145c0a9cd4fb90c58c2/charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {url = "https://files.pythonhosted.org/packages/5b/ae/ce2c12fcac59cb3860b2e2d76dc405253a4475436b1861d95fe75bdea520/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {url = "https://files.pythonhosted.org/packages/63/09/c1bc53dab74b1816a00d8d030de5bf98f724c52c1635e07681d312f20be8/charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {url = "https://files.pythonhosted.org/packages/66/fe/c7d3da40a66a6bf2920cce0f436fa1f62ee28aaf92f412f0bf3b84c8ad6c/charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {url = "https://files.pythonhosted.org/packages/68/77/02839016f6fbbf808e8b38601df6e0e66c17bbab76dff4613f7511413597/charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {url = "https://files.pythonhosted.org/packages/6c/c2/4a583f800c0708dd22096298e49f887b49d9746d0e78bfc1d7e29816614c/charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {url = "https://files.pythonhosted.org/packages/72/1a/641d5c9f59e6af4c7b53da463d07600a695b9824e20849cb6eea8a627761/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {url = "https://files.pythonhosted.org/packages/74/20/8923a06f15eb3d7f6a306729360bd58f9ead1dc39bc7ea8831f4b407e4ae/charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {url = "https://files.pythonhosted.org/packages/74/f1/0d9fe69ac441467b737ba7f48c68241487df2f4522dd7246d9426e7c690e/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {url = "https://files.pythonhosted.org/packages/79/66/8946baa705c588521afe10b2d7967300e49380ded089a62d38537264aece/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {url = "https://files.pythonhosted.org/packages/7b/ef/5eb105530b4da8ae37d506ccfa25057961b7b63d581def6f99165ea89c7e/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {url = "https://files.pythonhosted.org/packages/81/b2/160893421adfa3c45554fb418e321ed342bb10c0a4549e855b2b2a3699cb/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {url = "https://files.pythonhosted.org/packages/8d/b7/9e95102e9a8cce6654b85770794b582dda2921ec1fd924c10fbcf215ad31/charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {url = "https://files.pythonhosted.org/packages/91/33/749df346e93d7a30cdcb90cbfdd41a06026317bfbfb62cd68307c1a3c543/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {url = "https://files.pythonhosted.org/packages/91/95/e2cfa7ce962e6c4b59a44a6e19e541c3a0317e543f0e0923f844e8d7d21d/charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {url = "https://files.pythonhosted.org/packages/96/fc/0cae31c0f150cd1205a2a208079de865f69a8fd052a98856c40c99e36b3c/charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {url = "https://files.pythonhosted.org/packages/98/69/5d8751b4b670d623aa7a47bef061d69c279e9f922f6705147983aa76c3ce/charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {url = "https://files.pythonhosted.org/packages/99/b0/9c365f6d79a9f0f3c379ddb40a256a67aa69c59609608fe7feb6235896e1/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {url = "https://files.pythonhosted.org/packages/9e/ef/cd47a63d3200b232792e361cd67530173a09eb011813478b1c0fb8aa7226/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {url = "https://files.pythonhosted.org/packages/a0/b1/4e72ef73d68ebdd4748f2df97130e8428c4625785f2b6ece31f555590c2d/charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {url = "https://files.pythonhosted.org/packages/a2/51/e5023f937d7f307c948ed3e5c29c4b7a3e42ed2ee0b8cdf8f3a706089bf0/charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {url = "https://files.pythonhosted.org/packages/a2/a0/4af29e22cb5942488cf45630cbdd7cefd908768e69bdd90280842e4e8529/charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {url = "https://files.pythonhosted.org/packages/a8/31/47d018ef89f95b8aded95c589a77c072c55e94b50a41aa99c0a2008a45a4/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {url = "https://files.pythonhosted.org/packages/a8/6f/4ff299b97da2ed6358154b6eb3a2db67da2ae204e53d205aacb18a7e4f34/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {url = "https://files.pythonhosted.org/packages/ae/d5/4fecf1d58bedb1340a50f165ba1c7ddc0400252d6832ff619c4568b36cc0/charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {url = "https://files.pythonhosted.org/packages/b2/62/5a5dcb9a71390a9511a253bde19c9c89e0b20118e41080185ea69fb2c209/charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {url = "https://files.pythonhosted.org/packages/b3/c1/ebca8e87c714a6a561cfee063f0655f742e54b8ae6e78151f60ba8708b3a/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {url = "https://files.pythonhosted.org/packages/b6/7c/8debebb4f90174074b827c63242c23851bdf00a532489fba57fef3416e40/charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {url = "https://files.pythonhosted.org/packages/b8/60/e2f67915a51be59d4539ed189eb0a2b0d292bf79270410746becb32bc2c3/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {url = "https://files.pythonhosted.org/packages/bd/28/7ea29e73eea52c7e15b4b9108d0743fc9e4cc2cdb00d275af1df3d46d360/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {url = "https://files.pythonhosted.org/packages/be/4d/9e370f8281cec2fcc9452c4d1ac513324c32957c5f70c73dd2fa8442a21a/charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {url = "https://files.pythonhosted.org/packages/c1/9d/254a2f1bcb0ce9acad838e94ed05ba71a7cb1e27affaa4d9e1ca3958cdb6/charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {url = "https://files.pythonhosted.org/packages/c2/65/52aaf47b3dd616c11a19b1052ce7fa6321250a7a0b975f48d8c366733b9f/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {url = "https://files.pythonhosted.org/packages/c8/ce/09d6845504246d95c7443b8c17d0d3911ec5fdc838c3213e16c5e47dee44/charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {url = "https://files.pythonhosted.org/packages/c9/7a/6d8767fac16f2c80c7fa9f14e0f53d4638271635c306921844dc0b5fd8a6/charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {url = "https://files.pythonhosted.org/packages/cc/94/f7cf5e5134175de79ad2059edf2adce18e0685ebdb9227ff0139975d0e93/charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {url = "https://files.pythonhosted.org/packages/cf/7c/f3b682fa053cc21373c9a839e6beba7705857075686a05c72e0f8c4980ca/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {url = "https://files.pythonhosted.org/packages/d1/2f/0d1efd07c74c52b6886c32a3b906fb8afd2fecf448650e73ecb90a5a27f1/charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {url = "https://files.pythonhosted.org/packages/d1/b2/fcedc8255ec42afee97f9e6f0145c734bbe104aac28300214593eb326f1d/charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {url = "https://files.pythonhosted.org/packages/d8/b5/eb705c313100defa57da79277d9207dc8d8e45931035862fa64b625bfead/charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {url = "https://files.pythonhosted.org/packages/da/f1/3702ba2a7470666a62fd81c58a4c40be00670e5006a67f4d626e57f013ae/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {url = "https://files.pythonhosted.org/packages/db/fb/d29e343e7c57bbf1231275939f6e75eb740cd47a9d7cb2c52ffeb62ef869/charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {url = "https://files.pythonhosted.org/packages/dd/51/68b61b90b24ca35495956b718f35a9756ef7d3dd4b3c1508056fa98d1a1b/charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {url = "https://files.pythonhosted.org/packages/df/3e/a06b18788ca2eb6695c9b22325b6fde7dde0f1d1838b1792a0076f58fe9d/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {url = "https://files.pythonhosted.org/packages/e1/9c/60729bf15dc82e3aaf5f71e81686e42e50715a1399770bcde1a9e43d09db/charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {url = "https://files.pythonhosted.org/packages/e4/a6/7ee57823d46331ddc37dd00749c95b0edec2c79b15fc0d6e6efb532e89ac/charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {url = "https://files.pythonhosted.org/packages/eb/5c/97d97248af4920bc68687d9c3b3c0f47c910e21a8ff80af4565a576bd2f0/charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {url = "https://files.pythonhosted.org/packages/ed/3a/a448bf035dce5da359daf9ae8a16b8a39623cc395a2ffb1620aa1bce62b0/charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {url = "https://files.pythonhosted.org/packages/ee/fb/14d30eb4956408ee3ae09ad34299131fb383c47df355ddb428a7331cfa1e/charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {url = "https://files.pythonhosted.org/packages/ef/d4/a1d72a8f6aa754fdebe91b848912025d30ab7dced61e9ed8aabbf791ed65/charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {url = "https://files.pythonhosted.org/packages/f2/0e/e06bc07ef4673e4d24dc461333c254586bb759fdd075031539bab6514d07/charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {url = "https://files.pythonhosted.org/packages/f6/93/bb6cbeec3bf9da9b2eba458c15966658d1daa8b982c642f81c93ad9b40e1/charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {url = "https://files.pythonhosted.org/packages/f6/d3/bfc699ab2c4f9245867060744e8136d359412ff1e5ad93be38a46d160f9d/charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {url = "https://files.pythonhosted.org/packages/f7/9d/bcf4a449a438ed6f19790eee543a86a740c77508fbc5ddab210ab3ba3a9a/charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, -] -"click 8.1.7" = [ - {url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] -"colorama 0.4.6" = [ - {url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -"databases 0.9.0" = [ - {url = "https://files.pythonhosted.org/packages/76/de/ea55722907bd1b2389b01e362faa3c66a09d5a8463c13d44c80da7b32497/databases-0.9.0.tar.gz", hash = "sha256:d2f259677609bf187737644c95fa41701072e995dfeb8d2882f335795c5b61b0"}, - {url = "https://files.pythonhosted.org/packages/d5/43/6035922af5471f1a196851831a1d5f403447401b395f474bf673efa8959f/databases-0.9.0-py3-none-any.whl", hash = "sha256:9ee657c9863b34f8d3a06c06eafbe1bda68af2a434b56996312edf1f1c0b6297"}, -] -"decorator 5.1.1" = [ - {url = "https://files.pythonhosted.org/packages/66/0c/8d907af351aa16b42caae42f9d6aa37b900c67308052d10fdce809f8d952/decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, - {url = "https://files.pythonhosted.org/packages/d5/50/83c593b07763e1161326b3b8c6686f0f4b0f24d5526546bee538c89837d6/decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, -] -"dnspython 2.6.1" = [ - {url = "https://files.pythonhosted.org/packages/37/7d/c871f55054e403fdfd6b8f65fd6d1c4e147ed100d3e9f9ba1fe695403939/dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, - {url = "https://files.pythonhosted.org/packages/87/a1/8c5287991ddb8d3e4662f71356d9656d91ab3a36618c3dd11b280df0d255/dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, -] -"drone-flightplan 0.2.1" = [ - {url = "https://files.pythonhosted.org/packages/31/ae/75dd31b060ae999737c982fb0f2928e3fc08b2d1e71be57f1124c7d9214b/drone_flightplan-0.2.1-py3-none-any.whl", hash = "sha256:e5257eb43d706fe1d44a08a75476ed511d1bd5f5bc27d4c5129158f45d504a4d"}, - {url = "https://files.pythonhosted.org/packages/43/b2/7d5ef0e3b9744d545ef940e8db63ae7654dd4d2e88c6daef38cf75b79f50/drone_flightplan-0.2.1.tar.gz", hash = "sha256:e937961a5ac226603d374fe7b651cc7ebcb931dc35530ea8180b73fc77ac4a14"}, -] -"email-validator 2.2.0" = [ - {url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, - {url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, -] -"executing 2.0.1" = [ - {url = "https://files.pythonhosted.org/packages/08/41/85d2d28466fca93737592b7f3cc456d1cfd6bcd401beceeba17e8e792b50/executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, - {url = "https://files.pythonhosted.org/packages/80/03/6ea8b1b2a5ab40a7a60dc464d3daa7aa546e0a74d74a9f8ff551ea7905db/executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, -] -"fastapi 0.104.1" = [ - {url = "https://files.pythonhosted.org/packages/d9/d8/002e0ba7cf848a981b3ee92aaf5aa396c5700b0d7dec5d060031432a87d8/fastapi-0.104.1.tar.gz", hash = "sha256:e5e4540a7c5e1dcfbbcf5b903c234feddcdcd881f191977a1c5dfd917487e7ae"}, - {url = "https://files.pythonhosted.org/packages/f3/4f/0ce34195b63240b6693086496c9bab4ef23999112184399a3e88854c7674/fastapi-0.104.1-py3-none-any.whl", hash = "sha256:752dc31160cdbd0436bb93bad51560b57e525cbb1d4bbf6f4904ceee75548241"}, -] -"flatdict 4.0.1" = [ - {url = "https://files.pythonhosted.org/packages/3e/0d/424de6e5612f1399ff69bf86500d6a62ff0a4843979701ae97f120c7f1fe/flatdict-4.0.1.tar.gz", hash = "sha256:cd32f08fd31ed21eb09ebc76f06b6bd12046a24f77beb1fd0281917e47f26742"}, -] -"fmtm-splitter 1.2.2" = [ - {url = "https://files.pythonhosted.org/packages/10/ec/6aaceba5fa9fefd546fccb720d2b38f24a1e368f23e2a7510b42d7505c16/fmtm_splitter-1.2.2-py3-none-any.whl", hash = "sha256:bbef78cf0e1f2b67f8c8aeaadb7fd2927bfd333d216927059a12abbbb04a5742"}, - {url = "https://files.pythonhosted.org/packages/de/0c/e7036f581f744eff8834402c46250b5ff72a98b47ca2ec4637c063cf18b1/fmtm-splitter-1.2.2.tar.gz", hash = "sha256:9384dbf00c0e53e24e1f13046ae6693e13567ff3dc0f59f29f4a96ac4a54105e"}, -] -"gdal 3.6.2" = [ - {url = "https://files.pythonhosted.org/packages/49/4f/174743caf64d1999c46ab2ee72b2cb0d77a47bd7ed04f954f863e35a25fd/GDAL-3.6.2.tar.gz", hash = "sha256:a167cde1813707d91a938dad1a22f280f5ad28c45980d42e948fb8c59f890f5a"}, -] -"geoalchemy2 0.14.2" = [ - {url = "https://files.pythonhosted.org/packages/23/41/5011f934e34cc0995fdff6ade94fd897177a0119ba439eab7f49e9ac32b3/GeoAlchemy2-0.14.2.tar.gz", hash = "sha256:8ca023dcb9a36c6d312f3b4aee631d66385264e2fc9feb0ab0f446eb5609407d"}, - {url = "https://files.pythonhosted.org/packages/a6/a0/6a1c96ab4f95fb3573ac47f370404a78497509e5883420e9cb09e31a589b/GeoAlchemy2-0.14.2-py3-none-any.whl", hash = "sha256:ca81c2d924c0724458102bac93f68f3e3c337a65fcb811af5e504ce7c5d56ac2"}, -] -"geojson 3.1.0" = [ - {url = "https://files.pythonhosted.org/packages/0f/34/0ea653dec93d3a360856e629a897a1d3ab534f2952852bb59d55853055ed/geojson-3.1.0.tar.gz", hash = "sha256:58a7fa40727ea058efc28b0e9ff0099eadf6d0965e04690830208d3ef571adac"}, - {url = "https://files.pythonhosted.org/packages/8e/1b/4f57660aa148d3e3043d048b7e1ab87dfeb85204d0fdb5b4e19c08202162/geojson-3.1.0-py3-none-any.whl", hash = "sha256:68a9771827237adb8c0c71f8527509c8f5bef61733aa434cefc9c9d4f0ebe8f3"}, -] -"geojson-pydantic 1.0.1" = [ - {url = "https://files.pythonhosted.org/packages/5f/f9/9ff9a0fb046deae25d515385ab8e7f6056f6f2d9040572b85ede03a83f27/geojson_pydantic-1.0.1-py3-none-any.whl", hash = "sha256:da8c15f15a0a9fc3e0af0253f0c2bb8a948f95ece9a0356f43d4738fa2be5107"}, - {url = "https://files.pythonhosted.org/packages/74/c8/aabedb6c2bb3941cbba57557c7bf4cafe148ce77138d7d09032ab9f97205/geojson_pydantic-1.0.1.tar.gz", hash = "sha256:a996ffccd5a016d3acb4a0c6aac941d2c569e3c6163d5ce6a04b61ee131c8f94"}, -] -"greenlet 3.0.3" = [ - {url = "https://files.pythonhosted.org/packages/0b/8a/f5140c8713f919af0e98e6aaa40cb20edaaf3739d18c4a077581e2422ac4/greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, - {url = "https://files.pythonhosted.org/packages/13/af/8db0d63147c6362447eb49da60573b41aee5cf5864fe1e27bdbaf7060bd2/greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, - {url = "https://files.pythonhosted.org/packages/14/93/da5e3da0d4f5d7d2613e9a5d5bcb2d9d0a4af1cf71ac8768661a3238dff8/greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, - {url = "https://files.pythonhosted.org/packages/17/14/3bddb1298b9a6786539ac609ba4b7c9c0842e12aa73aaa4d8d73ec8f8185/greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, - {url = "https://files.pythonhosted.org/packages/19/76/1f33deb0161a439292a6d25fe9b44712c427ce3ecae74f61e4c003895e49/greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, - {url = "https://files.pythonhosted.org/packages/1c/2f/64628f6ae48e05f585e0eb3fb7399b52e240ef99f602107b445bf6be23ef/greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, - {url = "https://files.pythonhosted.org/packages/1c/fa/bd5ee0772c7bbcb99bbacdb5608895052349b0ab9f20962c0c81bf6bd41d/greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, - {url = "https://files.pythonhosted.org/packages/20/70/2f99bdcb4e3912d844dee279e077ee670ec43161d96670a9dfad16b89dd1/greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, - {url = "https://files.pythonhosted.org/packages/21/b4/90e06e07c78513ab03855768200bdb35c8e764e805b3f14fb488e56f82dc/greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, - {url = "https://files.pythonhosted.org/packages/24/35/945d5b10648fec9b20bcc6df8952d20bb3bba76413cd71c1fdbee98f5616/greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, - {url = "https://files.pythonhosted.org/packages/25/3b/6d6c5e475aa4d92832cd69c306513f1774f404266c2c9e3e7b225a87d384/greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, - {url = "https://files.pythonhosted.org/packages/27/94/a4c51f047f9ae9a3a2127985d35100afeca420f53897fdaa7cf01696a8d8/greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, - {url = "https://files.pythonhosted.org/packages/38/77/efb21ab402651896c74f24a172eb4d7479f9f53898bd5e56b9e20bb24ffd/greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, - {url = "https://files.pythonhosted.org/packages/3a/0d/11f039576f5b4b59b51f9517388a1597f4cc9ec754bde695374044d2288e/greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, - {url = "https://files.pythonhosted.org/packages/3d/4a/c9590b31bfefe089d8fae72201c77761a63c1685c7f511a692a267d7f25e/greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, - {url = "https://files.pythonhosted.org/packages/3d/f6/310a4cd1ffd5484cc922241d928777791113fac19277ee99e7bd6bf2140b/greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, - {url = "https://files.pythonhosted.org/packages/42/11/42ad6b1104c357826bbee7d7b9e4f24dbd9fde94899a03efb004aab62963/greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, - {url = "https://files.pythonhosted.org/packages/47/79/26d54d7d700ef65b689fc2665a40846d13e834da0486674a8d4f0f371a47/greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, - {url = "https://files.pythonhosted.org/packages/53/80/3d94d5999b4179d91bcc93745d1b0815b073d61be79dd546b840d17adb18/greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, - {url = "https://files.pythonhosted.org/packages/54/4b/965a542baf157f23912e466b50fa9c49dd66132d9495d201e6c607ea16f2/greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, - {url = "https://files.pythonhosted.org/packages/5f/71/db617b97026a5df444d2e953db163339cb9ca046999917e99f2adc3e581a/greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, - {url = "https://files.pythonhosted.org/packages/63/0f/847ed02cdfce10f0e6e3425cd054296bddb11a17ef1b34681fa01a055187/greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, - {url = "https://files.pythonhosted.org/packages/69/73/7034a57ccc914f6cdc75c55950e4341132a2ed6189f599e2af8e1928285e/greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, - {url = "https://files.pythonhosted.org/packages/6c/90/5b14670653f7363fb3e1665f8da6d64bd4c31d53a796d09ef69f48be7273/greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, - {url = "https://files.pythonhosted.org/packages/6e/20/68a278a6f93fa36e21cfc3d7599399a8a831225644eb3b6b18755cd3d6fc/greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, - {url = "https://files.pythonhosted.org/packages/74/00/27e2da76b926e9b5a2c97d3f4c0baf1b7d8181209d3026c0171f621ae6c0/greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, - {url = "https://files.pythonhosted.org/packages/74/3a/92f188ace0190f0066dca3636cf1b09481d0854c46e92ec5e29c7cefe5b1/greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, - {url = "https://files.pythonhosted.org/packages/74/82/9737e7dee4ccb9e1be2a8f17cf760458be2c36c6ff7bbaef55cbe279e729/greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, - {url = "https://files.pythonhosted.org/packages/74/9f/71df0154a13d77e92451891a087a4c5783375964132290fca70c7e80e5d4/greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, - {url = "https://files.pythonhosted.org/packages/7c/68/b5f4084c0a252d7e9c0d95fc1cfc845d08622037adb74e05be3a49831186/greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, - {url = "https://files.pythonhosted.org/packages/8a/74/498377804f8ebfb1efdfbe33e93cf3b29d77e207e9496f0c10912d5055b4/greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, - {url = "https://files.pythonhosted.org/packages/8d/73/9e934f07505ed8e1fed5cfcd99cc7db03fe8eb645dbb24e4ba97af41bc3c/greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, - {url = "https://files.pythonhosted.org/packages/94/ed/1e5f4bca691a81700e5a88e86d6f0e538acb10188cd2cc17140e523255ef/greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, - {url = "https://files.pythonhosted.org/packages/9d/ea/8bc7ed08ba274bdaff08f2cb546d832b8f44af267e03ca6e449840486915/greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, - {url = "https://files.pythonhosted.org/packages/a2/2f/461615adc53ba81e99471303b15ac6b2a6daa8d2a0f7f77fd15605e16d5b/greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, - {url = "https://files.pythonhosted.org/packages/a2/92/f11dbbcf33809421447b24d2eefee0575c59c8569d6d03f7ca4d2b34d56f/greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, - {url = "https://files.pythonhosted.org/packages/a4/fa/31e22345518adcd69d1d6ab5087a12c178aa7f3c51103f6d5d702199d243/greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, - {url = "https://files.pythonhosted.org/packages/a6/64/bea53c592e3e45799f7c8039a8ee7d6883c518eafef1fcae60beb776070f/greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, - {url = "https://files.pythonhosted.org/packages/a6/76/e1ee9f290bb0d46b09704c2fb0e609cae329eb308ad404c0ee6fa1ecb8a5/greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, - {url = "https://files.pythonhosted.org/packages/a6/d6/408ad9603339db28ce334021b1403dfcfbcb7501a435d49698408d928de7/greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, - {url = "https://files.pythonhosted.org/packages/af/05/b7e068070a6c143f34dfcd7e9144684271b8067e310f6da68269580db1d8/greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, - {url = "https://files.pythonhosted.org/packages/bb/6b/384dee7e0121cbd1757bdc1824a5ee28e43d8d4e3f99aa59521f629442fe/greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, - {url = "https://files.pythonhosted.org/packages/bd/37/56b0da468a85e7704f3b2bc045015301bdf4be2184a44868c71f6dca6fe2/greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, - {url = "https://files.pythonhosted.org/packages/c3/80/01ff837bc7122d049971960123d749ed16adbd43cbc008afdb780a40e3fa/greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, - {url = "https://files.pythonhosted.org/packages/c6/1f/12d5a6cc26e8b483c2e7975f9c22e088ac735c0d8dcb8a8f72d31a4e5f04/greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, - {url = "https://files.pythonhosted.org/packages/c7/ec/85b647e59e0f137c7792a809156f413e38379cf7f3f2e1353c37f4be4026/greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, - {url = "https://files.pythonhosted.org/packages/cf/5b/2de4a398840d3b4d99c4a3476cda0d82badfa349f3f89846ada2e32e9500/greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, - {url = "https://files.pythonhosted.org/packages/d9/84/3d9f0255ae3681010d9eee9f4d1bd4790e41c87dcbdad5cbf893605039b5/greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, - {url = "https://files.pythonhosted.org/packages/dc/c3/06ca5f34b01af6d6e2fd2f97c0ad3673123a442bf4a3add548d374b1cc7c/greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, - {url = "https://files.pythonhosted.org/packages/e1/65/506e0a80931170b0dac1a03d36b7fc299f3fa3576235b916718602fff2c3/greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, - {url = "https://files.pythonhosted.org/packages/e8/47/0fd13f50da7e43e313cce276c9ec9b5f862a8fedacdc30e7ca2a43ee7fd7/greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, - {url = "https://files.pythonhosted.org/packages/e9/55/2c3cfa3cdbb940cf7321fbcf544f0e9c74898eed43bf678abf416812d132/greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, - {url = "https://files.pythonhosted.org/packages/ef/17/e8e72cabfb5a906c0d976d7fbcc88310df292beea0f816efbefdaf694284/greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, - {url = "https://files.pythonhosted.org/packages/f4/b0/03142c8f64a2bc2512aaab6c636f73d30315da40b1e95387557b0ea31805/greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, - {url = "https://files.pythonhosted.org/packages/f6/a2/0ed21078039072f9dc738bbf3af12b103a84106b1385ac4723841f846ce7/greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, - {url = "https://files.pythonhosted.org/packages/fb/e6/d6db6e75d8a04eac3d18a8570213851bbd2a859cb4f114b637a9bf542f1b/greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, - {url = "https://files.pythonhosted.org/packages/fe/1f/b5cd033b55f347008235244626bb1ee2854adf9c3cb97ff406d98d6e1ea3/greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, - {url = "https://files.pythonhosted.org/packages/ff/76/0893f4fe7b841660a5d75116c7d755c58652a4e9e12f6a72984eaa396881/greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, -] -"h11 0.14.0" = [ - {url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] -"idna 3.7" = [ - {url = "https://files.pythonhosted.org/packages/21/ed/f86a79a07470cb07819390452f178b3bef1d375f2ec021ecfc709fc7cf07/idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, - {url = "https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, -] -"ipython 8.14.0" = [ - {url = "https://files.pythonhosted.org/packages/52/d1/f70cdafba20030cbc1412d7a7d6a89c5035071835cc50e47fc5ed8da553c/ipython-8.14.0-py3-none-any.whl", hash = "sha256:248aca623f5c99a6635bc3857677b7320b9b8039f99f070ee0d20a5ca5a8e6bf"}, - {url = "https://files.pythonhosted.org/packages/fa/cb/2b777f625cca49b4a747b0dfe9986c21f5b46e5b548176903a914cdbec55/ipython-8.14.0.tar.gz", hash = "sha256:1d197b907b6ba441b692c48cf2a3a2de280dc0ac91a3405b39349a50272ca0a1"}, -] -"itsdangerous 2.2.0" = [ - {url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, - {url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, -] -"jedi 0.19.1" = [ - {url = "https://files.pythonhosted.org/packages/20/9f/bc63f0f0737ad7a60800bfd472a4836661adae21f9c2535f3957b1e54ceb/jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, - {url = "https://files.pythonhosted.org/packages/d6/99/99b493cec4bf43176b678de30f81ed003fd6a647a301b9c927280c600f0a/jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, -] -"jinja2 3.1.4" = [ - {url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, -] -"loguru 0.7.2" = [ - {url = "https://files.pythonhosted.org/packages/03/0a/4f6fed21aa246c6b49b561ca55facacc2a44b87d65b8b92362a8e99ba202/loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, - {url = "https://files.pythonhosted.org/packages/9e/30/d87a423766b24db416a46e9335b9602b054a72b96a88a241f2b09b560fa8/loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, -] -"mako 1.3.5" = [ - {url = "https://files.pythonhosted.org/packages/03/62/70f5a0c2dd208f9f3f2f9afd103aec42ee4d9ad2401d78342f75e9b8da36/Mako-1.3.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a"}, - {url = "https://files.pythonhosted.org/packages/67/03/fb5ba97ff65ce64f6d35b582aacffc26b693a98053fa831ab43a437cbddb/Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"}, -] -"markupsafe 2.1.5" = [ - {url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {url = "https://files.pythonhosted.org/packages/02/8c/ab9a463301a50dab04d5472e998acbd4080597abc048166ded5c7aa768c8/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {url = "https://files.pythonhosted.org/packages/0b/cc/48206bd61c5b9d0129f4d75243b156929b04c94c09041321456fd06a876d/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {url = "https://files.pythonhosted.org/packages/0e/7d/968284145ffd9d726183ed6237c77938c021abacde4e073020f920e060b2/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {url = "https://files.pythonhosted.org/packages/0f/31/780bb297db036ba7b7bbede5e1d7f1e14d704ad4beb3ce53fb495d22bc62/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {url = "https://files.pythonhosted.org/packages/2f/69/30d29adcf9d1d931c75001dd85001adad7374381c9c2086154d9f6445be6/MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {url = "https://files.pythonhosted.org/packages/3a/03/63498d05bd54278b6ca340099e5b52ffb9cdf2ee4f2d9b98246337e21689/MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {url = "https://files.pythonhosted.org/packages/4a/1d/c4f5016f87ced614eacc7d5fb85b25bcc0ff53e8f058d069fc8cbfdc3c7a/MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {url = "https://files.pythonhosted.org/packages/4c/6f/f2b0f675635b05f6afd5ea03c094557bdb8622fa8e673387444fe8d8e787/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {url = "https://files.pythonhosted.org/packages/4f/14/6f294b9c4f969d0c801a4615e221c1e084722ea6114ab2114189c5b8cbe0/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {url = "https://files.pythonhosted.org/packages/51/e0/393467cf899b34a9d3678e78961c2c8cdf49fb902a959ba54ece01273fb1/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {url = "https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {url = "https://files.pythonhosted.org/packages/68/79/11b4fe15124692f8673b603433e47abca199a08ecd2a4851bfbdc97dc62d/MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {url = "https://files.pythonhosted.org/packages/6a/18/ae5a258e3401f9b8312f92b028c54d7026a97ec3ab20bfaddbdfa7d8cce8/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {url = "https://files.pythonhosted.org/packages/6c/4c/3577a52eea1880538c435176bc85e5b3379b7ab442327ccd82118550758f/MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {url = "https://files.pythonhosted.org/packages/6c/77/d77701bbef72892affe060cdacb7a2ed7fd68dae3b477a8642f15ad3b132/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {url = "https://files.pythonhosted.org/packages/81/d4/fd74714ed30a1dedd0b82427c02fa4deec64f173831ec716da11c51a50aa/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, - {url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {url = "https://files.pythonhosted.org/packages/92/21/357205f03514a49b293e214ac39de01fadd0970a6e05e4bf1ddd0ffd0881/MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {url = "https://files.pythonhosted.org/packages/a7/88/a940e11827ea1c136a34eca862486178294ae841164475b9ab216b80eb8e/MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {url = "https://files.pythonhosted.org/packages/b3/fb/c18b8c9fbe69e347fdbf782c6478f1bc77f19a830588daa224236678339b/MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {url = "https://files.pythonhosted.org/packages/bc/29/9bc18da763496b055d8e98ce476c8e718dcfd78157e17f555ce6dd7d0895/MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {url = "https://files.pythonhosted.org/packages/bf/f3/ecb00fc8ab02b7beae8699f34db9357ae49d9f21d4d3de6f305f34fa949e/MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {url = "https://files.pythonhosted.org/packages/c7/bd/50319665ce81bb10e90d1cf76f9e1aa269ea6f7fa30ab4521f14d122a3df/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {url = "https://files.pythonhosted.org/packages/cb/06/0d28bd178db529c5ac762a625c335a9168a7a23f280b4db9c95e97046145/MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {url = "https://files.pythonhosted.org/packages/d1/06/a41c112ab9ffdeeb5f77bc3e331fdadf97fa65e52e44ba31880f4e7f983c/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {url = "https://files.pythonhosted.org/packages/d9/a7/1e558b4f78454c8a3a0199292d96159eb4d091f983bc35ef258314fe7269/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {url = "https://files.pythonhosted.org/packages/ed/88/408bdbf292eb86f03201c17489acafae8358ba4e120d92358308c15cea7c/MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {url = "https://files.pythonhosted.org/packages/f6/02/5437e2ad33047290dafced9df741d9efc3e716b75583bbd73a9984f1b6f7/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {url = "https://files.pythonhosted.org/packages/f8/ff/2c942a82c35a49df5de3a630ce0a8456ac2969691b230e530ac12314364c/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, -] -"matplotlib-inline 0.1.7" = [ - {url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, - {url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, -] -"minio 7.2.7" = [ - {url = "https://files.pythonhosted.org/packages/51/9a/66fc4e8c861fa4e3029da41569531a56c471abb3c3e08d236115807fb476/minio-7.2.7-py3-none-any.whl", hash = "sha256:59d1f255d852fe7104018db75b3bebbd987e538690e680f7c5de835e422de837"}, - {url = "https://files.pythonhosted.org/packages/ea/96/979d7231fbe2768813cd41675ced868ecbc47c4fb4c926d1c29d557a79e6/minio-7.2.7.tar.gz", hash = "sha256:473d5d53d79f340f3cd632054d0c82d2f93177ce1af2eac34a235bea55708d98"}, -] -"numpy 1.26.4" = [ - {url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, - {url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, - {url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, - {url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, - {url = "https://files.pythonhosted.org/packages/16/ee/9df80b06680aaa23fc6c31211387e0db349e0e36d6a63ba3bd78c5acdf11/numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, - {url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, - {url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, - {url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, - {url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, - {url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, - {url = "https://files.pythonhosted.org/packages/28/7d/4b92e2fe20b214ffca36107f1a3e75ef4c488430e64de2d9af5db3a4637d/numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, - {url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, - {url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, - {url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, - {url = "https://files.pythonhosted.org/packages/3f/72/3df6c1c06fc83d9cfe381cccb4be2532bbd38bf93fbc9fad087b6687f1c0/numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, - {url = "https://files.pythonhosted.org/packages/43/12/01a563fc44c07095996d0129b8899daf89e4742146f7044cdbdb3101c57f/numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, - {url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, - {url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, - {url = "https://files.pythonhosted.org/packages/54/30/c2a907b9443cf42b90c17ad10c1e8fa801975f01cb9764f3f8eb8aea638b/numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, - {url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, - {url = "https://files.pythonhosted.org/packages/6d/64/c3bcdf822269421d85fe0d64ba972003f9bb4aa9a419da64b86856c9961f/numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, - {url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, - {url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, - {url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, - {url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, - {url = "https://files.pythonhosted.org/packages/7d/24/ce71dc08f06534269f66e73c04f5709ee024a1afe92a7b6e1d73f158e1f8/numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, - {url = "https://files.pythonhosted.org/packages/8e/02/570545bac308b58ffb21adda0f4e220ba716fb658a63c151daecc3293350/numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, - {url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, - {url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, - {url = "https://files.pythonhosted.org/packages/ae/8c/ab03a7c25741f9ebc92684a20125fbc9fc1b8e1e700beb9197d750fdff88/numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, - {url = "https://files.pythonhosted.org/packages/b5/42/054082bd8220bbf6f297f982f0a8f5479fcbc55c8b511d928df07b965869/numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, - {url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, - {url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, - {url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, - {url = "https://files.pythonhosted.org/packages/f4/5f/fafd8c51235f60d49f7a88e2275e13971e90555b67da52dd6416caec32fe/numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, - {url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, -] -"oauthlib 3.2.2" = [ - {url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, - {url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, -] -"osm-rawdata 0.3.1" = [ - {url = "https://files.pythonhosted.org/packages/d7/99/b0bf848a85e4dbc97efd8e2bb7a863c3332f598f3c4fa8ff0523d462445c/osm_rawdata-0.3.1-py3-none-any.whl", hash = "sha256:21ef255381610c05ff6628a2d30bd2e84c4538c7d7a7355530f706b2dbeeab9c"}, - {url = "https://files.pythonhosted.org/packages/ea/6f/ab20af2aa087969d404814da23027d67a9c0704ab90f1bbea6ba42fe6976/osm-rawdata-0.3.1.tar.gz", hash = "sha256:8714eebc2a774b8ab366caecfce608f30a18a11d3601db19a5d3c10dddcd4514"}, -] -"packaging 24.1" = [ - {url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, -] -"parso 0.8.4" = [ - {url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, - {url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, -] -"passlib 1.7.4" = [ - {url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, - {url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, -] -"pexpect 4.9.0" = [ - {url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, - {url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, -] -"pickleshare 0.7.5" = [ - {url = "https://files.pythonhosted.org/packages/9a/41/220f49aaea88bc6fa6cba8d05ecf24676326156c23b991e80b3f2fc24c77/pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, - {url = "https://files.pythonhosted.org/packages/d8/b6/df3c1c9b616e9c0edbc4fbab6ddd09df9535849c64ba51fcb6531c32d4d8/pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, -] -"prompt-toolkit 3.0.47" = [ - {url = "https://files.pythonhosted.org/packages/47/6d/0279b119dafc74c1220420028d490c4399b790fc1256998666e3a341879f/prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, - {url = "https://files.pythonhosted.org/packages/e8/23/22750c4b768f09386d1c3cc4337953e8936f48a888fa6dddfb669b2c9088/prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, -] -"psycopg2 2.9.9" = [ - {url = "https://files.pythonhosted.org/packages/13/13/f74ffe6b6f58822e807c70391dc5679a53feb92ce119ccb8a6546c3fb893/psycopg2-2.9.9-cp312-cp312-win32.whl", hash = "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024"}, - {url = "https://files.pythonhosted.org/packages/1f/78/86b90d30c4e02e88379184ade34c2fd4883a4d3e420cc3c0f6da2b8f3a9a/psycopg2-2.9.9-cp38-cp38-win32.whl", hash = "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c"}, - {url = "https://files.pythonhosted.org/packages/2b/77/ffeb9ac356b3d99d97ca681bf0d0aa74f6d1d8c2ce0d6c4f2f34e396dbc0/psycopg2-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa"}, - {url = "https://files.pythonhosted.org/packages/37/2c/5133dd3183a3bd82371569f0dd783e6927672de7e671b278ce248810b7f7/psycopg2-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981"}, - {url = "https://files.pythonhosted.org/packages/58/4b/c4a26e191882b60150bfcb639e416524ae7f8249ab7ee854fb5247f16c40/psycopg2-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693"}, - {url = "https://files.pythonhosted.org/packages/6b/a8/5080c0e61a3b393a379ea2fa93402135c73baffcd5f08b9503e508aac116/psycopg2-2.9.9-cp39-cp39-win32.whl", hash = "sha256:c92811b2d4c9b6ea0285942b2e7cac98a59e166d59c588fe5cfe1eda58e72d59"}, - {url = "https://files.pythonhosted.org/packages/8e/e8/c439b378efc9f2d0fd1fd5f66b03cb9ed41423f179997a935f10374f3c0d/psycopg2-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:bac58c024c9922c23550af2a581998624d6e02350f4ae9c5f0bc642c633a2d5e"}, - {url = "https://files.pythonhosted.org/packages/91/2c/1fc5b9d33cd248c548ba19f2cef8e89cabaafab9858a602868a592cdc1b0/psycopg2-2.9.9-cp311-cp311-win32.whl", hash = "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372"}, - {url = "https://files.pythonhosted.org/packages/a2/14/2767d963915f957c07f5d4c3d9c5c9a407415289f5cde90b82cb3e8c2a12/psycopg2-2.9.9-cp310-cp310-win32.whl", hash = "sha256:38a8dcc6856f569068b47de286b472b7c473ac7977243593a288ebce0dc89516"}, - {url = "https://files.pythonhosted.org/packages/bc/bc/6572dec6834e779668421e25f8812a872d978e241f85491a5e4dda606a98/psycopg2-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3"}, - {url = "https://files.pythonhosted.org/packages/be/a7/0a39176d369a8289191f3d327139cfb4923dcedcfd7105774e57996f63cd/psycopg2-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a"}, - {url = "https://files.pythonhosted.org/packages/c9/5e/dc6acaf46d78979d6b03458b7a1618a68e152a6776fce95daac5e0f0301b/psycopg2-2.9.9.tar.gz", hash = "sha256:d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156"}, - {url = "https://files.pythonhosted.org/packages/f8/ec/ec73fe66d4317db006a38ebafbde02cb7e1d727ed65f5bbe54efb191d9e6/psycopg2-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:de80739447af31525feddeb8effd640782cf5998e1a4e9192ebdf829717e3913"}, -] -"psycopg2-binary 2.9.9" = [ - {url = "https://files.pythonhosted.org/packages/04/37/2429360ac5547378202db14eec0dde76edbe1f6627df5a43c7e164922859/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, - {url = "https://files.pythonhosted.org/packages/0a/7c/6aaf8c3cb05d86d2c3f407b95bac0c71a43f2718e38c1091972aacb5e1b2/psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, - {url = "https://files.pythonhosted.org/packages/0a/b7/3046fd37fdf84c1945741d16fa7536ddcf8576bad6d77ee2891c5255dce3/psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, - {url = "https://files.pythonhosted.org/packages/0e/6d/e97245eabff29d7c2de5fc1fc17cf7ef427beee93d20a5ae114c6e6718bd/psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, - {url = "https://files.pythonhosted.org/packages/14/33/12818c157e333cb9d9e6753d1b2463b6f60dbc1fade115f8e4dc5c52cac4/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, - {url = "https://files.pythonhosted.org/packages/18/ca/da384fd47233e300e3e485c90e7aab5d7def896d1281239f75901faf87d4/psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, - {url = "https://files.pythonhosted.org/packages/19/57/9f172b900795ea37246c78b5f52e00f4779984370855b3e161600156906d/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, - {url = "https://files.pythonhosted.org/packages/19/7a/e806ed82d954b9ec29b62f12ae8929da8910cde5ab7e919ec0983e56672d/psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, - {url = "https://files.pythonhosted.org/packages/24/1b/99b948254604838de668d81cc39a294a987721120c67963e00d47a290f7f/psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, - {url = "https://files.pythonhosted.org/packages/24/bf/4856c4985823ef094fc5a52eeb194f038dfbc57921f1d091853efc8e01b4/psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, - {url = "https://files.pythonhosted.org/packages/25/1f/7ae31759142999a8d06b3e250c1346c4abcdcada8fa884376775dc1de686/psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, - {url = "https://files.pythonhosted.org/packages/2e/49/9550febcc90105089bb89881b8cba73db227b9685cfddaae2479791a2379/psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, - {url = "https://files.pythonhosted.org/packages/3b/76/e46dae1b2273814ef80231f86d59cadf94ec36fd757045ed713c5b75cde7/psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, - {url = "https://files.pythonhosted.org/packages/3b/92/b463556409cdc12791cd8b1dae0072bf8efe817ef68b7ea3d9cf7d0e5656/psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, - {url = "https://files.pythonhosted.org/packages/3e/06/7d0085889cba209185f1a52d646c4dbdbd9075345f4ca685bacf08d30755/psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, - {url = "https://files.pythonhosted.org/packages/41/af/bce37630c525d2b9cf93f930110fc98616d6aca308d59b833b83b3a38176/psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, - {url = "https://files.pythonhosted.org/packages/42/46/c4170e77d71e76c1fe52ddc0d90f5e33f7afb72451e550268aba99675624/psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, - {url = "https://files.pythonhosted.org/packages/48/10/320b014e2fda509dc16409f76815ad0229e4fcec5ed229e64f7f917cc23f/psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, - {url = "https://files.pythonhosted.org/packages/4c/dc/a5d52ac82bd581bc18dbba2e7540cea458d85a0a72e3a58df7d9bfd8e133/psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"}, - {url = "https://files.pythonhosted.org/packages/50/66/fa53d2d3d92f6e1ef469d92afc6a4fe3f6e8a9a04b687aa28fb1f1d954ee/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, - {url = "https://files.pythonhosted.org/packages/56/a2/7851c68fe8768f3c9c246198b6356ee3e4a8a7f6820cc798443faada3400/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, - {url = "https://files.pythonhosted.org/packages/5b/ae/4a40f97bed02725b6c8b095fbe0587acda9f350f424d3d154acbf4d4c9aa/psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"}, - {url = "https://files.pythonhosted.org/packages/5e/4c/9233e0e206634a5387f3ab40f334a5325fb8bef2ca4e12ee7dbdeaf96afc/psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, - {url = "https://files.pythonhosted.org/packages/5f/e9/115e4f4f86d4aa5c300fdc90ac3d3e74401b4060d782281fe5b47d6c832d/psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, - {url = "https://files.pythonhosted.org/packages/62/2a/c0530b59d7e0d09824bc2102ecdcec0456b8ca4d47c0caa82e86fce3ed4c/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, - {url = "https://files.pythonhosted.org/packages/63/41/815d19767e2adb1a585213b801c954f46102f305c352c4a4f96287342d44/psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, - {url = "https://files.pythonhosted.org/packages/65/04/d0266fd446cb65384ce5442bc08449fd0b5d3419494574f020275f9e2273/psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, - {url = "https://files.pythonhosted.org/packages/65/4f/083f6b652b7fbecec1dc10bc9d1ffb66efd63611a25dddd0b4c1ef043ba6/psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"}, - {url = "https://files.pythonhosted.org/packages/66/41/fd79b2baf3079938f3c447e1a6087b10d09d2f0c54513e27a6872797f841/psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, - {url = "https://files.pythonhosted.org/packages/68/a2/67732accfffdb6696dc9523f82e91823e8cb1cc311f1c4ce006d2bb85016/psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, - {url = "https://files.pythonhosted.org/packages/6b/ae/05d1e52e8535ef3c73fa327e179d982b903e2c08987fa0e2167842c153a7/psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, - {url = "https://files.pythonhosted.org/packages/6f/ee/3ba07c6dc7c3294e717e94720da1597aedc82a10b1b180203ce183d4631a/psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, - {url = "https://files.pythonhosted.org/packages/70/a7/2cd2c9d5e23b556c11e3b7da41895808d9b056f8f34f50de4375a35b4951/psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, - {url = "https://files.pythonhosted.org/packages/70/bb/aec2646a705a09079d008ce88073401cd61fc9b04f92af3eb282caa3a2ec/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, - {url = "https://files.pythonhosted.org/packages/72/3d/acab427845198794aafd963dd073ee35810e2c52606e8a28c12db39821fb/psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, - {url = "https://files.pythonhosted.org/packages/73/17/ba28bb0022db5e2015a82d2df1c4b0d419c37fa07a588b3aff3adc4939f6/psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, - {url = "https://files.pythonhosted.org/packages/7a/1f/a6cf0cdf944253f7c45d90fbc876cc8bed5cc9942349306245715c0d88d6/psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, - {url = "https://files.pythonhosted.org/packages/7b/08/9c66c269b0d417a0af9fb969535f0371b8c538633535a7a6a5ca3f9231e2/psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, - {url = "https://files.pythonhosted.org/packages/7c/ae/cedd56e1f4a2b0e37213283caf3733a875c4c76f3372241e19c0d2a87355/psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, - {url = "https://files.pythonhosted.org/packages/81/0b/3adf561107c865928455891156d1dde5325253f7f4316fe56cd2c3f73570/psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, - {url = "https://files.pythonhosted.org/packages/82/69/c25f8bd5c189cee89bc00e16b49a84f930b3b4c2cfec953a26c99076a586/psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, - {url = "https://files.pythonhosted.org/packages/83/50/a054076c6358753661cd1da59f4dabc03e83d51690371f3fd1edb9e2cf72/psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, - {url = "https://files.pythonhosted.org/packages/88/d8/3bbedf39ab90d64d7a6f04e3f3744c74f46f55bd305cc2db3aa12e9c5667/psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, - {url = "https://files.pythonhosted.org/packages/92/57/96576e07132d7f7a1ac1df939575e6fdd8951aea337ee152b586bb51a971/psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, - {url = "https://files.pythonhosted.org/packages/94/62/6f8d281e279a71f20d1670f4de97d116b1d90948eb16c3c41ac305f94a30/psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, - {url = "https://files.pythonhosted.org/packages/94/68/1176fc14ea76861b7b8360be5176e87fb20d5091b137c76570eb4e237324/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, - {url = "https://files.pythonhosted.org/packages/97/17/0a4dab7369047d2d381acc79383f046b1208481b9da867b60a03d82adb31/psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"}, - {url = "https://files.pythonhosted.org/packages/97/18/7dfc189f70a170d7a7f9fbdda00e450f46a04420b3a06c0c7017e6017889/psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"}, - {url = "https://files.pythonhosted.org/packages/9a/6f/b593ca17c14907e35ea1077e372ce47b4e721002bd272577880d2541d331/psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"}, - {url = "https://files.pythonhosted.org/packages/9c/02/826dc5cdfc9515423ec912ba00cc2e4eb09f69e0339b177c9c742f2a09a2/psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, - {url = "https://files.pythonhosted.org/packages/a3/96/3153069df1378d398dcbd6968d4da89735bb63dccbb13a435bc65291c59c/psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, - {url = "https://files.pythonhosted.org/packages/a5/ac/702d300f3df169b9d0cbef0340d9f34a78bc18dc2dbafbcb39ff0f165cf8/psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, - {url = "https://files.pythonhosted.org/packages/a7/d0/5f2db14e7b53552276ab613399a83f83f85b173a862d3f20580bc7231139/psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, - {url = "https://files.pythonhosted.org/packages/ba/69/e38aeaace3a87dda1c152c039c72181fdcbff0787d5d2466944b663c4a57/psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, - {url = "https://files.pythonhosted.org/packages/ba/7e/c91f4c1e364444c8f59e82501f600019717f265ae4092057cece02d2d945/psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, - {url = "https://files.pythonhosted.org/packages/bc/0d/486e3fa27f39a00168abfcf14a3d8444f437f4b755cc34316da1124f293d/psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, - {url = "https://files.pythonhosted.org/packages/bc/14/76879e83245d8dc72812fb71fb3e710fa7482a05c4b1788b41b22e9fffcc/psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, - {url = "https://files.pythonhosted.org/packages/c1/d3/30a58e2399ad0d7830bb2c3c07b2e937e2ea8fe53e1d9e4b95d03d995362/psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, - {url = "https://files.pythonhosted.org/packages/c2/05/81e8bc7fca95574c9323e487d9ce1b58a4cfcc17f89b8fe843af46361211/psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, - {url = "https://files.pythonhosted.org/packages/c5/22/0b832bc8a83d8fed74c5a45fbacf4a6e6eb66bcb0cd3836fca9a709f25f4/psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, - {url = "https://files.pythonhosted.org/packages/cb/35/009b43d67a7010b789690a46d49521fd435ce33ce722fe8d7ac7efe35c21/psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, - {url = "https://files.pythonhosted.org/packages/cb/bd/e5fb2aa9737e50c0ffb9c3851710055eafedd4c8628c6ef9863bb2e9c434/psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, - {url = "https://files.pythonhosted.org/packages/ce/85/62825cabc6aad53104b7b6d12eb2ad74737d268630032d07b74d4444cb72/psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, - {url = "https://files.pythonhosted.org/packages/da/e0/073f50a65093a0bd087aff95754bdd61e8ef24d6ea29f47ea97575d4a784/psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, - {url = "https://files.pythonhosted.org/packages/dc/d0/512a73da2253c1904366155fd2c7ddf6d4e04a4eb434c90a18af8ce3d24b/psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, - {url = "https://files.pythonhosted.org/packages/e9/b0/9ca2b8e01a0912c9a14234fd5df7a241a1e44778c5797bf4b8eaa8dc3d3a/psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, - {url = "https://files.pythonhosted.org/packages/eb/08/bf324316b7e135a5bc82a850109095177a89e9624a08bcefa78f4b25a10d/psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"}, - {url = "https://files.pythonhosted.org/packages/ed/be/6c787962d706e55a528ef1693dd7251de657ae60e4d9d767ed61e8e2975c/psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, - {url = "https://files.pythonhosted.org/packages/f0/85/dff2170a1e6748b7068a4523ea9fef8373572c4cd283cffb4802f871a556/psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, - {url = "https://files.pythonhosted.org/packages/f4/bc/f273a15ccdbd479a1c60d1efd50bd2c96bab2b17c2d0760408e033c08165/psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"}, - {url = "https://files.pythonhosted.org/packages/f7/98/c2fedcbf0a9607519a010dcf88571138b2251062dbde3610cdba5ba1eee1/psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, - {url = "https://files.pythonhosted.org/packages/fc/07/e720e53bfab016ebcc34241695ccc06a9e3d91ba19b40ca81317afbdc440/psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, -] -"ptyprocess 0.7.0" = [ - {url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, - {url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, -] -"pure-eval 0.2.3" = [ - {url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, - {url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, -] -"pyarrow 17.0.0" = [ - {url = "https://files.pythonhosted.org/packages/18/4c/3db637d7578f683b0a8fb8999b436bdbedd6e3517bd4f90c70853cf3ad20/pyarrow-17.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c06d4624c0ad6674364bb46ef38c3132768139ddec1c56582dbac54f2663e2"}, - {url = "https://files.pythonhosted.org/packages/18/d8/7161d87d07ea51be70c49f615004c1446d5723622a18b2681f7e4b71bf6e/pyarrow-17.0.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c7916bff914ac5d4a8fe25b7a25e432ff921e72f6f2b7547d1e325c1ad9d155"}, - {url = "https://files.pythonhosted.org/packages/19/09/b0a02908180a25d57312ab5919069c39fddf30602568980419f4b02393f6/pyarrow-17.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:5984f416552eea15fd9cee03da53542bf4cddaef5afecefb9aa8d1010c335087"}, - {url = "https://files.pythonhosted.org/packages/27/4e/ea6d43f324169f8aec0e57569443a38bab4b398d09769ca64f7b4d467de3/pyarrow-17.0.0.tar.gz", hash = "sha256:4beca9521ed2c0921c1023e68d097d0299b62c362639ea315572a58f3f50fd28"}, - {url = "https://files.pythonhosted.org/packages/30/d1/63a7c248432c71c7d3ee803e706590a0b81ce1a8d2b2ae49677774b813bb/pyarrow-17.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a27532c38f3de9eb3e90ecab63dfda948a8ca859a66e3a47f5f42d1e403c4d03"}, - {url = "https://files.pythonhosted.org/packages/39/5d/78d4b040bc5ff2fc6c3d03e80fca396b742f6c125b8af06bcf7427f931bc/pyarrow-17.0.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:a5c8b238d47e48812ee577ee20c9a2779e6a5904f1708ae240f53ecbee7c9f07"}, - {url = "https://files.pythonhosted.org/packages/39/f4/90258b4de753df7cc61cefb0312f8abcf226672e96cc64996e66afce817a/pyarrow-17.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:a48ddf5c3c6a6c505904545c25a4ae13646ae1f8ba703c4df4a1bfe4f4006bda"}, - {url = "https://files.pythonhosted.org/packages/3b/73/8ed168db7642e91180330e4ea9f3ff8bab404678f00d32d7df0871a4933b/pyarrow-17.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db023dc4c6cae1015de9e198d41250688383c3f9af8f565370ab2b4cb5f62655"}, - {url = "https://files.pythonhosted.org/packages/3b/c8/5675719570eb1acd809481c6d64e2136ffb340bc387f4ca62dce79516cea/pyarrow-17.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b244dc8e08a23b3e352899a006a26ae7b4d0da7bb636872fa8f5884e70acf15"}, - {url = "https://files.pythonhosted.org/packages/3f/08/bc497130789833de09e345e3ce4647e3ce86517c4f70f2144f0367ca378b/pyarrow-17.0.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f553ca691b9e94b202ff741bdd40f6ccb70cdd5fbf65c187af132f1317de6145"}, - {url = "https://files.pythonhosted.org/packages/43/e0/a898096d35be240aa61fb2d54db58b86d664b10e1e51256f9300f47565e8/pyarrow-17.0.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:13d7a460b412f31e4c0efa1148e1d29bdf18ad1411eb6757d38f8fbdcc8645fb"}, - {url = "https://files.pythonhosted.org/packages/4c/21/9ca93b84b92ef927814cb7ba37f0774a484c849d58f0b692b16af8eebcfb/pyarrow-17.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:e3343cb1e88bc2ea605986d4b94948716edc7a8d14afd4e2c097232f729758b4"}, - {url = "https://files.pythonhosted.org/packages/59/22/f7d14907ed0697b5dd488d393129f2738629fa5bcba863e00931b7975946/pyarrow-17.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b564a51fbccfab5a04a80453e5ac6c9954a9c5ef2890d1bcf63741909c3f8df"}, - {url = "https://files.pythonhosted.org/packages/5e/78/3931194f16ab681ebb87ad252e7b8d2c8b23dad49706cadc865dff4a1dd3/pyarrow-17.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b72e87fe3e1db343995562f7fff8aee354b55ee83d13afba65400c178ab2597"}, - {url = "https://files.pythonhosted.org/packages/64/d9/51e35550f2f18b8815a2ab25948f735434db32000c0e91eba3a32634782a/pyarrow-17.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:edca18eaca89cd6382dfbcff3dd2d87633433043650c07375d095cd3517561d8"}, - {url = "https://files.pythonhosted.org/packages/75/63/29d1bfcc57af73cde3fc3baccab2f37548de512dbe0ab294b033cd203516/pyarrow-17.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:dec8d129254d0188a49f8a1fc99e0560dc1b85f60af729f47de4046015f9b0a5"}, - {url = "https://files.pythonhosted.org/packages/81/36/e78c24be99242063f6d0590ef68c857ea07bdea470242c361e9a15bd57a4/pyarrow-17.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da1e060b3876faa11cee287839f9cc7cdc00649f475714b8680a05fd9071d545"}, - {url = "https://files.pythonhosted.org/packages/81/3c/0580626896c842614a523e66b351181ed5bb14e5dfc263cd68cea2c46d90/pyarrow-17.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:fa3c246cc58cb5a4a5cb407a18f193354ea47dd0648194e6265bd24177982fe8"}, - {url = "https://files.pythonhosted.org/packages/8d/8e/ce2e9b2146de422f6638333c01903140e9ada244a2a477918a368306c64c/pyarrow-17.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e19f569567efcbbd42084e87f948778eb371d308e137a0f97afe19bb860ccb3"}, - {url = "https://files.pythonhosted.org/packages/8d/bd/8f52c1d7b430260f80a349cffa2df351750a737b5336313d56dcadeb9ae1/pyarrow-17.0.0-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:af5ff82a04b2171415f1410cff7ebb79861afc5dae50be73ce06d6e870615204"}, - {url = "https://files.pythonhosted.org/packages/8e/0a/dbd0c134e7a0c30bea439675cc120012337202e5fac7163ba839aa3691d2/pyarrow-17.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1e70de6cb5790a50b01d2b686d54aaf73da01266850b05e3af2a1bc89e16053"}, - {url = "https://files.pythonhosted.org/packages/ae/49/baafe2a964f663413be3bd1cf5c45ed98c5e42e804e2328e18f4570027c1/pyarrow-17.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:392bc9feabc647338e6c89267635e111d71edad5fcffba204425a7c8d13610d7"}, - {url = "https://files.pythonhosted.org/packages/af/61/bcd9b58e38ead6ad42b9ed00da33a3f862bc1d445e3d3164799c25550ac2/pyarrow-17.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a155acc7f154b9ffcc85497509bcd0d43efb80d6f733b0dc3bb14e281f131c8b"}, - {url = "https://files.pythonhosted.org/packages/bf/ee/661211feac0ed48467b1d5c57298c91403809ec3ab78b1d175e1d6ad03cf/pyarrow-17.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32503827abbc5aadedfa235f5ece8c4f8f8b0a3cf01066bc8d29de7539532687"}, - {url = "https://files.pythonhosted.org/packages/c2/0c/ea2107236740be8fa0e0d4a293a095c9f43546a2465bb7df34eee9126b09/pyarrow-17.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:757074882f844411fcca735e39aae74248a1531367a7c80799b4266390ae51cc"}, - {url = "https://files.pythonhosted.org/packages/cb/05/3f4a16498349db79090767620d6dc23c1ec0c658a668d61d76b87706c65d/pyarrow-17.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0071ce35788c6f9077ff9ecba4858108eebe2ea5a3f7cf2cf55ebc1dbc6ee24a"}, - {url = "https://files.pythonhosted.org/packages/d1/db/42ac644453cfdfc60fe002b46d647fe7a6dfad753ef7b28e99b4c936ad5d/pyarrow-17.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:02dae06ce212d8b3244dd3e7d12d9c4d3046945a5933d28026598e9dbbda1fca"}, - {url = "https://files.pythonhosted.org/packages/d3/2e/493dd7db889402b4c7871ca7dfdd20f2c5deedbff802d3eb8576359930f9/pyarrow-17.0.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0cdb0e627c86c373205a2f94a510ac4376fdc523f8bb36beab2e7f204416163c"}, - {url = "https://files.pythonhosted.org/packages/d4/62/ce6ac1275a432b4a27c55fe96c58147f111d8ba1ad800a112d31859fae2f/pyarrow-17.0.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9b8a823cea605221e61f34859dcc03207e52e409ccf6354634143e23af7c8d22"}, - {url = "https://files.pythonhosted.org/packages/d8/81/69b6606093363f55a2a574c018901c40952d4e902e670656d18213c71ad7/pyarrow-17.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dc5c31c37409dfbc5d014047817cb4ccd8c1ea25d19576acf1a001fe07f5b420"}, - {url = "https://files.pythonhosted.org/packages/e6/c1/4c6bcdf7a820034aa91a8b4d25fef38809be79b42ca7aaa16d4680b0bbac/pyarrow-17.0.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:d7d192305d9d8bc9082d10f361fc70a73590a4c65cf31c3e6926cd72b76bc35c"}, - {url = "https://files.pythonhosted.org/packages/e7/f6/b75d4816c32f1618ed31a005ee635dd1d91d8164495d94f2ea092f594661/pyarrow-17.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:42bf93249a083aca230ba7e2786c5f673507fa97bbd9725a1e2754715151a204"}, - {url = "https://files.pythonhosted.org/packages/ee/fb/c1b47f0ada36d856a352da261a44d7344d8f22e2f7db3945f8c3b81be5dd/pyarrow-17.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:f7ae2de664e0b158d1607699a16a488de3d008ba99b3a7aa5de1cbc13574d047"}, - {url = "https://files.pythonhosted.org/packages/f1/c4/9625418a1413005e486c006e56675334929fad864347c5ae7c1b2e7fe639/pyarrow-17.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b0c6ac301093b42d34410b187bba560b17c0330f64907bfa4f7f7f2444b0cf9b"}, - {url = "https://files.pythonhosted.org/packages/f6/b0/b9164a8bc495083c10c281cc65064553ec87b7537d6f742a89d5953a2a3e/pyarrow-17.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9ba11c4f16976e89146781a83833df7f82077cdab7dc6232c897789343f7891a"}, - {url = "https://files.pythonhosted.org/packages/f9/46/ce89f87c2936f5bb9d879473b9663ce7a4b1f4359acc2f0eb39865eaa1af/pyarrow-17.0.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:1c8856e2ef09eb87ecf937104aacfa0708f22dfeb039c363ec99735190ffb977"}, -] -"pycparser 2.22" = [ - {url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] -"pycryptodome 3.20.0" = [ - {url = "https://files.pythonhosted.org/packages/09/12/34eb6587adcee5d676533e4c217a6385a2f4d90086198a3b1ade5dcdf684/pycryptodome-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acf6e43fa75aca2d33e93409f2dafe386fe051818ee79ee8a3e21de9caa2ac9e"}, - {url = "https://files.pythonhosted.org/packages/0d/08/01987ab75ca789247a88c8b2f0ce374ef7d319e79589e0842e316a272662/pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c"}, - {url = "https://files.pythonhosted.org/packages/17/87/c7153fcd400df0f4a67d7d92cdb6b5e43f309c22434374b8a61849dfb280/pycryptodome-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a"}, - {url = "https://files.pythonhosted.org/packages/1f/90/d131c0eb643290230dfa4108b7c2d135122d88b714ad241d77beb4782a76/pycryptodome-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9"}, - {url = "https://files.pythonhosted.org/packages/24/0a/9e0791833984305a8ee7cb8b1feaabffdbe1607f79f6890b38259befacc4/pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5601c934c498cd267640b57569e73793cb9a83506f7c73a8ec57a516f5b0b091"}, - {url = "https://files.pythonhosted.org/packages/24/80/56a04e2ae622d7f38c1c01aef46a26c6b73a2ad15c9705a8e008b5befb03/pycryptodome-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a"}, - {url = "https://files.pythonhosted.org/packages/30/4b/cbc67cda0efd55d7ddcc98374c4b9c853022a595ed1d78dd15c961bc7f6e/pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128"}, - {url = "https://files.pythonhosted.org/packages/39/12/5fe7f5b9212dda9f5a26f842a324d6541fe1ca8059602124ff30db1e874b/pycryptodome-3.20.0-cp35-abi3-win32.whl", hash = "sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72"}, - {url = "https://files.pythonhosted.org/packages/42/4c/706ef0c97ef61598d6b3745cfdae57c09b10b61fd60700d69443173bd430/pycryptodome-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ec0bb1188c1d13426039af8ffcb4dbe3aad1d7680c35a62d8eaf2a529b5d3d4f"}, - {url = "https://files.pythonhosted.org/packages/53/a3/1345f914963d7d668a5423dc563deafae02479bd1c69b39180724475584f/pycryptodome-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a60fedd2b37b4cb11ccb5d0399efe26db9e0dd149016c1cc6c8161974ceac2d6"}, - {url = "https://files.pythonhosted.org/packages/5d/c3/5530f270c4ec87953fbed203e4f1f4a2fa002bc43efdc1b3cf9ab442e741/pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab6ab0cb755154ad14e507d1df72de9897e99fd2d4922851a276ccc14f4f1a5"}, - {url = "https://files.pythonhosted.org/packages/68/31/d444cbb52f348ea89f90e2aff4804e03b42671c784719ee7c75d51db2913/pycryptodome-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a1250b7ea809f752b68e3e6f3fd946b5939a52eaeea18c73bdab53e9ba3c2dd"}, - {url = "https://files.pythonhosted.org/packages/68/9a/88d984405b087e8c8dd9a9d4c81a6fa675454e5fcf2ae01d9553b3128637/pycryptodome-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e"}, - {url = "https://files.pythonhosted.org/packages/6a/3d/ba3905a0ae6dd4e8686dbde85c71ce38e27f5ad3587424891238ad520aaf/pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:405002eafad114a2f9a930f5db65feef7b53c4784495dd8758069b89baf68eab"}, - {url = "https://files.pythonhosted.org/packages/75/00/744661e96afcb5016c10ee821fe6ff6962f5feb020d7286d082c004a36dd/pycryptodome-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:3cd3ef3aee1079ae44afaeee13393cf68b1058f70576b11439483e34f93cf818"}, - {url = "https://files.pythonhosted.org/packages/80/fc/bc18a2951ab3104caa67cae290d42d9cd230884f0d27bf0891f821636f32/pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d29daa681517f4bc318cd8a23af87e1f2a7bad2fe361e8aa29c77d652a065de4"}, - {url = "https://files.pythonhosted.org/packages/8b/61/522235ca81d9dcfcf8b4cbc253b3a8a1f2231603d486369a8a02eb998f31/pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea"}, - {url = "https://files.pythonhosted.org/packages/9a/6a/ccfc4b1c7eee616dd9b3a663a26ec1ba2a13319dd51876f64867b4ab3d27/pycryptodome-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:06d6de87c19f967f03b4cf9b34e538ef46e99a337e9a61a77dbe44b2cbcf0690"}, - {url = "https://files.pythonhosted.org/packages/a2/40/63dff38fa4f7888f812263494d4a745eeed180ff09dd7b8350a81eb09d21/pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3"}, - {url = "https://files.pythonhosted.org/packages/ac/1e/d0fbf9c82e49c0e0c5ceebf4e9c3acdbdad21fe47b2a7cc5db2284140401/pycryptodome-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:f0e6d631bae3f231d3634f91ae4da7a960f7ff87f2865b2d2b831af1dfb04e9a"}, - {url = "https://files.pythonhosted.org/packages/af/20/5f29ec45462360e7f61e8688af9fe4a0afae057edfabdada662e11bf97e7/pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c"}, - {url = "https://files.pythonhosted.org/packages/b5/bf/798630923b67f4201059c2d690105998f20a6a8fb9b5ab68d221985155b3/pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4"}, - {url = "https://files.pythonhosted.org/packages/b9/ed/19223a0a0186b8a91ebbdd2852865839237a21c74f1fbc4b8d5b62965239/pycryptodome-3.20.0.tar.gz", hash = "sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7"}, - {url = "https://files.pythonhosted.org/packages/c7/10/88fb67d2fa545ce2ac61cfda70947bcbb1769f1956315c4b919d79774897/pycryptodome-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04"}, - {url = "https://files.pythonhosted.org/packages/dd/20/b4b6bd07bfb6f6826b147131dcea9fea99559077842ad7e304a7464353c5/pycryptodome-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:417a276aaa9cb3be91f9014e9d18d10e840a7a9b9a9be64a42f553c5b50b4d1d"}, - {url = "https://files.pythonhosted.org/packages/e5/1f/6bc4beb4adc07b847e5d3fddbec4522c2c3aa05df9e61b91dc4eff6a4946/pycryptodome-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25"}, - {url = "https://files.pythonhosted.org/packages/e9/a7/5aa0596f7fc710fd55b4e6bbb025fedacfec929465a618f20e61ebf7df76/pycryptodome-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b"}, - {url = "https://files.pythonhosted.org/packages/ea/94/82ebfa5c83d980907ceebf79b00909a569d258bdfd9b0264d621fa752cfd/pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2"}, - {url = "https://files.pythonhosted.org/packages/ef/50/090be8ca0ea560037bf515c5b2f27547777e2175244f168555ecccc23c54/pycryptodome-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:baee115a9ba6c5d2709a1e88ffe62b73ecc044852a925dcb67713a288c4ec70f"}, - {url = "https://files.pythonhosted.org/packages/f0/65/6cb997318100aa9f7dfc2753a611c4728a84825990645a0391859deeaa6f/pycryptodome-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3427d9e5310af6680678f4cce149f54e0bb4af60101c7f2c16fdf878b39ccccc"}, - {url = "https://files.pythonhosted.org/packages/fb/0b/eb6bfe34a9b7a265e103084a3cfc0dbb2a102d04a6239ce91434b03641c0/pycryptodome-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:d5954acfe9e00bc83ed9f5cb082ed22c592fbbef86dc48b907238be64ead5c33"}, - {url = "https://files.pythonhosted.org/packages/ff/96/b0d494defb3346378086848a8ece5ddfd138a66c4a05e038fca873b2518c/pycryptodome-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044"}, -] -"pydantic 2.5.2" = [ - {url = "https://files.pythonhosted.org/packages/0a/2b/64066de1c4cf3d4ed623beeb3bbf3f8d0cc26661f1e7d180ec5eb66b75a5/pydantic-2.5.2-py3-none-any.whl", hash = "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0"}, - {url = "https://files.pythonhosted.org/packages/b7/41/3c8108f79fb7da2d2b17f35744232af4ffcd9e764ebe1e3fd4b26669b325/pydantic-2.5.2.tar.gz", hash = "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd"}, -] -"pydantic-core 2.14.5" = [ - {url = "https://files.pythonhosted.org/packages/00/47/88baa62574f06e2dd5b9c0285b5b9b300c79e3d808c5d5a81f04e0817b82/pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0"}, - {url = "https://files.pythonhosted.org/packages/03/99/f7eb0cc34ea21e94aa0610a9c0794064847adc38ab824c8722e9fe35ebba/pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789"}, - {url = "https://files.pythonhosted.org/packages/04/a1/36cea283ded0641e8c374cdcacfdab035c102467ac5ec721b7527c8ac1cf/pydantic_core-2.14.5-cp311-none-win_amd64.whl", hash = "sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077"}, - {url = "https://files.pythonhosted.org/packages/04/f7/0a58ef9ff38e79cf99dcc56a031568717a7b78f0e23117b4a7ccfad4f7b9/pydantic_core-2.14.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c7d5b5005f177764e96bd584d7bf28d6e26e96f2a541fdddb934c486e36fd59"}, - {url = "https://files.pythonhosted.org/packages/05/7b/9083133f247b9f712f5718c66b3e39194ea679fbe85567bf4dc9d08557bb/pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07"}, - {url = "https://files.pythonhosted.org/packages/05/f2/b880e32258a4bf118ecd19eeaa2eb371de0c119083814a88cd4278ae8c6d/pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c1988019752138b974c28f43751528116bcceadad85f33a258869e641d753"}, - {url = "https://files.pythonhosted.org/packages/08/01/ced0c6a1ac6737cfddbe8e81ec73278f3ec6e2627890fbf052b3ece56b48/pydantic_core-2.14.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3"}, - {url = "https://files.pythonhosted.org/packages/09/7e/4f228a4af0eb52a91ccb794b85ad9832b1148ee2f9df78825b698d0f8bc7/pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634"}, - {url = "https://files.pythonhosted.org/packages/0b/32/0a6ee79ed34e8934a54548495883017dfaf3fc742b0d0d02afa154f1f49d/pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4"}, - {url = "https://files.pythonhosted.org/packages/10/89/bbb9bb3bd59b1cb36a87c2f6b6e3b2858fdb6ac438539f67a6c93a91ba5e/pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d"}, - {url = "https://files.pythonhosted.org/packages/12/00/bd693e0bf24fa016c7194ac9ca671903b0938a5aa2603f7b5779070a15a0/pydantic_core-2.14.5-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459"}, - {url = "https://files.pythonhosted.org/packages/14/1e/248f84fbc14868a09792dc0cb4536885ec91c3ba94b58eae09603d39b067/pydantic_core-2.14.5-cp38-none-win32.whl", hash = "sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867"}, - {url = "https://files.pythonhosted.org/packages/19/1c/d9ba54c20c76706eb04491187d2d22ce56982ec3d999c6915ceb16755ebd/pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209"}, - {url = "https://files.pythonhosted.org/packages/1a/b8/7f1ca7c80dcb44bd525ba5e5feba5e45be686daeee535b434628be0f6cd7/pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5"}, - {url = "https://files.pythonhosted.org/packages/1d/0f/bb0bd20e5bbabdf99d0a25858cf77b74926826a75d0458dc4842cf360ea5/pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8"}, - {url = "https://files.pythonhosted.org/packages/1f/f0/a588fd5d66c9c3bf16d63cac3437e2260cbddd7df7a089ca58b8e94dcb3e/pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3"}, - {url = "https://files.pythonhosted.org/packages/21/4d/ab83317443b0dd764f75c0224b959faff30e0150ac8b54fceb4cec682ee8/pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e47a76848f92529879ecfc417ff88a2806438f57be4a6a8bf2961e8f9ca9ec7"}, - {url = "https://files.pythonhosted.org/packages/22/11/3f332887a888217e28b23c115c343ef89ccf5f49bbbd88d9317c707b00ac/pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937"}, - {url = "https://files.pythonhosted.org/packages/26/40/8d8f5f432c081889cc06af631dc3a0952426bc06bc26ca563b8d828213d9/pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34708cc82c330e303f4ce87758828ef6e457681b58ce0e921b6e97937dd1e2a3"}, - {url = "https://files.pythonhosted.org/packages/28/27/83ad40b64e8503b0eaeb88f6206225d0a3be1bd1d852dfdc4437f7e02a69/pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd"}, - {url = "https://files.pythonhosted.org/packages/28/81/f5452ccf3b15aa280188fbf2b6ab39ed700623df4fcc28675f19eee9634a/pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada"}, - {url = "https://files.pythonhosted.org/packages/2a/83/05756b6656c3478e34e5dd5fcb693034f586bb1d437365928f6989bb0050/pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3"}, - {url = "https://files.pythonhosted.org/packages/2a/b7/f85e5fd4504fae0df3eadd4bf9e0c495ecbdb804dc9be65653119454571e/pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db"}, - {url = "https://files.pythonhosted.org/packages/2c/43/d94f10d82ccffc86bd69bfac73c54589703008236d63965dd40005a80af9/pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997"}, - {url = "https://files.pythonhosted.org/packages/2f/eb/4b07b31c4a728b02cae14cc2a447ebd460dfdf7076fe56a074ff7e27be4f/pydantic_core-2.14.5-cp311-none-win32.whl", hash = "sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651"}, - {url = "https://files.pythonhosted.org/packages/36/53/d4ae1f5273cbc83d5a4c158916a9235c1bfc8194be63958b4b5ff11bf838/pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911"}, - {url = "https://files.pythonhosted.org/packages/3a/dd/fc81e3ea962a356a705fa06965a7dbc0b204da014f238df95f1cd276bfab/pydantic_core-2.14.5-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093"}, - {url = "https://files.pythonhosted.org/packages/3b/42/3c5c49421ed99af065413c9f99e6e5abbeb0e9700e1f20934d0d9f6b3e23/pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5"}, - {url = "https://files.pythonhosted.org/packages/3c/5e/2a822aa3f3dd68fa45129d4d50290625e97b9b223cf76bafeb765430a0bc/pydantic_core-2.14.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f"}, - {url = "https://files.pythonhosted.org/packages/3d/9f/bd9a41853a8ad6854cf126e72bb19a4849f79efe2d544b1a44f5351b9748/pydantic_core-2.14.5-cp310-none-win_amd64.whl", hash = "sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b"}, - {url = "https://files.pythonhosted.org/packages/41/0a/1c0372929f3723587d66c188cbdd0c47d269447e0ac8f231f0db0f9bb03c/pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f"}, - {url = "https://files.pythonhosted.org/packages/41/64/43de643a6d2d157a8ac508a7c2a6a9746c941a659a6c64e00ebd13d5db4f/pydantic_core-2.14.5-cp39-none-win_amd64.whl", hash = "sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab"}, - {url = "https://files.pythonhosted.org/packages/43/d9/aafcec666d3629994ca6572606821c33aee5bf14c0325177d4038f486ecd/pydantic_core-2.14.5-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04"}, - {url = "https://files.pythonhosted.org/packages/46/df/5159aa30c4b2128f14634f3b3e9e19df228364c2107cda7910d058cc1bca/pydantic_core-2.14.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66"}, - {url = "https://files.pythonhosted.org/packages/47/85/190ee74d99149a6d16bf14016d0011b629702d37b955070a5fabaa3be8a8/pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144"}, - {url = "https://files.pythonhosted.org/packages/47/b0/4123a00675f2712c57da7659ec1e20a01842454a05a4d49ad8978ec0dada/pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e4d090e73e0725b2904fdbdd8d73b8802ddd691ef9254577b708d413bf3006e"}, - {url = "https://files.pythonhosted.org/packages/4a/5c/cc41dad06acd213f093581454812d6bb20311524ecf265f893e05e4fbe84/pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2"}, - {url = "https://files.pythonhosted.org/packages/4c/66/f762b78fccf7b0ba99417a807df4e65164b32960bb0568c788cca68fafea/pydantic_core-2.14.5-cp37-none-win32.whl", hash = "sha256:de790a3b5aa2124b8b78ae5faa033937a72da8efe74b9231698b5a1dd9be3405"}, - {url = "https://files.pythonhosted.org/packages/4e/77/02bb9e292fdce2c25cf690a5d7a63487eaf264ff200ecba03ffeff3376da/pydantic_core-2.14.5-cp39-none-win32.whl", hash = "sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6"}, - {url = "https://files.pythonhosted.org/packages/4f/10/c44d89cb2fa31a27766aeb39b11380ad2e01bdab7f4bf63b18dfea20ec00/pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b"}, - {url = "https://files.pythonhosted.org/packages/55/04/9bcff971fb8f35a34a2550ba26b8c79a1b776a00963211bda64cf7bed7a4/pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d81e6987b27bc7d101c8597e1cd2bcaa2fee5e8e0f356735c7ed34368c471550"}, - {url = "https://files.pythonhosted.org/packages/56/63/aa9ba88b8e4514d1c8ba5b30ba5207586b9ec416dc62e24191832b28658b/pydantic_core-2.14.5-cp37-none-win_amd64.whl", hash = "sha256:6c327e9cd849b564b234da821236e6bcbe4f359a42ee05050dc79d8ed2a91588"}, - {url = "https://files.pythonhosted.org/packages/57/03/0f238853ad2c93ba344ad702234ee02ff8daa10b7cd680523a40a851499d/pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c"}, - {url = "https://files.pythonhosted.org/packages/5a/cf/1348242330768c4014ba26c51a847c23db105da6b21bdcefbc9087926af3/pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc"}, - {url = "https://files.pythonhosted.org/packages/5b/49/84996421688461dd919f54e33eb4c689944ad434e6b527ec6359ebb99049/pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a71891847f0a73b1b9eb86d089baee301477abef45f7eaf303495cd1473613e4"}, - {url = "https://files.pythonhosted.org/packages/60/5a/3161e1a1c138407cd2037b12ecdbe29f4890ccda1c0a0be69438c7d0065d/pydantic_core-2.14.5-cp312-none-win_amd64.whl", hash = "sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36"}, - {url = "https://files.pythonhosted.org/packages/62/5c/de43c71edd1cda67e5cc194873ee84483230ac9cf576d6020ee945e0494e/pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec"}, - {url = "https://files.pythonhosted.org/packages/63/e6/8887679b7f923290db2638bf80733c609aaefaae29b9fe99b83f800c1910/pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26"}, - {url = "https://files.pythonhosted.org/packages/64/26/cffb93fe9c6b5a91c497f37fae14a4b073ecbc47fc36a9979c7aa888b245/pydantic_core-2.14.5.tar.gz", hash = "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71"}, - {url = "https://files.pythonhosted.org/packages/66/11/f3e35b74745b5167df5f1dc15bd2368dbaa9e70d2ad8438a0c9485b78da5/pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093"}, - {url = "https://files.pythonhosted.org/packages/66/44/ed210be2a055e612d58146be167017e43a76ff79807c753a264d7084d24d/pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520"}, - {url = "https://files.pythonhosted.org/packages/6c/ba/f3eee66c90f2e4f468fc01cace46ec633f9d47d53e1610ef3bc6003fc936/pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8"}, - {url = "https://files.pythonhosted.org/packages/75/cf/2f6e6410ae735c11df32c391948a6c601a22f40f414b5dfc24f2def8c064/pydantic_core-2.14.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4"}, - {url = "https://files.pythonhosted.org/packages/76/b3/54001e0b49c3eb135cccb1d353c8bd758b77b60d3c610b47888ac1e12fa6/pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124"}, - {url = "https://files.pythonhosted.org/packages/78/ef/4fd3b40a82ea729a2566575aeec119449b0bf1b4c13d9255e8ac2a40a58b/pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9"}, - {url = "https://files.pythonhosted.org/packages/79/73/d1d3846f19b11a7d62e93e5c38c5386c42f3e42abad46c0d1904ccdf8fef/pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e"}, - {url = "https://files.pythonhosted.org/packages/7c/f5/3e59681bd53955da311a7f4efbb6315d01006e9d18b8a06b527a22d3d923/pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113"}, - {url = "https://files.pythonhosted.org/packages/7d/d4/4069e3864fa5fd32679e25cedb5892bd4ab823762a2f0a844e2cff30c509/pydantic_core-2.14.5-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:45e95333b8418ded64745f14574aa9bfc212cb4fbeed7a687b0c6e53b5e188cd"}, - {url = "https://files.pythonhosted.org/packages/7d/de/df454233c7960a899846f037209204df1d8ab761bb81a7561abb4daf2288/pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3"}, - {url = "https://files.pythonhosted.org/packages/7e/ff/72d57544a70f4f37a06c40cfe1c4a038bc21db308e916a277faa1854a1d8/pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d"}, - {url = "https://files.pythonhosted.org/packages/80/ee/c1ce56f63f08bf261f243d7f5faed5b1d2215d231996e74f7dd89559e9e5/pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706"}, - {url = "https://files.pythonhosted.org/packages/84/01/079cd694491f1e05a1caae15a2ee32321a8fa748a34a183f6a38bf885af9/pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d"}, - {url = "https://files.pythonhosted.org/packages/89/5c/e0584d534863639757e05479a3c1172550e3d3dab0c39b79e41692d1804d/pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189"}, - {url = "https://files.pythonhosted.org/packages/89/e5/74008150a3b0fd26e1375359612454951234ee02dd9b8c6700f41bcdeee6/pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88"}, - {url = "https://files.pythonhosted.org/packages/8c/80/b7678c547b947cec35c136d88baf315fa6837500d9f8ce7353347f50a521/pydantic_core-2.14.5-cp312-none-win32.whl", hash = "sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260"}, - {url = "https://files.pythonhosted.org/packages/8f/af/b202d44845f89e9c997f2f351be35a76ff78304eb926b1bdb33929de40db/pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc"}, - {url = "https://files.pythonhosted.org/packages/90/6f/52cb83061430628878c34fdb199ccc8313a104f1390d99bff4a29b2ff6fe/pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69"}, - {url = "https://files.pythonhosted.org/packages/94/cd/de236ed3c5a2a0f5545cf78e7a6aaa04d8ee10dc3b738cc516bfc59dfb18/pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8"}, - {url = "https://files.pythonhosted.org/packages/9a/e1/c33fcdbdad7f5c29376fa2e57f8d60f966c44fc77fc36a70d0ae03bbe813/pydantic_core-2.14.5-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7"}, - {url = "https://files.pythonhosted.org/packages/9b/3f/e45bc705e168afbcbc2d7669aa5c54d4c68d5882af09fa7410daf6159de5/pydantic_core-2.14.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944"}, - {url = "https://files.pythonhosted.org/packages/9c/52/2fc8b7e07f360993bc3d5f9ea743aac9f59287002035887c7d4f45bc6fb6/pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef"}, - {url = "https://files.pythonhosted.org/packages/9e/f3/9e3d334976b5051cd18e3feef06516ead3230efb8b9af8514bc52b2795b1/pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed"}, - {url = "https://files.pythonhosted.org/packages/a4/09/90f5a03ab19e21601c6fec11fc9dea30e3228731e12b2f75f58d02430b85/pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b"}, - {url = "https://files.pythonhosted.org/packages/a6/c6/01758bde5022817fd202ee9de506ea5ba3cedc9aa4b421edabda0d1b9fa4/pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca"}, - {url = "https://files.pythonhosted.org/packages/a7/be/6be1245f78b72da970cf52cf4c55d8abcfd1655114d122ee6cf5641fc3f5/pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00"}, - {url = "https://files.pythonhosted.org/packages/a9/2b/f1dca235271785f19e0f3696b31140d6a69ff5349970253c034f9c603b8e/pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe"}, - {url = "https://files.pythonhosted.org/packages/ab/43/77d8f56eb332e84097f18fc294346d214e9f0d22fb9ec67ebed4b8e90e35/pydantic_core-2.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88"}, - {url = "https://files.pythonhosted.org/packages/ab/a6/e6e660299765ae03a55375935d5c6edc9d3e4798e63642f6c3030e15fddf/pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1"}, - {url = "https://files.pythonhosted.org/packages/af/ab/79c2126e5504a3f0ecc0b1d97768594f9baa090134b0053309a2d938efaa/pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda"}, - {url = "https://files.pythonhosted.org/packages/b0/a6/8ec00902795bcc3f2b77e4618f31f1ddb12a9a6980b0f3d8fd6e0a672733/pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7"}, - {url = "https://files.pythonhosted.org/packages/b2/83/ae5698f7a8121599b239ea547f58f7b135e299e87cfe1a88fb1e6319d57c/pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6"}, - {url = "https://files.pythonhosted.org/packages/b4/2d/310b1c6050af78b6710e85e132a75b65b07624e01b2f83a6a0c7ee79f045/pydantic_core-2.14.5-cp38-none-win_amd64.whl", hash = "sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d"}, - {url = "https://files.pythonhosted.org/packages/ba/95/d1104b88d5e3ad42db30935a4c258da2385139dd216ec8dfbc347a32dbff/pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331"}, - {url = "https://files.pythonhosted.org/packages/ba/9b/5246600a17467ad8071174250d7727b34f5dc0dfe74abf3e99dbdf1beee1/pydantic_core-2.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de"}, - {url = "https://files.pythonhosted.org/packages/bb/3b/9a6f42b52856348b054b13ca79df1e359fff8d6c04dcf1dd3a44f12b7f79/pydantic_core-2.14.5-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:af36f36538418f3806048f3b242a1777e2540ff9efaa667c27da63d2749dbce0"}, - {url = "https://files.pythonhosted.org/packages/bf/d2/4820db26970effb5d6fdee68f578585448b2eb6dd7344ab55b20958a0874/pydantic_core-2.14.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863"}, - {url = "https://files.pythonhosted.org/packages/bf/ed/ee221482b51f368884ea6453f3784eeaeb17f5b737589d39d68a89bffde7/pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6"}, - {url = "https://files.pythonhosted.org/packages/c0/d2/b31c030802f29c35fa0c8ab92891bee9dcedd2793df560041b6d38f5fd49/pydantic_core-2.14.5-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd"}, - {url = "https://files.pythonhosted.org/packages/cb/96/27421976cde52555eb20636d59743621d4fa3bba278a0e4dbb4751e3f5c1/pydantic_core-2.14.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e"}, - {url = "https://files.pythonhosted.org/packages/cf/b7/9bacf7f9439f785b2fe6d8199e28ad75ad25406f97f33c0186274a48a36d/pydantic_core-2.14.5-cp312-none-win_arm64.whl", hash = "sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27"}, - {url = "https://files.pythonhosted.org/packages/d2/d7/0f13f8cce749c4c5484ddfe60239bcce21a2a6cdcea250f13ae471cb86cb/pydantic_core-2.14.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955"}, - {url = "https://files.pythonhosted.org/packages/d4/bb/923eeeb3e87ba9024e311e0f3d1f0a4baad609ed7bfc7da7341e95981bd4/pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4"}, - {url = "https://files.pythonhosted.org/packages/d9/54/8fbc8a49814e7caf174d906e636a82d4f34e6d642f6b67f792080ae7ced4/pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a717aef6971208f0851a2420b075338e33083111d92041157bbe0e2713b37325"}, - {url = "https://files.pythonhosted.org/packages/dc/1b/eb3861748a1669865f7b01dd73dedc185f1e2dad84c56a0fd00672e7fac8/pydantic_core-2.14.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b"}, - {url = "https://files.pythonhosted.org/packages/e1/f7/b8dee069f0365fc4f0b39ab7ab8759d2037c6082c982dc5cac24c442a6fe/pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18"}, - {url = "https://files.pythonhosted.org/packages/e5/15/5ccdb37835f710819305024fb07512bf202da1a247b4ffdbdb82a6c34f7a/pydantic_core-2.14.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b"}, - {url = "https://files.pythonhosted.org/packages/e6/7c/af522a1bce278dda0f0fdc9e64a081af51cbfedeafe44cbb6a4cc8617dad/pydantic_core-2.14.5-cp311-none-win_arm64.whl", hash = "sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf"}, - {url = "https://files.pythonhosted.org/packages/e6/bc/e5cd49beafe7bf0f640bfd0a1b42e00b17b81ab072dea77c4a60cf986127/pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b"}, - {url = "https://files.pythonhosted.org/packages/eb/45/5eef8d36c2bf4c63e73e598fe523a0bc15069a97994481e27bef933ff423/pydantic_core-2.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb"}, - {url = "https://files.pythonhosted.org/packages/ed/b0/afd8f57e4ac5eaa4f1562b6f04cf10140cd6596c97d378aae2af6a236234/pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68"}, - {url = "https://files.pythonhosted.org/packages/ef/e9/ffaec12924f90d4f2f589b0f6f510b671a561b02dce47ce9fad559b41ac3/pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe"}, - {url = "https://files.pythonhosted.org/packages/f2/a4/fcb082e0723f9e4fcdbc5564879255c7f6de1f3d4d6acdd1b8799a86aa97/pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a"}, - {url = "https://files.pythonhosted.org/packages/f7/e8/d2a534d8c555f6e375296f7d534405dbc247b0da91f1c067cdca5220d95f/pydantic_core-2.14.5-cp310-none-win32.whl", hash = "sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720"}, - {url = "https://files.pythonhosted.org/packages/fb/84/f7e4556343ea0a483fa4e18505efaf10002581d2e980867a5b1ed22bfd21/pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7"}, - {url = "https://files.pythonhosted.org/packages/fd/83/65e9db6549a01a369202fadac682c1a9f5ec57a637e672554ee50ef7f625/pydantic_core-2.14.5-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf"}, -] -"pydantic-settings 2.1.0" = [ - {url = "https://files.pythonhosted.org/packages/5d/c9/8042368e9a1e6e229b5ec5d88449441a3ee8f8afe09988faeb190af30248/pydantic_settings-2.1.0-py3-none-any.whl", hash = "sha256:7621c0cb5d90d1140d2f0ef557bdf03573aac7035948109adf2574770b77605a"}, - {url = "https://files.pythonhosted.org/packages/a5/10/664e41fd884d8cd3fa8bcd75a537bd82f540b19d7c0d1ff17eef69a2ffa8/pydantic_settings-2.1.0.tar.gz", hash = "sha256:26b1492e0a24755626ac5e6d715e9077ab7ad4fb5f19a8b7ed7011d52f36141c"}, -] -"pygments 2.18.0" = [ - {url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, - {url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, -] -"pyjwt 2.9.0" = [ - {url = "https://files.pythonhosted.org/packages/79/84/0fdf9b18ba31d69877bd39c9cd6052b47f3761e9910c15de788e519f079f/PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, - {url = "https://files.pythonhosted.org/packages/fb/68/ce067f09fca4abeca8771fe667d89cc347d1e99da3e093112ac329c6020e/pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, -] -"pyproj 3.6.1" = [ - {url = "https://files.pythonhosted.org/packages/0b/64/93232511a7906a492b1b7dfdfc17f4e95982d76a24ef4f86d18cfe7ae2c9/pyproj-3.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1e9fbaf920f0f9b4ee62aab832be3ae3968f33f24e2e3f7fbb8c6728ef1d9746"}, - {url = "https://files.pythonhosted.org/packages/0e/ab/1c2159ec757677c5a6b8803f6be45c2b550dc42c84ec4a228dc219849bbb/pyproj-3.6.1-cp312-cp312-win32.whl", hash = "sha256:2d6ff73cc6dbbce3766b6c0bce70ce070193105d8de17aa2470009463682a8eb"}, - {url = "https://files.pythonhosted.org/packages/10/f2/b550b1f65cc7e51c9116b220b50aade60c439103432a3fd5b12efbc77e15/pyproj-3.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d227a865356f225591b6732430b1d1781e946893789a609bb34f59d09b8b0f8"}, - {url = "https://files.pythonhosted.org/packages/14/6d/ae373629a1723f0db80d7b8c93598b00d9ecb930ed9ebf4f35826a33e97c/pyproj-3.6.1-cp311-cp311-win32.whl", hash = "sha256:8b8acc31fb8702c54625f4d5a2a6543557bec3c28a0ef638778b7ab1d1772132"}, - {url = "https://files.pythonhosted.org/packages/18/86/2e7cb9de40492f1bafbf11f4c9072edc394509a40b5e4c52f8139546f039/pyproj-3.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bc0472302919e59114aa140fd7213c2370d848a7249d09704f10f5b062031fe"}, - {url = "https://files.pythonhosted.org/packages/19/9b/c57569132174786aa3f72275ac306956859a639dad0ce8d95c8411ce8209/pyproj-3.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb059ba3bced6f6725961ba758649261d85ed6ce670d3e3b0a26e81cf1aa8d"}, - {url = "https://files.pythonhosted.org/packages/2c/c2/8d4f61065dfed965e53badd41201ad86a05af0c1bbc75dffb12ef0f5a7dd/pyproj-3.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18faa54a3ca475bfe6255156f2f2874e9a1c8917b0004eee9f664b86ccc513d3"}, - {url = "https://files.pythonhosted.org/packages/30/bd/b9bd3761f08754e8dbb34c5a647db2099b348ab5da338e90980caf280e37/pyproj-3.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:447db19c7efad70ff161e5e46a54ab9cc2399acebb656b6ccf63e4bc4a04b97a"}, - {url = "https://files.pythonhosted.org/packages/31/38/2cf8777cb2d5622a78195e690281b7029098795fde4751aec8128238b8bb/pyproj-3.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd43bd9a9b9239805f406fd82ba6b106bf4838d9ef37c167d3ed70383943ade1"}, - {url = "https://files.pythonhosted.org/packages/43/28/e8d2ca71dd56c27cbe668e4226963d61956cded222a2e839e6fec1ab6d82/pyproj-3.6.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd93c1a0c6c4aedc77c0fe275a9f2aba4d59b8acf88cebfc19fe3c430cfabf4f"}, - {url = "https://files.pythonhosted.org/packages/43/d0/cbe29a4dcf38ee7e72bf695d0d3f2bee21b4f22ee6cf579ad974de9edfc8/pyproj-3.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:36b64c2cb6ea1cc091f329c5bd34f9c01bb5da8c8e4492c709bda6a09f96808f"}, - {url = "https://files.pythonhosted.org/packages/5e/c5/928d5a26995dbefbebd7507d982141cd9153bc7e4392b334fff722c4af12/pyproj-3.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5279586013b8d6582e22b6f9e30c49796966770389a9d5b85e25a4223286cd3f"}, - {url = "https://files.pythonhosted.org/packages/64/90/dfe5c00de1ca4dbb82606e79790659d4ed7f0ed8d372bccb3baca2a5abe0/pyproj-3.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65ad699e0c830e2b8565afe42bd58cc972b47d829b2e0e48ad9638386d994915"}, - {url = "https://files.pythonhosted.org/packages/79/95/eb68113c5b5737c342bde1bab92705dabe69c16299c5a122616e50f1fbd6/pyproj-3.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:38a3361941eb72b82bd9a18f60c78b0df8408416f9340521df442cebfc4306e2"}, - {url = "https://files.pythonhosted.org/packages/7d/84/2b39bbf888c753ea48b40d47511548c77aa03445465c35cc4c4e9649b643/pyproj-3.6.1.tar.gz", hash = "sha256:44aa7c704c2b7d8fb3d483bbf75af6cb2350d30a63b144279a09b75fead501bf"}, - {url = "https://files.pythonhosted.org/packages/84/a6/a300c1b14b2112e966e9f90b18f9c13b586bdcf417207cee913ae9005da3/pyproj-3.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ebfbdbd0936e178091309f6cd4fcb4decd9eab12aa513cdd9add89efa3ec2882"}, - {url = "https://files.pythonhosted.org/packages/89/8f/27350c8fba71a37cd0d316f100fbd96bf139cc2b5ff1ab0dcbc7ac64010a/pyproj-3.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:db3aedd458e7f7f21d8176f0a1d924f1ae06d725228302b872885a1c34f3119e"}, - {url = "https://files.pythonhosted.org/packages/8d/e8/e826e0a962f36bd925a933829cf6ef218efe2055db5ea292be40974a929d/pyproj-3.6.1-cp39-cp39-win32.whl", hash = "sha256:9274880263256f6292ff644ca92c46d96aa7e57a75c6df3f11d636ce845a1877"}, - {url = "https://files.pythonhosted.org/packages/97/0a/b1525be9680369cc06dd288e12c59d24d5798b4afcdcf1b0915836e1caa6/pyproj-3.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50100b2726a3ca946906cbaa789dd0749f213abf0cbb877e6de72ca7aa50e1ae"}, - {url = "https://files.pythonhosted.org/packages/c5/32/63cf474f4a8d4804b3bdf7c16b8589f38142e8e2f8319dcea27e0bc21a87/pyproj-3.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab7aa4d9ff3c3acf60d4b285ccec134167a948df02347585fdd934ebad8811b4"}, - {url = "https://files.pythonhosted.org/packages/c7/f3/2f32fe143cd7ba1d4d68f1b6dce9ca402d909cbd5a5830e3a8fa3d1acbbf/pyproj-3.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:7a27151ddad8e1439ba70c9b4b2b617b290c39395fa9ddb7411ebb0eb86d6fb0"}, - {url = "https://files.pythonhosted.org/packages/cb/39/1ce27cb86f51a1f5aed3a1617802a6131b59ea78492141d1fbe36722595e/pyproj-3.6.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6420ea8e7d2a88cb148b124429fba8cd2e0fae700a2d96eab7083c0928a85110"}, - {url = "https://files.pythonhosted.org/packages/d7/50/d369bbe62d7a0d1e2cb40bc211da86a3f6e0f3c99f872957a72c3d5492d6/pyproj-3.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ba1f9b03d04d8cab24d6375609070580a26ce76eaed54631f03bab00a9c737b"}, - {url = "https://files.pythonhosted.org/packages/d9/a8/7193f46032636be917bc775506ae987aad72c931b1f691b775ca812a2917/pyproj-3.6.1-cp310-cp310-win32.whl", hash = "sha256:c41e80ddee130450dcb8829af7118f1ab69eaf8169c4bf0ee8d52b72f098dc2f"}, - {url = "https://files.pythonhosted.org/packages/f4/0a/d82aeeb605b5d6870bc72307c3b5e044e632eb7720df8885e144f51a8eac/pyproj-3.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7e13c40183884ec7f94eb8e0f622f08f1d5716150b8d7a134de48c6110fee85"}, - {url = "https://files.pythonhosted.org/packages/f6/2b/b60cf73b0720abca313bfffef34e34f7f7dae23852b2853cf0368d49426b/pyproj-3.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fafd1f3eb421694857f254a9bdbacd1eb22fc6c24ca74b136679f376f97d35"}, - {url = "https://files.pythonhosted.org/packages/fe/4b/2f8f6f94643b9fe2083338eff294feda84d916409b5840b7a402d2be93f8/pyproj-3.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83039e5ae04e5afc974f7d25ee0870a80a6bd6b7957c3aca5613ccbe0d3e72bf"}, -] -"python-dotenv 1.0.0" = [ - {url = "https://files.pythonhosted.org/packages/31/06/1ef763af20d0572c032fa22882cfbfb005fba6e7300715a37840858c919e/python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, - {url = "https://files.pythonhosted.org/packages/44/2f/62ea1c8b593f4e093cc1a7768f0d46112107e790c3e478532329e434f00b/python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, -] -"python-multipart 0.0.9" = [ - {url = "https://files.pythonhosted.org/packages/3d/47/444768600d9e0ebc82f8e347775d24aef8f6348cf00e9fa0e81910814e6d/python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, - {url = "https://files.pythonhosted.org/packages/5c/0f/9c55ac6c84c0336e22a26fa84ca6c51d58d7ac3a2d78b0dfa8748826c883/python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, -] -"python-slugify 8.0.4" = [ - {url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, - {url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, -] -"pyyaml 6.0.1" = [ - {url = "https://files.pythonhosted.org/packages/02/74/b2320ebe006b6a521cf929c78f12a220b9db319b38165023623ed195654b/PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {url = "https://files.pythonhosted.org/packages/03/5c/c4671451b2f1d76ebe352c0945d4cd13500adb5d05f5a51ee296d80152f7/PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {url = "https://files.pythonhosted.org/packages/03/f7/4f8b71f3ce8cfb2c06e814aeda5b26ecc62ecb5cf85f5c8898be34e6eb6a/PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {url = "https://files.pythonhosted.org/packages/06/92/e0224aa6ebf9dc54a06a4609da37da40bb08d126f5535d81bff6b417b2ae/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {url = "https://files.pythonhosted.org/packages/07/91/45dfd0ef821a7f41d9d0136ea3608bb5b1653e42fd56a7970532cb5c003f/PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {url = "https://files.pythonhosted.org/packages/0d/46/62ae77677e532c0af6c81ddd6f3dbc16bdcc1208b077457354442d220bfb/PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {url = "https://files.pythonhosted.org/packages/0e/88/21b2f16cb2123c1e9375f2c93486e35fdc86e63f02e274f0e99c589ef153/PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {url = "https://files.pythonhosted.org/packages/1e/ae/964ccb88a938f20ece5754878f182cfbd846924930d02d29d06af8d4c69e/PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {url = "https://files.pythonhosted.org/packages/24/62/7fcc372442ec8ea331da18c24b13710e010c5073ab851ef36bf9dacb283f/PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {url = "https://files.pythonhosted.org/packages/24/97/9b59b43431f98d01806b288532da38099cc6f2fea0f3d712e21e269c0279/PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {url = "https://files.pythonhosted.org/packages/27/d5/fb4f7a3c96af89c214387af42c76117d2c2a0a40576e217632548a6e1aff/PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {url = "https://files.pythonhosted.org/packages/28/09/55f715ddbf95a054b764b547f617e22f1d5e45d83905660e9a088078fe67/PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {url = "https://files.pythonhosted.org/packages/29/0f/9782fa5b10152abf033aec56a601177ead85ee03b57781f2d9fced09eefc/PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {url = "https://files.pythonhosted.org/packages/29/61/bf33c6c85c55bc45a29eee3195848ff2d518d84735eb0e2d8cb42e0d285e/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {url = "https://files.pythonhosted.org/packages/2b/9f/fbade56564ad486809c27b322d0f7e6a89c01f6b4fe208402e90d4443a99/PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {url = "https://files.pythonhosted.org/packages/2e/97/3e0e089ee85e840f4b15bfa00e4e63d84a3691ababbfea92d6f820ea6f21/PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {url = "https://files.pythonhosted.org/packages/40/da/a175a35cf5583580e90ac3e2a3dbca90e43011593ae62ce63f79d7b28d92/PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {url = "https://files.pythonhosted.org/packages/41/9a/1c4c51f1a0d2b6fd805973701ab0ec84d5e622c5aaa573b0e1157f132809/PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {url = "https://files.pythonhosted.org/packages/4a/4b/c71ef18ef83c82f99e6da8332910692af78ea32bd1d1d76c9787dfa36aea/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {url = "https://files.pythonhosted.org/packages/4d/f1/08f06159739254c8947899c9fc901241614195db15ba8802ff142237664c/PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {url = "https://files.pythonhosted.org/packages/4f/78/77b40157b6cb5f2d3d31a3d9b2efd1ba3505371f76730d267e8b32cf4b7f/PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {url = "https://files.pythonhosted.org/packages/57/c5/5d09b66b41d549914802f482a2118d925d876dc2a35b2d127694c1345c34/PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {url = "https://files.pythonhosted.org/packages/5b/07/10033a403b23405a8fc48975444463d3d10a5c2736b7eb2550b07b367429/PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {url = "https://files.pythonhosted.org/packages/5e/94/7d5ee059dfb92ca9e62f4057dcdec9ac08a9e42679644854dc01177f8145/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {url = "https://files.pythonhosted.org/packages/62/2a/df7727c52e151f9e7b852d7d1580c37bd9e39b2f29568f0f81b29ed0abc2/PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {url = "https://files.pythonhosted.org/packages/73/9c/766e78d1efc0d1fca637a6b62cea1b4510a7fb93617eb805223294fef681/PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {url = "https://files.pythonhosted.org/packages/7b/5e/efd033ab7199a0b2044dab3b9f7a4f6670e6a52c089de572e928d2873b06/PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {url = "https://files.pythonhosted.org/packages/7d/39/472f2554a0f1e825bd7c5afc11c817cd7a2f3657460f7159f691fbb37c51/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {url = "https://files.pythonhosted.org/packages/7f/5d/2779ea035ba1e533c32ed4a249b4e0448f583ba10830b21a3cddafe11a4e/PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {url = "https://files.pythonhosted.org/packages/84/02/404de95ced348b73dd84f70e15a41843d817ff8c1744516bf78358f2ffd2/PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {url = "https://files.pythonhosted.org/packages/84/4d/82704d1ab9290b03da94e6425f5e87396b999fd7eb8e08f3a92c158402bf/PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {url = "https://files.pythonhosted.org/packages/96/06/4beb652c0fe16834032e54f0956443d4cc797fe645527acee59e7deaa0a2/PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {url = "https://files.pythonhosted.org/packages/ac/6c/967d91a8edf98d2b2b01d149bd9e51b8f9fb527c98d80ebb60c6b21d60c4/PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {url = "https://files.pythonhosted.org/packages/b3/34/65bb4b2d7908044963ebf614fe0fdb080773fc7030d7e39c8d3eddcd4257/PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {url = "https://files.pythonhosted.org/packages/b4/33/720548182ffa8344418126017aa1d4ab4aeec9a2275f04ce3f3573d8ace8/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {url = "https://files.pythonhosted.org/packages/b6/a0/b6700da5d49e9fed49dc3243d3771b598dad07abb37cc32e524607f96adc/PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {url = "https://files.pythonhosted.org/packages/ba/91/090818dfa62e85181f3ae23dd1e8b7ea7f09684864a900cab72d29c57346/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {url = "https://files.pythonhosted.org/packages/bc/06/1b305bf6aa704343be85444c9d011f626c763abb40c0edc1cad13bfd7f86/PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {url = "https://files.pythonhosted.org/packages/c1/39/47ed4d65beec9ce07267b014be85ed9c204fa373515355d3efa62d19d892/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {url = "https://files.pythonhosted.org/packages/c7/4c/4a2908632fc980da6d918b9de9c1d9d7d7e70b2672b1ad5166ed27841ef7/PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {url = "https://files.pythonhosted.org/packages/c7/d1/02baa09d39b1bb1ebaf0d850d106d1bdcb47c91958557f471153c49dc03b/PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {url = "https://files.pythonhosted.org/packages/c8/6b/6600ac24725c7388255b2f5add93f91e58a5d7efaf4af244fdbcc11a541b/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {url = "https://files.pythonhosted.org/packages/cc/5c/fcabd17918348c7db2eeeb0575705aaf3f7ab1657f6ce29b2e31737dd5d1/PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {url = "https://files.pythonhosted.org/packages/cd/e5/af35f7ea75cf72f2cd079c95ee16797de7cd71f29ea7c68ae5ce7be1eda0/PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, - {url = "https://files.pythonhosted.org/packages/d6/6a/439d1a6f834b9a9db16332ce16c4a96dd0e3970b65fe08cbecd1711eeb77/PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {url = "https://files.pythonhosted.org/packages/d7/8f/db62b0df635b9008fe90aa68424e99cee05e68b398740c8a666a98455589/PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {url = "https://files.pythonhosted.org/packages/e1/a1/27bfac14b90adaaccf8c8289f441e9f76d94795ec1e7a8f134d9f2cb3d0b/PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {url = "https://files.pythonhosted.org/packages/e5/31/ba812efa640a264dbefd258986a5e4e786230cb1ee4a9f54eb28ca01e14a/PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {url = "https://files.pythonhosted.org/packages/ec/0d/26fb23e8863e0aeaac0c64e03fd27367ad2ae3f3cccf3798ee98ce160368/PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {url = "https://files.pythonhosted.org/packages/f1/26/55e4f21db1f72eaef092015d9017c11510e7e6301c62a6cfee91295d13c6/PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {url = "https://files.pythonhosted.org/packages/fe/88/def2e57fe740544f2eefb1645f1d6e0094f56c00f4eade708140b6137ead/PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, -] -"requests 2.32.3" = [ - {url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, - {url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, -] -"requests-oauthlib 2.0.0" = [ - {url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, - {url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, -] -"shapely 2.0.2" = [ - {url = "https://files.pythonhosted.org/packages/01/c0/ef2c5eff1e8381710e211a063d0aa3e7215cea9e6fd8c31e75bf5f93df85/shapely-2.0.2.tar.gz", hash = "sha256:1713cc04c171baffc5b259ba8531c58acc2a301707b7f021d88a15ed090649e7"}, - {url = "https://files.pythonhosted.org/packages/0b/45/72b2f6c9c02e02d73edeb3e63cb724e5a67f84d1ecadb06a2309a2c022a9/shapely-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dea9a0651333cf96ef5bb2035044e3ad6a54f87d90e50fe4c2636debf1b77abc"}, - {url = "https://files.pythonhosted.org/packages/14/c8/0747225a0fa3f2b45cf9e6e5eef51f4b9ec3777f0eb2e594a6c90ff4ab53/shapely-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36480e32c434d168cdf2f5e9862c84aaf4d714a43a8465ae3ce8ff327f0affb7"}, - {url = "https://files.pythonhosted.org/packages/16/c9/1d8fa4ff1d08d4a1541c18d8cd64de85cd442aef8ad6466acad4482401a5/shapely-2.0.2-cp38-cp38-win32.whl", hash = "sha256:03e63a99dfe6bd3beb8d5f41ec2086585bb969991d603f9aeac335ad396a06d4"}, - {url = "https://files.pythonhosted.org/packages/17/d7/ec615b2285a3b31d5a3a84ba89cc8006c0df1b99854ff26697aefcca3129/shapely-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ef753200cbffd4f652efb2c528c5474e5a14341a473994d90ad0606522a46a2"}, - {url = "https://files.pythonhosted.org/packages/18/93/2f3076f374dfc4509e1a3b05ba479b68f243dee6fe5e1b9a7e2eb8bd0693/shapely-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:394e5085b49334fd5b94fa89c086edfb39c3ecab7f669e8b2a4298b9d523b3a5"}, - {url = "https://files.pythonhosted.org/packages/1c/23/678ca61d390b3ae1c1dec54085dfb83047e54986f8c3a643ac138359ecce/shapely-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d41a116fcad58048d7143ddb01285e1a8780df6dc1f56c3b1e1b7f12ed296651"}, - {url = "https://files.pythonhosted.org/packages/1c/b4/e08e2d0732dcf2b9432f1a13ee85e1150e6a2b31fbb242e2adbf50843a57/shapely-2.0.2-cp39-cp39-win32.whl", hash = "sha256:b8eb0a92f7b8c74f9d8fdd1b40d395113f59bd8132ca1348ebcc1f5aece94b96"}, - {url = "https://files.pythonhosted.org/packages/21/f3/00a7b1c28ccd3c1807a6e8c1a616aee9ae448be9da860cfcf1b107874fb1/shapely-2.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6ca8cffbe84ddde8f52b297b53f8e0687bd31141abb2c373fd8a9f032df415d6"}, - {url = "https://files.pythonhosted.org/packages/28/32/c404f6d5566025b9d94eef0cc21039b0a4c10a392bad2f81edf778b3c2bc/shapely-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:34eac2337cbd67650248761b140d2535855d21b969d76d76123317882d3a0c1a"}, - {url = "https://files.pythonhosted.org/packages/29/b7/754f971ebbcb747ef38147949f90b30d2232ee5b9af9daacd558cc1705e2/shapely-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5533a925d8e211d07636ffc2fdd9a7f9f13d54686d00577eeb11d16f00be9c4"}, - {url = "https://files.pythonhosted.org/packages/2c/b1/ca09649b4abe06366d41e90c3eee95a7741657404404a63bd0e8b53e32b8/shapely-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94ac128ae2ab4edd0bffcd4e566411ea7bdc738aeaf92c32a8a836abad725f9f"}, - {url = "https://files.pythonhosted.org/packages/2e/1b/e05169491d43178ca7602ebcb5acdba9d3865786769e659f3d3c71d029c9/shapely-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7d897e6bdc6bc64f7f65155dbbb30e49acaabbd0d9266b9b4041f87d6e52b3a"}, - {url = "https://files.pythonhosted.org/packages/35/fd/55ab3686ef8c2f53e258bda56404a444eff50d811acd324726f9782b6f8c/shapely-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:5324be299d4c533ecfcfd43424dfd12f9428fd6f12cda38a4316da001d6ef0ea"}, - {url = "https://files.pythonhosted.org/packages/48/2a/6e590f4f13b1bd8ee39626bab5c1643b8746a4e469c61c5e38571f6cccd9/shapely-2.0.2-cp311-cp311-win32.whl", hash = "sha256:45ac6906cff0765455a7b49c1670af6e230c419507c13e2f75db638c8fc6f3bd"}, - {url = "https://files.pythonhosted.org/packages/51/18/7a6dfefc08d5899dfca0cde9ada7a58006d06cccc0818e68b2a60bdba8e3/shapely-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:794affd80ca0f2c536fc948a3afa90bd8fb61ebe37fe873483ae818e7f21def4"}, - {url = "https://files.pythonhosted.org/packages/5b/51/b19feeebc5982d32b9c3dab7a55aacda38373b0cc814985aeebd95202c5e/shapely-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:baa14fc27771e180c06b499a0a7ba697c7988c7b2b6cba9a929a19a4d2762de3"}, - {url = "https://files.pythonhosted.org/packages/5c/d9/d557c09d15406aad8252caeae181c2db2c562cc1b2ca6e6482b43274b25f/shapely-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7e92e7c255f89f5cdf777690313311f422aa8ada9a3205b187113274e0135cd8"}, - {url = "https://files.pythonhosted.org/packages/77/e6/5043c8c8b7e21922559b4faa9011566b0df9315c3d51f15fa07816b4409d/shapely-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa3ee28f5e63a130ec5af4dc3c4cb9c21c5788bb13c15e89190d163b14f9fb89"}, - {url = "https://files.pythonhosted.org/packages/81/50/c7768a0a71c012464927228b6b949e55ad8d7ed50325b56b7224ea7dc7a6/shapely-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2d217e56ae067e87b4e1731d0dc62eebe887ced729ba5c2d4590e9e3e9fdbd88"}, - {url = "https://files.pythonhosted.org/packages/8c/47/05c8bb8322861113e72b903aebaaa4678ae6e44c886c189ad8fe297f2008/shapely-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:737dba15011e5a9b54a8302f1748b62daa207c9bc06f820cd0ad32a041f1c6f2"}, - {url = "https://files.pythonhosted.org/packages/8e/79/b002906cbc5f57892a75d698085172658a6d0a882ebcedf1df0205b85176/shapely-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:78128357a0cee573257a0c2c388d4b7bf13cb7dbe5b3fe5d26d45ebbe2a39e25"}, - {url = "https://files.pythonhosted.org/packages/95/9f/75b0bb894341f0714e0ca7b034bbdee318119da1756f010711083ab0eef3/shapely-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7c95d3379ae3abb74058938a9fcbc478c6b2e28d20dace38f8b5c587dde90aa"}, - {url = "https://files.pythonhosted.org/packages/99/9d/a8f71f9d9eb4ee301f500d7bfeac912aec401ceb1c7ad8c7b0399ea8c7e6/shapely-2.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ed1e99702125e7baccf401830a3b94d810d5c70b329b765fe93451fe14cf565b"}, - {url = "https://files.pythonhosted.org/packages/99/e9/a996a080d8478f4ab5ea82f64a5f39aaa8e05c99c2703e0ee03ec8c9e924/shapely-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9a41ff4323fc9d6257759c26eb1cf3a61ebc7e611e024e6091f42977303fd3a"}, - {url = "https://files.pythonhosted.org/packages/9e/39/029c441d8af32ab423b229c4525ce5ce6707318155b59634811a4c56f5c4/shapely-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:dc9342fc82e374130db86a955c3c4525bfbf315a248af8277a913f30911bed9e"}, - {url = "https://files.pythonhosted.org/packages/a0/70/8ac9c70da0e9428b8827953c7345a2f9f0e62adeccd9ca5a69425d693b8c/shapely-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b0c052709c8a257c93b0d4943b0b7a3035f87e2d6a8ac9407b6a992d206422f"}, - {url = "https://files.pythonhosted.org/packages/a4/6a/f453df473e317cc844420a003d2f558d8808404c9b7a43d3a6f806e51610/shapely-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:c6fd29fbd9cd76350bd5cc14c49de394a31770aed02d74203e23b928f3d2f1aa"}, - {url = "https://files.pythonhosted.org/packages/a7/e4/d0fdebc973ccfefe9feb8bca0995e0071c37aabb40448d9f170f94058a0b/shapely-2.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:06f193091a7c6112fc08dfd195a1e3846a64306f890b151fa8c63b3e3624202c"}, - {url = "https://files.pythonhosted.org/packages/aa/92/92e420d5be37b0cb0266fab2fe1366496a71f5966ab1108c99ad28976b8e/shapely-2.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:87dc2be34ac3a3a4a319b963c507ac06682978a5e6c93d71917618b14f13066e"}, - {url = "https://files.pythonhosted.org/packages/bc/e0/237b127f163737b3b4aa275943f4e75e56f50f0aa1c05fe997ddac6bd256/shapely-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be46d5509b9251dd9087768eaf35a71360de6afac82ce87c636990a0871aa18b"}, - {url = "https://files.pythonhosted.org/packages/c0/31/4f5e544c7d661a6007a5f003aa1824f41211ddeb10bcd5d967d300d6f30f/shapely-2.0.2-cp310-cp310-win32.whl", hash = "sha256:72b5997272ae8c25f0fd5b3b967b3237e87fab7978b8d6cd5fa748770f0c5d68"}, - {url = "https://files.pythonhosted.org/packages/c7/d7/1bf4f48d83af09ce1af06c21752670fa8cdcafa72dbc452b809b215cda2a/shapely-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:ea84d1cdbcf31e619d672b53c4532f06253894185ee7acb8ceb78f5f33cbe033"}, - {url = "https://files.pythonhosted.org/packages/cc/fe/e72acdce088d7e3b6d530909f44e003293ded02dfb663214a8df5bc2970a/shapely-2.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f217d28ecb48e593beae20a0082a95bd9898d82d14b8fcb497edf6bff9a44d7"}, - {url = "https://files.pythonhosted.org/packages/cd/77/d36919327b4c6f5a92909ea194a1a4138bf0515bf0d6a5ca29f53cd0d879/shapely-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:eebe544df5c018134f3c23b6515877f7e4cd72851f88a8d0c18464f414d141a2"}, - {url = "https://files.pythonhosted.org/packages/d2/37/36bee542cf77d8902e1a83ae8fe6372bcf64a835597a3d9993297f27ee21/shapely-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd3ad17b64466a033848c26cb5b509625c87d07dcf39a1541461cacdb8f7e91c"}, - {url = "https://files.pythonhosted.org/packages/d2/dd/9f0bc472d4a501b0ea333ecb8d6366320c59b7d3200f9ae2026165526971/shapely-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0521d76d1e8af01e712db71da9096b484f081e539d4f4a8c97342e7971d5e1b4"}, - {url = "https://files.pythonhosted.org/packages/d8/41/3db53b03a0f2da266ca9ea35a779b0c4bb34bac06eb2778d4203ce613305/shapely-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a21353d28209fb0d8cc083e08ca53c52666e0d8a1f9bbe23b6063967d89ed24"}, - {url = "https://files.pythonhosted.org/packages/e3/0e/e113e4b7b31317af964ae152fafed933c9d252ed8256444623ccab7ce9ae/shapely-2.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:42997ac806e4583dad51c80a32d38570fd9a3d4778f5e2c98f9090aa7db0fe91"}, - {url = "https://files.pythonhosted.org/packages/e6/25/b10e9de9c366453be9618032cec74bdefb298b39247aa7a93cea675dfa27/shapely-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ccfd5fa10a37e67dbafc601c1ddbcbbfef70d34c3f6b0efc866ddbdb55893a6c"}, - {url = "https://files.pythonhosted.org/packages/eb/5e/7aca7c3181f0bedbdb0931c825171b499c8a91b5aff667077efe81e75b4a/shapely-2.0.2-cp312-cp312-win32.whl", hash = "sha256:084b023dae8ad3d5b98acee9d3bf098fdf688eb0bb9b1401e8b075f6a627b611"}, -] -"six 1.16.0" = [ - {url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, - {url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, -] -"sniffio 1.3.1" = [ - {url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, - {url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, -] -"sqlalchemy 2.0.23" = [ - {url = "https://files.pythonhosted.org/packages/12/61/ee1619cea002a94e954c353c2e93a4da85cb1106a24a7c66d5794301e84e/SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac"}, - {url = "https://files.pythonhosted.org/packages/17/60/8cfa64fe57a8e9a5cca61027519e7fb44b0e9124a9cb0509708f30d38cc1/SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e"}, - {url = "https://files.pythonhosted.org/packages/1d/40/9c7169cb4dc66c0a2d4cf5ff016faab417d30a1d2deea57780b329dd4fe5/SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3"}, - {url = "https://files.pythonhosted.org/packages/29/b2/70b88e4e6dac15dd2f380e18b7c2144b4334ac1587d6a7cc031297fac96c/SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e0dc9031baa46ad0dd5a269cb7a92a73284d1309228be1d5935dac8fb3cae24"}, - {url = "https://files.pythonhosted.org/packages/2c/a4/f1978e5a12cbe7aa7043d2094b0d793287307e36e0bc351daea8b8cfa376/SQLAlchemy-2.0.23-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:14aebfe28b99f24f8a4c1346c48bc3d63705b1f919a24c27471136d2f219f02d"}, - {url = "https://files.pythonhosted.org/packages/2d/ca/1ad0106300c9cbd14e2b88bffe9f52f03ce2abe48f3b88747e520180014e/SQLAlchemy-2.0.23-cp310-cp310-win_amd64.whl", hash = "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6"}, - {url = "https://files.pythonhosted.org/packages/2f/4e/cbbb63dc6eb55138311a949ab4221e69a0aff8d95bf294fd948727392a14/SQLAlchemy-2.0.23-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45"}, - {url = "https://files.pythonhosted.org/packages/35/63/0ee6deb8409e6159ad7fccf89c9f36040666f63887f4147d716ae5b97d06/SQLAlchemy-2.0.23-cp38-cp38-win32.whl", hash = "sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306"}, - {url = "https://files.pythonhosted.org/packages/36/29/15a839fa2faf9d588afd7d0ea4ed9c323232bc682dbb683daaca3fbec0f6/SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0"}, - {url = "https://files.pythonhosted.org/packages/36/70/202467dc568aa211d234acedab908ca4e09dfbb6b8eedc91217c2e9208ac/SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda"}, - {url = "https://files.pythonhosted.org/packages/39/7f/3030a3af7f96434fd756ae31a586b23fdd0a07df0ec4a648336ade7edde3/SQLAlchemy-2.0.23-cp37-cp37m-win32.whl", hash = "sha256:f48ed89dd11c3c586f45e9eec1e437b355b3b6f6884ea4a4c3111a3358fd0c18"}, - {url = "https://files.pythonhosted.org/packages/3c/eb/66fc0910c7ca5617b7c042bb4bc345226ad015ce5fc6543f453bffb6e47b/SQLAlchemy-2.0.23-cp37-cp37m-win_amd64.whl", hash = "sha256:1e018aba8363adb0599e745af245306cb8c46b9ad0a6fc0a86745b6ff7d940fc"}, - {url = "https://files.pythonhosted.org/packages/4d/e0/d9551da0fc8c01c5a8342b7b30a7552329abf7a2a4a60411dd004e4b4501/SQLAlchemy-2.0.23-cp312-cp312-win32.whl", hash = "sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846"}, - {url = "https://files.pythonhosted.org/packages/53/30/339fd68c0200a8f70d9198ca250c2ba0e4683ba11ba42423f300a041dfc9/SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34"}, - {url = "https://files.pythonhosted.org/packages/53/ce/d7262dcc228f66c4d805c8975f71deca581e8a56b46612eb35708ec5cb51/SQLAlchemy-2.0.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea"}, - {url = "https://files.pythonhosted.org/packages/55/b6/928ed9857b9974d40741bc1cd07f569f38904d13a5fa61fc782f3245e130/SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35"}, - {url = "https://files.pythonhosted.org/packages/5a/e4/f8b9828117adb533891c4d6872a6f1a047b6049faf44db6a3c502e88acd7/SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:63bfc3acc970776036f6d1d0e65faa7473be9f3135d37a463c5eba5efcdb24c8"}, - {url = "https://files.pythonhosted.org/packages/5c/1c/6de9e2c94b61a666cf05143a66b35f4608c066175573a81acd42f96fadcc/SQLAlchemy-2.0.23-cp38-cp38-win_amd64.whl", hash = "sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b"}, - {url = "https://files.pythonhosted.org/packages/5c/b2/4914a76c35952d899dc5f4385c55fab15b718e9aff68c8104a8d193c23bd/SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d"}, - {url = "https://files.pythonhosted.org/packages/5c/e4/f2b196d8779c09c32337ff1e17d6edd57547fe674413b3a82e42a370a65c/SQLAlchemy-2.0.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4"}, - {url = "https://files.pythonhosted.org/packages/5c/e7/d363abf1882ef31e780821b27bda4750d31fd46149da6b3741e99b23f873/SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221"}, - {url = "https://files.pythonhosted.org/packages/5e/fb/3cd382bdd402154832b2142d93b0ddedcfa4c6ecb8120690e11e5086de33/SQLAlchemy-2.0.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df"}, - {url = "https://files.pythonhosted.org/packages/62/20/3a0cbfbb17b311c6323769c09ec559a2324e09871a2f66d6040bec930031/SQLAlchemy-2.0.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55"}, - {url = "https://files.pythonhosted.org/packages/62/7f/0155fe62b6054c0642bcf187088d9faaa66c1d057ee2c0038a2a673ca2ae/SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204"}, - {url = "https://files.pythonhosted.org/packages/67/e7/7c77fd5290646f929b499992607cf1bc940573098a593080fcc8f7e13a08/SQLAlchemy-2.0.23-cp311-cp311-win_amd64.whl", hash = "sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22"}, - {url = "https://files.pythonhosted.org/packages/7c/1f/d34a789e188c754560f54893f9a559c432af3b3ef6d9401d406f10d6c1ab/SQLAlchemy-2.0.23-cp39-cp39-win_amd64.whl", hash = "sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b"}, - {url = "https://files.pythonhosted.org/packages/90/00/f588b08bb2ce214df96e7a045ddf9ffafab6bfac75e6c75f0a50fb9f8d2a/SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d"}, - {url = "https://files.pythonhosted.org/packages/90/63/981fb1f20f1705e0bd31153a68d0fdf59a3ae6e41baa8b410a9f0d5aa901/SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4"}, - {url = "https://files.pythonhosted.org/packages/97/9b/bb6a4c80c2e62719330fcdb57f024329fb2a2ce17664ddabcc74d2cda812/SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab"}, - {url = "https://files.pythonhosted.org/packages/9b/41/e92e465f91aeeea7974a77549edc6736544cf881448ddb4b133903d008fe/SQLAlchemy-2.0.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74"}, - {url = "https://files.pythonhosted.org/packages/9c/af/c61d98f6c21f35b13f22259e38b47669afe960a348e2c01ce262f183dcbe/SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577"}, - {url = "https://files.pythonhosted.org/packages/a4/90/9bc93131211b628e4339700713647aeecbc194e66ac6de869711c631b0cd/SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4"}, - {url = "https://files.pythonhosted.org/packages/a6/da/49d4f9cd352259fa29c5110b78458c5046058b4fe27caf8644ebf11d8df7/SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5"}, - {url = "https://files.pythonhosted.org/packages/a9/a3/9afc2bf14c5892640c15d050bd9c9bfefead29cb041560734dff13bf0890/SQLAlchemy-2.0.23-py3-none-any.whl", hash = "sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d"}, - {url = "https://files.pythonhosted.org/packages/aa/1c/0b66318368b1c9ef51c5c8560530b8ef842164e10eea08cacb06739388e0/SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab"}, - {url = "https://files.pythonhosted.org/packages/ac/40/efe4320de1de9af3b6e538f89e2f1d56f83209fddbec77bafa06ee4f42c6/SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965"}, - {url = "https://files.pythonhosted.org/packages/b7/a8/4495f521f8ac6bde513436867755788c9efe632cb2a2e9d5add880e889bb/SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60"}, - {url = "https://files.pythonhosted.org/packages/be/67/7c054e93e1cca2d04ed69548ebf21134ace9c74efd008f936aa371a001be/SQLAlchemy-2.0.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1"}, - {url = "https://files.pythonhosted.org/packages/c3/3c/a79b9541de3eb2efeaa785b2f11acbcf6e16cc118c2791aa27ed23a448f8/SQLAlchemy-2.0.23-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693"}, - {url = "https://files.pythonhosted.org/packages/c3/b4/93e0a6743ae3657ce3681d5f19ba5b9cde918f3489f47818d60615320abc/SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e983fa42164577d073778d06d2cc5d020322425a509a08119bdcee70ad856bf"}, - {url = "https://files.pythonhosted.org/packages/c7/55/d1d2ad054fb7e9188681d56df40ed81c2c198314a805b180b0ec99019da1/SQLAlchemy-2.0.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72"}, - {url = "https://files.pythonhosted.org/packages/d0/35/baf485da71f15b37d8d8e926d471490ed7b9c0ce1bd82da58f7d46509107/SQLAlchemy-2.0.23-cp311-cp311-win32.whl", hash = "sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855"}, - {url = "https://files.pythonhosted.org/packages/d3/8a/321205f6ab88307618650f916f7c04f51864cd716c9583a25230ace70dc3/SQLAlchemy-2.0.23-cp312-cp312-win_amd64.whl", hash = "sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca"}, - {url = "https://files.pythonhosted.org/packages/da/9b/ba3591423ad1d62f0f98132040aba67ae460073ecbfa956df63014f89580/SQLAlchemy-2.0.23-cp39-cp39-win32.whl", hash = "sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884"}, - {url = "https://files.pythonhosted.org/packages/e0/32/6e168c00cd90037d37d6ce238fc2b18ea6c9591d1ca42a7cfdce8139675b/SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5f94aeb99f43729960638e7468d4688f6efccb837a858b34574e01143cf11f89"}, - {url = "https://files.pythonhosted.org/packages/e7/25/cfcc50c21cb133ae44f9aba61b48285451b6ecb882af291fe9da6445f4da/SQLAlchemy-2.0.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d"}, - {url = "https://files.pythonhosted.org/packages/ed/9b/002a02bae5b464d557197e5a241bdc5d88d1aa6ef9373841fee61019243d/SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab"}, - {url = "https://files.pythonhosted.org/packages/ee/46/a3196db7ffd2609c7935798730e21bed8806d9bf4401921587dac4be0747/SQLAlchemy-2.0.23.tar.gz", hash = "sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69"}, - {url = "https://files.pythonhosted.org/packages/f2/e0/fa52d06ed2ad85d6f3c61133559c7bc174e7dc6d3987a633ae9bbbd716bc/SQLAlchemy-2.0.23-cp310-cp310-win32.whl", hash = "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8"}, -] -"sqlalchemy-utils 0.41.1" = [ - {url = "https://files.pythonhosted.org/packages/73/d8/3863fdfe6b27f6c0dffc650aaa2929f313b33aea615b102279fd46ab550b/SQLAlchemy_Utils-0.41.1-py3-none-any.whl", hash = "sha256:6c96b0768ea3f15c0dc56b363d386138c562752b84f647fb8d31a2223aaab801"}, - {url = "https://files.pythonhosted.org/packages/a3/e0/6906a8a9b8e9deb82923e02e2c1f750c567d69a34f6e1fe566792494a682/SQLAlchemy-Utils-0.41.1.tar.gz", hash = "sha256:a2181bff01eeb84479e38571d2c0718eb52042f9afd8c194d0d02877e84b7d74"}, -] -"stack-data 0.6.3" = [ - {url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, - {url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, -] -"starlette 0.27.0" = [ - {url = "https://files.pythonhosted.org/packages/06/68/559bed5484e746f1ab2ebbe22312f2c25ec62e4b534916d41a8c21147bf8/starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, - {url = "https://files.pythonhosted.org/packages/58/f8/e2cca22387965584a409795913b774235752be4176d276714e15e1a58884/starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, -] -"text-unidecode 1.3" = [ - {url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, - {url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, -] -"traitlets 5.14.3" = [ - {url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, - {url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, -] -"typing-extensions 4.12.2" = [ - {url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] -"urllib3 2.2.2" = [ - {url = "https://files.pythonhosted.org/packages/43/6d/fa469ae21497ddc8bc93e5877702dca7cb8f911e337aca7452b5724f1bb6/urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, - {url = "https://files.pythonhosted.org/packages/ca/1c/89ffc63a9605b583d5df2be791a27bc1a42b7c32bab68d3c8f2f73a98cd4/urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, -] -"uvicorn 0.24.0" = [ - {url = "https://files.pythonhosted.org/packages/af/c9/dc0b3b6f944271d5f71564c2db08a1879a384cda7100f6f2f71b4ec9b751/uvicorn-0.24.0.tar.gz", hash = "sha256:368d5d81520a51be96431845169c225d771c9dd22a58613e1a181e6c4512ac33"}, - {url = "https://files.pythonhosted.org/packages/ed/0c/a9b90a856bbdd75bf71a1dd191af1e9c9ac8a272ed337f7200950c3d3dd4/uvicorn-0.24.0-py3-none-any.whl", hash = "sha256:3d19f13dfd2c2af1bfe34dd0f7155118ce689425fdf931177abe832ca44b8a04"}, -] -"wcwidth 0.2.13" = [ - {url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, - {url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, -] -"werkzeug 3.0.1" = [ - {url = "https://files.pythonhosted.org/packages/0d/cc/ff1904eb5eb4b455e442834dabf9427331ac0fa02853bf83db817a7dd53d/werkzeug-3.0.1.tar.gz", hash = "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc"}, - {url = "https://files.pythonhosted.org/packages/c3/fc/254c3e9b5feb89ff5b9076a23218dafbc99c96ac5941e900b71206e6313b/werkzeug-3.0.1-py3-none-any.whl", hash = "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10"}, -] -"win32-setctime 1.1.0" = [ - {url = "https://files.pythonhosted.org/packages/0a/e6/a7d828fef907843b2a5773ebff47fb79ac0c1c88d60c0ca9530ee941e248/win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, - {url = "https://files.pythonhosted.org/packages/6b/dd/f95a13d2b235a28d613ba23ebad55191514550debb968b46aab99f2e3a30/win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, ] diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index 26943fc5..4e257555 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -9,32 +9,23 @@ dependencies = [ "fastapi==0.104.1", "geojson==3.1.0", "uvicorn==0.24.0", + "python-multipart>=0.0.9", "pydantic==2.5.2", "pydantic-settings==2.1.0", "geojson-pydantic==1.0.1", - "python-dotenv==1.0.0", - "ipython==8.14.0", - "werkzeug==3.0.1", "shapely==2.0.2", "sqlalchemy==2.0.23", - "SQLAlchemy-Utils==0.41.1", "geoalchemy2==0.14.2", - "psycopg2>=2.9.9", + "psycopg[pool]>=3.2.1", "requests>=2.32.3", + "requests-oauthlib>=2.0.0", "loguru>=0.7.2", - "python-multipart>=0.0.9", "fmtm-splitter==1.2.2", "minio>=7.2.7", "pyjwt>=2.8.0", "passlib[bcrypt]==1.7.4", - "bcrypt==4.0.1", - "email-validator>=2.1.1", "alembic>=1.13.1", - "psycopg2-binary>=2.9.9", - "requests-oauthlib>=2.0.0", "itsdangerous>=2.2.0", - "databases>=0.9.0", - "asyncpg>=0.29.0", "Jinja2>=3.1.4", "numpy==1.26.4", "GDAL==3.6.2", diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt deleted file mode 100644 index 4ea8bd3d..00000000 --- a/src/backend/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -fastapi==0.111.0 -geojson==3.1.0 -uvicorn==0.22.0 -pydantic==2.5.2 -pydantic-settings==2.1.0 -geojson-pydantic==1.0.1 -python-dotenv==1.0.0 -ipython==8.14.0 -werkzeug==3.0.1 -python-jose[cryptography]==3.3.0 -shapely==2.0.2 From 1216d1ca61c444d7263d252cb73ca862d6a0d111 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 6 Aug 2024 13:54:21 +0100 Subject: [PATCH 02/66] fix(backend): attach lifespan to fastapi for startup events --- .gitignore | 1 - src/backend/app/main.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8666acff..dac6e97d 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,6 @@ db.sqlite3 # ignore python environments venv -fmtm-env # project related temp_webmaps/local_only diff --git a/src/backend/app/main.py b/src/backend/app/main.py index 1e8b0081..4cd20a3d 100644 --- a/src/backend/app/main.py +++ b/src/backend/app/main.py @@ -81,6 +81,7 @@ def get_application() -> FastAPI: docs_url="/api/docs", openapi_url="/api/openapi.json", redoc_url="/api/redoc", + lifespan=lifespan, ) # Set custom logger From 3ac8a5cbdf6528fd375a98f79fb044296d5c043c Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 6 Aug 2024 13:55:28 +0100 Subject: [PATCH 03/66] refactor: use psycopg connection pool --- src/backend/app/db/database.py | 81 +++++++++++++++++++++++++--------- src/backend/app/main.py | 11 +++-- 2 files changed, 69 insertions(+), 23 deletions(-) diff --git a/src/backend/app/db/database.py b/src/backend/app/db/database.py index fa7cce69..f73374bd 100644 --- a/src/backend/app/db/database.py +++ b/src/backend/app/db/database.py @@ -1,32 +1,73 @@ """Config for the DTM database connection.""" -from databases import Database +from typing import AsyncGenerator +from fastapi import Request +from psycopg import Connection +from psycopg_pool import AsyncConnectionPool from app.config import settings -class DatabaseConnection: - """Manages database connection (sqlalchemy & encode databases)""" +async def get_db_connection_pool() -> AsyncConnectionPool: + """Get the connection pool for psycopg.""" + return AsyncConnectionPool(conninfo=settings.DTM_DB_URL.unicode_string()) - def __init__(self): - self.database = Database( - settings.DTM_DB_URL.unicode_string(), - min_size=5, - max_size=20, - ) - async def connect(self): - """Connect to the database.""" - await self.database.connect() +async def get_db(request: Request) -> AsyncGenerator[Connection, None]: + """Get a connection from the psycopg pool. - async def disconnect(self): - """Disconnect from the database.""" - await self.database.disconnect() + Info on connections vs cursors: + https://www.psycopg.org/psycopg3/docs/advanced/async.html + Here we are getting a connection from the pool, which will be returned + after the session ends / endpoint finishes processing. -db_connection = DatabaseConnection() + In summary: + - Connection is created on endpoint call. + - Cursors are used to execute commands throughout endpoint. + Note it is possible to create multiple cursors from the connection, + but all will be executed in the same db 'transaction'. + - Connection is closed on endpoint finish. + ----------------------------------- + To use the connection in endpoints: + ----------------------------------- -async def get_db(): - """Get the encode database connection""" - await db_connection.connect() - yield db_connection.database + @app.get("/something/") + async def do_stuff(db = Depends(get_db)): + async with db.cursor() as cursor: + await cursor.execute("SELECT * FROM items") + result = await cursor.fetchall() + return result + + ----------------------------------- + Additionally, the connection could be passed through to a function to + utilise the Pydantic model serialisation on the cursor: + ----------------------------------- + + from psycopg.rows import class_row + async def get_user_by_id(db: Connection, id: int): + async with conn.cursor(row_factory=class_row(User)) as cur: + await cur.execute( + ''' + SELECT id, first_name, last_name, dob + FROM (VALUES + (1, 'John', 'Doe', '2000-01-01'::date), + (2, 'Jane', 'White', NULL) + ) AS data (id, first_name, last_name, dob) + WHERE id = %(id)s; + ''', + {"id": id}, + ) + obj = await cur.fetchone() + + # reveal_type(obj) would return 'Optional[User]' here + + if not obj: + raise KeyError(f"user {id} not found") + + # reveal_type(obj) would return 'User' here + + return obj + """ + async with request.app.state.db_pool.connection() as conn: + yield conn diff --git a/src/backend/app/main.py b/src/backend/app/main.py index 4cd20a3d..0a6a2c6d 100644 --- a/src/backend/app/main.py +++ b/src/backend/app/main.py @@ -14,7 +14,7 @@ from app.waypoints import waypoint_routes from app.users import user_routes from app.tasks import task_routes -from app.db.database import db_connection +from app.db.database import get_db_connection_pool root = os.path.dirname(os.path.abspath(__file__)) @@ -110,13 +110,18 @@ async def lifespan( ): """FastAPI startup/shutdown event.""" log.debug("Starting up FastAPI server.") - await db_connection.connect() + + db_pool = await get_db_connection_pool() + await db_pool.open() + # Create a pooled db connection and make available in app state + # NOTE we can access 'request.app.state.db_pool' in endpoints + app.state.db_pool = db_pool yield # Shutdown events log.debug("Shutting down FastAPI server.") - await db_connection.disconnect() + await app.state.db_pool.close() api = get_application() From e6b0a3ceb134521e9af2e55ab0863ecd314a3b52 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 6 Aug 2024 13:55:56 +0100 Subject: [PATCH 04/66] build: correctly use alembic binary directly (no pdm required) --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index c32cd5da..5c2bf479 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -100,5 +100,5 @@ services: - .env networks: - dtm-network - entrypoint: ["pdm", "run", "alembic", "upgrade", "head"] + entrypoint: ["alembic", "upgrade", "head"] restart: "no" From ca53e51d9d4f6a0db9199e898de04d1c2d8cbf62 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 6 Aug 2024 13:56:13 +0100 Subject: [PATCH 05/66] build: bind port for local db, add initdb args for locale --- docker-compose.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5c2bf479..7a7dda17 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -48,7 +48,9 @@ services: env_file: .env environment: LANG: en-GB.utf8 - # POSTGRES_INITDB_ARGS: "--locale-provider=icu --icu-locale=en-GB" + POSTGRES_INITDB_ARGS: "--locale-provider=icu --icu-locale=en-GB" + ports: + - "5467:5432" networks: - dtm-network restart: unless-stopped From cf150d483f9327495bbbf41318d2da2b1fe23e18 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 6 Aug 2024 14:03:25 +0100 Subject: [PATCH 06/66] build: update fastapi, pydantic, add email-validator as pydantic extras --- src/backend/pdm.lock | 201 ++++++++++++++++++++++--------------- src/backend/pyproject.toml | 12 +-- 2 files changed, 124 insertions(+), 89 deletions(-) diff --git a/src/backend/pdm.lock b/src/backend/pdm.lock index ec66c351..7701a090 100644 --- a/src/backend/pdm.lock +++ b/src/backend/pdm.lock @@ -5,7 +5,7 @@ groups = ["default"] strategy = ["cross_platform"] lock_version = "4.4.1" -content_hash = "sha256:ac15441fa1ce71b998872b835deef9897bb7dfaa906832d70c7aa32180555ccc" +content_hash = "sha256:b61c15503db44e214cd5a882cf2a2d08fac2a5ac55befd4692ba60fc4f7c9444" [[package]] name = "aiosmtplib" @@ -236,6 +236,16 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "dnspython" +version = "2.6.1" +requires_python = ">=3.8" +summary = "DNS toolkit" +files = [ + {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, + {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, +] + [[package]] name = "drone-flightplan" version = "0.2.1" @@ -251,20 +261,33 @@ files = [ {file = "drone_flightplan-0.2.1.tar.gz", hash = "sha256:e937961a5ac226603d374fe7b651cc7ebcb931dc35530ea8180b73fc77ac4a14"}, ] +[[package]] +name = "email-validator" +version = "2.2.0" +requires_python = ">=3.8" +summary = "A robust email address syntax and deliverability validation library." +dependencies = [ + "dnspython>=2.0.0", + "idna>=2.0.0", +] +files = [ + {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, + {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, +] + [[package]] name = "fastapi" -version = "0.104.1" +version = "0.112.0" requires_python = ">=3.8" summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" dependencies = [ - "anyio<4.0.0,>=3.7.1", "pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4", - "starlette<0.28.0,>=0.27.0", + "starlette<0.38.0,>=0.37.2", "typing-extensions>=4.8.0", ] files = [ - {file = "fastapi-0.104.1-py3-none-any.whl", hash = "sha256:752dc31160cdbd0436bb93bad51560b57e525cbb1d4bbf6f4904ceee75548241"}, - {file = "fastapi-0.104.1.tar.gz", hash = "sha256:e5e4540a7c5e1dcfbbcf5b903c234feddcdcd881f191977a1c5dfd917487e7ae"}, + {file = "fastapi-0.112.0-py3-none-any.whl", hash = "sha256:3487ded9778006a45834b8c816ec4a48d522e2631ca9e75ec5a774f1b052f821"}, + {file = "fastapi-0.112.0.tar.gz", hash = "sha256:d262bc56b7d101d1f4e8fc0ad2ac75bb9935fec504d2b7117686cec50710cf05"}, ] [[package]] @@ -695,99 +718,111 @@ files = [ [[package]] name = "pydantic" -version = "2.5.2" -requires_python = ">=3.7" +version = "2.8.2" +requires_python = ">=3.8" summary = "Data validation using Python type hints" dependencies = [ "annotated-types>=0.4.0", - "pydantic-core==2.14.5", - "typing-extensions>=4.6.1", + "pydantic-core==2.20.1", + "typing-extensions>=4.12.2; python_version >= \"3.13\"", + "typing-extensions>=4.6.1; python_version < \"3.13\"", ] files = [ - {file = "pydantic-2.5.2-py3-none-any.whl", hash = "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0"}, - {file = "pydantic-2.5.2.tar.gz", hash = "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd"}, + {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, + {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, ] [[package]] name = "pydantic-core" -version = "2.14.5" -requires_python = ">=3.7" -summary = "" +version = "2.20.1" +requires_python = ">=3.8" +summary = "Core functionality for Pydantic validation and serialization" dependencies = [ "typing-extensions!=4.7.0,>=4.6.0", ] files = [ - {file = "pydantic_core-2.14.5-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459"}, - {file = "pydantic_core-2.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4"}, - {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada"}, - {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda"}, - {file = "pydantic_core-2.14.5-cp311-none-win32.whl", hash = "sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651"}, - {file = "pydantic_core-2.14.5-cp311-none-win_amd64.whl", hash = "sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077"}, - {file = "pydantic_core-2.14.5-cp311-none-win_arm64.whl", hash = "sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf"}, - {file = "pydantic_core-2.14.5-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093"}, - {file = "pydantic_core-2.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e"}, - {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69"}, - {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d"}, - {file = "pydantic_core-2.14.5-cp312-none-win32.whl", hash = "sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260"}, - {file = "pydantic_core-2.14.5-cp312-none-win_amd64.whl", hash = "sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36"}, - {file = "pydantic_core-2.14.5-cp312-none-win_arm64.whl", hash = "sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3"}, - {file = "pydantic_core-2.14.5.tar.gz", hash = "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, + {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, + {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, + {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, + {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, + {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, + {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, + {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, ] [[package]] name = "pydantic-settings" -version = "2.1.0" +version = "2.4.0" requires_python = ">=3.8" summary = "Settings management using Pydantic" dependencies = [ - "pydantic>=2.3.0", + "pydantic>=2.7.0", "python-dotenv>=0.21.0", ] files = [ - {file = "pydantic_settings-2.1.0-py3-none-any.whl", hash = "sha256:7621c0cb5d90d1140d2f0ef557bdf03573aac7035948109adf2574770b77605a"}, - {file = "pydantic_settings-2.1.0.tar.gz", hash = "sha256:26b1492e0a24755626ac5e6d715e9077ab7ad4fb5f19a8b7ed7011d52f36141c"}, + {file = "pydantic_settings-2.4.0-py3-none-any.whl", hash = "sha256:bb6849dc067f1687574c12a639e231f3a6feeed0a12d710c1382045c5db1c315"}, + {file = "pydantic_settings-2.4.0.tar.gz", hash = "sha256:ed81c3a0f46392b4d7c0a565c05884e6e54b3456e6f0fe4d8814981172dc9a88"}, +] + +[[package]] +name = "pydantic" +version = "2.8.2" +extras = ["email"] +requires_python = ">=3.8" +summary = "Data validation using Python type hints" +dependencies = [ + "email-validator>=2.0.0", + "pydantic==2.8.2", +] +files = [ + {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, + {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, ] [[package]] @@ -994,15 +1029,15 @@ files = [ [[package]] name = "starlette" -version = "0.27.0" -requires_python = ">=3.7" +version = "0.37.2" +requires_python = ">=3.8" summary = "The little ASGI library that shines." dependencies = [ "anyio<5,>=3.4.0", ] files = [ - {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, - {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, + {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, + {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, ] [[package]] @@ -1046,7 +1081,7 @@ files = [ [[package]] name = "uvicorn" -version = "0.24.0" +version = "0.30.5" requires_python = ">=3.8" summary = "The lightning-fast ASGI server." dependencies = [ @@ -1054,8 +1089,8 @@ dependencies = [ "h11>=0.8", ] files = [ - {file = "uvicorn-0.24.0-py3-none-any.whl", hash = "sha256:3d19f13dfd2c2af1bfe34dd0f7155118ce689425fdf931177abe832ca44b8a04"}, - {file = "uvicorn-0.24.0.tar.gz", hash = "sha256:368d5d81520a51be96431845169c225d771c9dd22a58613e1a181e6c4512ac33"}, + {file = "uvicorn-0.30.5-py3-none-any.whl", hash = "sha256:b2d86de274726e9878188fa07576c9ceeff90a839e2b6e25c917fe05f5a6c835"}, + {file = "uvicorn-0.30.5.tar.gz", hash = "sha256:ac6fdbd4425c5fd17a9fe39daf4d4d075da6fdc80f653e5894cdc2fd98752bee"}, ] [[package]] diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index 4e257555..ccff3590 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -6,14 +6,14 @@ authors = [ {name = "HOTOSM", email = "sysadmin@hotosm.org"}, ] dependencies = [ - "fastapi==0.104.1", + "fastapi==0.112.0", "geojson==3.1.0", - "uvicorn==0.24.0", + "uvicorn==0.30.5", "python-multipart>=0.0.9", - "pydantic==2.5.2", - "pydantic-settings==2.1.0", - "geojson-pydantic==1.0.1", - "shapely==2.0.2", + "pydantic[email]>=2.8.2", + "pydantic-settings==2.4.0", + "geojson-pydantic==1.1.0", + "shapely==2.0.5", "sqlalchemy==2.0.23", "geoalchemy2==0.14.2", "psycopg[pool]>=3.2.1", From 65fdcc87bf91a2b7f10a83547ccda3bc1c6e44f7 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 6 Aug 2024 14:50:24 +0100 Subject: [PATCH 07/66] build: update drone-flight plan --> v0.2.3 --- src/backend/pdm.lock | 56 ++++++++++++++++++-------------------- src/backend/pyproject.toml | 2 +- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/src/backend/pdm.lock b/src/backend/pdm.lock index 7701a090..4a6709d2 100644 --- a/src/backend/pdm.lock +++ b/src/backend/pdm.lock @@ -5,7 +5,7 @@ groups = ["default"] strategy = ["cross_platform"] lock_version = "4.4.1" -content_hash = "sha256:b61c15503db44e214cd5a882cf2a2d08fac2a5ac55befd4692ba60fc4f7c9444" +content_hash = "sha256:6d0fbebe18e8fa30f6585b6a216ad32720ee4e61cb689f83bb0f4fb2f695b98f" [[package]] name = "aiosmtplib" @@ -248,17 +248,17 @@ files = [ [[package]] name = "drone-flightplan" -version = "0.2.1" +version = "0.2.3" requires_python = ">=3.10" summary = "Generates an optimized flight plan for drones to conduct precise and efficient aerial mapping" dependencies = [ - "geojson==3.1.0", - "pyproj>=3.6.1", - "shapely==2.0.2", + "geojson>=3.0.0", + "pyproj>=3.0.0", + "shapely>=2.0.0", ] files = [ - {file = "drone_flightplan-0.2.1-py3-none-any.whl", hash = "sha256:e5257eb43d706fe1d44a08a75476ed511d1bd5f5bc27d4c5129158f45d504a4d"}, - {file = "drone_flightplan-0.2.1.tar.gz", hash = "sha256:e937961a5ac226603d374fe7b651cc7ebcb931dc35530ea8180b73fc77ac4a14"}, + {file = "drone_flightplan-0.2.3-py3-none-any.whl", hash = "sha256:01cb4aea8c6c523b71dd4d359a31c0784d06735044016eabe2c4e805ec5e0d41"}, + {file = "drone_flightplan-0.2.3.tar.gz", hash = "sha256:1fbd981490fe4ceb9fe91f8f8782c852ab12404ca5e486a3b078236e5ccafc3d"}, ] [[package]] @@ -350,15 +350,15 @@ files = [ [[package]] name = "geojson-pydantic" -version = "1.0.1" +version = "1.1.0" requires_python = ">=3.8" summary = "Pydantic data models for the GeoJSON spec." dependencies = [ "pydantic~=2.0", ] files = [ - {file = "geojson_pydantic-1.0.1-py3-none-any.whl", hash = "sha256:da8c15f15a0a9fc3e0af0253f0c2bb8a948f95ece9a0356f43d4738fa2be5107"}, - {file = "geojson_pydantic-1.0.1.tar.gz", hash = "sha256:a996ffccd5a016d3acb4a0c6aac941d2c569e3c6163d5ce6a04b61ee131c8f94"}, + {file = "geojson_pydantic-1.1.0-py3-none-any.whl", hash = "sha256:0de723719d66e585123db10ead99dfa51aff8cec08be512646df25224ac425f4"}, + {file = "geojson_pydantic-1.1.0.tar.gz", hash = "sha256:b214dc746f1e085641e32f0aaa47e0bfa67eefa2cf60a516326c68b87808e2ae"}, ] [[package]] @@ -950,28 +950,26 @@ files = [ [[package]] name = "shapely" -version = "2.0.2" +version = "2.0.5" requires_python = ">=3.7" summary = "Manipulation and analysis of geometric objects" dependencies = [ - "numpy>=1.14", -] -files = [ - {file = "shapely-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b0c052709c8a257c93b0d4943b0b7a3035f87e2d6a8ac9407b6a992d206422f"}, - {file = "shapely-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2d217e56ae067e87b4e1731d0dc62eebe887ced729ba5c2d4590e9e3e9fdbd88"}, - {file = "shapely-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94ac128ae2ab4edd0bffcd4e566411ea7bdc738aeaf92c32a8a836abad725f9f"}, - {file = "shapely-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa3ee28f5e63a130ec5af4dc3c4cb9c21c5788bb13c15e89190d163b14f9fb89"}, - {file = "shapely-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:737dba15011e5a9b54a8302f1748b62daa207c9bc06f820cd0ad32a041f1c6f2"}, - {file = "shapely-2.0.2-cp311-cp311-win32.whl", hash = "sha256:45ac6906cff0765455a7b49c1670af6e230c419507c13e2f75db638c8fc6f3bd"}, - {file = "shapely-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:dc9342fc82e374130db86a955c3c4525bfbf315a248af8277a913f30911bed9e"}, - {file = "shapely-2.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:06f193091a7c6112fc08dfd195a1e3846a64306f890b151fa8c63b3e3624202c"}, - {file = "shapely-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:eebe544df5c018134f3c23b6515877f7e4cd72851f88a8d0c18464f414d141a2"}, - {file = "shapely-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7e92e7c255f89f5cdf777690313311f422aa8ada9a3205b187113274e0135cd8"}, - {file = "shapely-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be46d5509b9251dd9087768eaf35a71360de6afac82ce87c636990a0871aa18b"}, - {file = "shapely-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5533a925d8e211d07636ffc2fdd9a7f9f13d54686d00577eeb11d16f00be9c4"}, - {file = "shapely-2.0.2-cp312-cp312-win32.whl", hash = "sha256:084b023dae8ad3d5b98acee9d3bf098fdf688eb0bb9b1401e8b075f6a627b611"}, - {file = "shapely-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:ea84d1cdbcf31e619d672b53c4532f06253894185ee7acb8ceb78f5f33cbe033"}, - {file = "shapely-2.0.2.tar.gz", hash = "sha256:1713cc04c171baffc5b259ba8531c58acc2a301707b7f021d88a15ed090649e7"}, + "numpy<3,>=1.14", +] +files = [ + {file = "shapely-2.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5bbfb048a74cf273db9091ff3155d373020852805a37dfc846ab71dde4be93ec"}, + {file = "shapely-2.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93be600cbe2fbaa86c8eb70656369f2f7104cd231f0d6585c7d0aa555d6878b8"}, + {file = "shapely-2.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f8e71bb9a46814019f6644c4e2560a09d44b80100e46e371578f35eaaa9da1c"}, + {file = "shapely-2.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5251c28a29012e92de01d2e84f11637eb1d48184ee8f22e2df6c8c578d26760"}, + {file = "shapely-2.0.5-cp311-cp311-win32.whl", hash = "sha256:35110e80070d664781ec7955c7de557456b25727a0257b354830abb759bf8311"}, + {file = "shapely-2.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c6b78c0007a34ce7144f98b7418800e0a6a5d9a762f2244b00ea560525290c9"}, + {file = "shapely-2.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:03bd7b5fa5deb44795cc0a503999d10ae9d8a22df54ae8d4a4cd2e8a93466195"}, + {file = "shapely-2.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ff9521991ed9e201c2e923da014e766c1aa04771bc93e6fe97c27dcf0d40ace"}, + {file = "shapely-2.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b65365cfbf657604e50d15161ffcc68de5cdb22a601bbf7823540ab4918a98d"}, + {file = "shapely-2.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21f64e647a025b61b19585d2247137b3a38a35314ea68c66aaf507a1c03ef6fe"}, + {file = "shapely-2.0.5-cp312-cp312-win32.whl", hash = "sha256:3ac7dc1350700c139c956b03d9c3df49a5b34aaf91d024d1510a09717ea39199"}, + {file = "shapely-2.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:30e8737983c9d954cd17feb49eb169f02f1da49e24e5171122cf2c2b62d65c95"}, + {file = "shapely-2.0.5.tar.gz", hash = "sha256:bff2366bc786bfa6cb353d6b47d0443c570c32776612e527ee47b6df63fcfe32"}, ] [[package]] diff --git a/src/backend/pyproject.toml b/src/backend/pyproject.toml index ccff3590..707dba8c 100644 --- a/src/backend/pyproject.toml +++ b/src/backend/pyproject.toml @@ -31,7 +31,7 @@ dependencies = [ "GDAL==3.6.2", "aiosmtplib>=3.0.1", "python-slugify>=8.0.4", - "drone-flightplan>=0.2.1", + "drone-flightplan>=0.2.3", ] requires-python = ">=3.11" license = {text = "GPL-3.0-only"} From dab56a874e4039134f3ba64e9b9d6351e5f09477 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Tue, 6 Aug 2024 23:47:25 +0100 Subject: [PATCH 08/66] fix: log level using text level instead of number levels --- src/backend/app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/app/main.py b/src/backend/app/main.py index 0a6a2c6d..e73eb9bb 100644 --- a/src/backend/app/main.py +++ b/src/backend/app/main.py @@ -30,7 +30,7 @@ def emit(self, record): This happens to be in the 6th frame upward. """ logger_opt = log.opt(depth=6, exception=record.exc_info) - logger_opt.log(record.levelno, record.getMessage()) + logger_opt.log(logging.getLevelName(record.levelno), record.getMessage()) def get_logger(): From 9412c7d3853a6511563a6c115569ce4ed04031b3 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Wed, 7 Aug 2024 00:49:43 +0100 Subject: [PATCH 09/66] refactor: update geojson parsing logic, add slugify to utils --- src/backend/app/utils.py | 145 +++++++++++++++++++++------------------ 1 file changed, 79 insertions(+), 66 deletions(-) diff --git a/src/backend/app/utils.py b/src/backend/app/utils.py index a93d5edf..52e8fa8b 100644 --- a/src/backend/app/utils.py +++ b/src/backend/app/utils.py @@ -2,6 +2,7 @@ import geojson import requests import shapely +import json from datetime import datetime, timezone from typing import Optional, Union, Any from geojson_pydantic import Feature, MultiPolygon, Polygon @@ -16,7 +17,6 @@ from jinja2 import Template from pathlib import Path from dataclasses import dataclass -from slugify import slugify from email.mime.text import MIMEText from email.utils import formataddr from aiosmtplib import send as send_email @@ -179,7 +179,7 @@ def remove_z_dimension(coord): """Remove z dimension from geojson.""" return coord.pop() if len(coord) == 3 else None - features = parse_featcol(features) + features = geojson_to_featcol(features) multi_polygons = [] # handles both collection or single feature @@ -208,27 +208,6 @@ def remove_z_dimension(coord): ) from e -def parse_featcol(features: Union[Feature, FeatCol, MultiPolygon, Polygon]): - """Parse a feature collection or feature into a GeoJSON FeatureCollection. - - Args: - features: Feature, FeatCol, MultiPolygon, Polygon or dict. - - Returns: - dict: Parsed GeoJSON FeatureCollection. - """ - if isinstance(features, dict): - return features - - feat_col = features.model_dump_json() - feat_col = geojson.loads(feat_col) - if isinstance(features, (Polygon, MultiPolygon)): - feat_col = geojson.FeatureCollection([geojson.Feature(geometry=feat_col)]) - elif isinstance(features, Feature): - feat_col = geojson.FeatureCollection([feat_col]) - return feat_col - - def get_address_from_lat_lon(latitude, longitude): """Get address using Nominatim, using lat,lon.""" base_url = "https://nominatim.openstreetmap.org/reverse" @@ -268,35 +247,98 @@ def get_address_from_lat_lon(latitude, longitude): return address_str -def multipolygon_to_polygon(features: Union[Feature, FeatCol, MultiPolygon, Polygon]): +def multipolygon_to_polygon( + featcol: geojson.FeatureCollection, +) -> geojson.FeatureCollection: """Converts a GeoJSON FeatureCollection of MultiPolygons to Polygons. Args: - features : A GeoJSON FeatureCollection containing MultiPolygons/Polygons. + featcol : A GeoJSON FeatureCollection containing MultiPolygons/Polygons. Returns: geojson.FeatureCollection: A GeoJSON FeatureCollection containing Polygons. """ - geojson_feature = [] - features = parse_featcol(features) - - # handles both collection or single feature - features = features.get("features", [features]) + final_features = [] - for feature in features: + for feature in featcol.get("features", []): properties = feature["properties"] - geom = shape(feature["geometry"]) + try: + geom = shape(feature["geometry"]) + except ValueError: + log.warning(f"Geometry is not valid, so was skipped: {feature['geometry']}") + continue + if geom.geom_type == "Polygon": - geojson_feature.append( - geojson.Feature(geometry=geom, properties=properties) - ) + final_features.append(geojson.Feature(geometry=geom, properties=properties)) elif geom.geom_type == "MultiPolygon": - geojson_feature.extend( + final_features.extend( geojson.Feature(geometry=polygon_coords, properties=properties) for polygon_coords in geom.geoms ) - return geojson.FeatureCollection(geojson_feature) + return geojson.FeatureCollection(final_features) + + +def normalise_featcol(featcol: geojson.FeatureCollection) -> geojson.FeatureCollection: + """Normalise a FeatureCollection into a standadised format. + + The final FeatureCollection will only contain: + - Polygon + - Polyline + - Point + + Processed: + - MultiPolygons will be divided out into individual polygons. + - GeometryCollections wrappers will be stripped out. + - Removes any z-dimension coordinates, e.g. [43, 32, 0.0] + + Args: + featcol: A parsed FeatureCollection. + + Returns: + geojson.FeatureCollection: A normalised FeatureCollection. + """ + for feat in featcol.get("features", []): + geom = feat.get("geometry") + + # Strip out GeometryCollection wrappers + if ( + geom.get("type") == "GeometryCollection" + and len(geom.get("geometries", [])) == 1 + ): + feat["geometry"] = geom.get("geometries")[0] + + # Remove any z-dimension coordinates + coords = geom.get("coordinates") + if isinstance(coords, list) and len(coords) == 3: + coords.pop() + + # Convert MultiPolygon type --> individual Polygons + return multipolygon_to_polygon(featcol) + + +def geojson_to_featcol(geojson_obj: dict) -> geojson.FeatureCollection: + """Enforce GeoJSON is wrapped in FeatureCollection. + + The type check is done directly from the GeoJSON to allow parsing + from different upstream libraries (e.g. geojson_pydantic). + """ + # We do a dumps/loads cycle to strip any extra obj logic + geojson_type = json.loads(json.dumps(geojson_obj)).get("type") + + if geojson_type == "FeatureCollection": + log.debug("Already in FeatureCollection format, reparsing") + features = geojson_obj.get("features") + elif geojson_type == "Feature": + log.debug("Converting Feature to FeatureCollection") + features = [geojson_obj] + else: + log.debug("Converting Geometry to FeatureCollection") + features = [geojson.Feature(geometry=geojson_obj)] + + featcol = geojson.FeatureCollection(features=features) + + return normalise_featcol(featcol) @dataclass @@ -383,32 +425,3 @@ def test_email(email_to: str, subject: str = "Test email") -> None: send_notification_email( email_to=email_to, subject=subject, html_content=html_content ) - - -def generate_slug(name: str) -> str: - """ - Generate a unique slug based on the provided name. - - The slug is created by converting the given name into a URL-friendly format and appending - the current date and time to ensure uniqueness. The date and time are formatted as - "ddmmyyyyHHMM" to create a timestamp. - - Args: - name (str): The name from which the slug will be generated. - - Returns: - str: The generated slug, which includes the URL-friendly version of the name and - a timestamp. If an error occurs during the generation, an empty string is returned. - - Raises: - Exception: If an error occurs during the slug generation process. - """ - try: - slug = slugify(name) - now = datetime.now() - date_time_str = now.strftime("%d%m%Y%H%M") - slug_with_date = f"{slug}-{date_time_str}" - return slug_with_date - except Exception as e: - print(f"An error occurred while generating the slug: {e}") - return "" From 99c036c3b8b4d78314dc720b5db755e1005e5d55 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Wed, 7 Aug 2024 00:50:29 +0100 Subject: [PATCH 10/66] feat: working project routes using psycopg + pydantic models --- src/backend/app/projects/project_crud.py | 199 ++---------- src/backend/app/projects/project_deps.py | 64 ++++ src/backend/app/projects/project_routes.py | 128 +++----- src/backend/app/projects/project_schemas.py | 327 ++++++++++++++++---- 4 files changed, 401 insertions(+), 317 deletions(-) create mode 100644 src/backend/app/projects/project_deps.py diff --git a/src/backend/app/projects/project_crud.py b/src/backend/app/projects/project_crud.py index 7d070b0f..4c157bb1 100644 --- a/src/backend/app/projects/project_crud.py +++ b/src/backend/app/projects/project_crud.py @@ -1,87 +1,18 @@ import json import uuid -from app.projects import project_schemas from loguru import logger as log -import shapely.wkb as wkblib -from shapely.geometry import shape from fastapi import HTTPException -from app.utils import merge_multipolygon from fmtm_splitter.splitter import split_by_square from fastapi.concurrency import run_in_threadpool -from databases import Database -from app.models.enums import ProjectStatus -from app.utils import generate_slug - +from psycopg import Connection +from geojson_pydantic import FeatureCollection -async def create_project_with_project_info( - db: Database, author_id: uuid.UUID, project_metadata: project_schemas.ProjectIn -): - """Create a project in database.""" - _id = uuid.uuid4() - query = """ - INSERT INTO projects ( - id, slug, author_id, name, description, per_task_instructions, status, visibility, outline, no_fly_zones, dem_url, output_orthophoto_url, output_pointcloud_url, output_raw_url, task_split_dimension, deadline_at, final_output, requires_approval_from_manager_for_locking, front_overlap, side_overlap, created_at) - VALUES ( - :id, - :slug, - :author_id, - :name, - :description, - :per_task_instructions, - :status, - :visibility, - :outline, - :no_fly_zones, - :dem_url, - :output_orthophoto_url, - :output_pointcloud_url, - :output_raw_url, - :task_split_dimension, - :deadline_at, - :final_output, - :requires_approval_from_manager_for_locking, - :front_overlap, - :side_overlap, - CURRENT_TIMESTAMP - ) - RETURNING id - """ - try: - project_id = await db.execute( - query, - values={ - "id": _id, - "slug": generate_slug(project_metadata.name), - "author_id": author_id, - "name": project_metadata.name, - "description": project_metadata.description, - "per_task_instructions": project_metadata.per_task_instructions, - "status": ProjectStatus.DRAFT.name, - "visibility": project_metadata.visibility.name, - "outline": str(project_metadata.outline), - "no_fly_zones": str(project_metadata.no_fly_zones) - if project_metadata.no_fly_zones is not None - else None, - "dem_url": project_metadata.dem_url, - "output_orthophoto_url": project_metadata.output_orthophoto_url, - "output_pointcloud_url": project_metadata.output_pointcloud_url, - "output_raw_url": project_metadata.output_raw_url, - "task_split_dimension": project_metadata.task_split_dimension, - "deadline_at": project_metadata.deadline_at, - "final_output": [item.value for item in project_metadata.final_output], - "requires_approval_from_manager_for_locking": project_metadata.requires_approval_from_manager_for_locking, - "front_overlap": project_metadata.front_overlap, - "side_overlap": project_metadata.side_overlap, - }, - ) - return project_id - - except Exception as e: - log.exception(e) - raise HTTPException(e) from e +from app.models.enums import HTTPStatus +from app.utils import merge_multipolygon -async def get_project_by_id(db: Database, project_id: uuid.UUID): +# TODO delete me +async def get_project_by_id(db: Connection, project_id: uuid.UUID): "Get a single database project object by project_id" query = """ select * from projects where id=:project_id""" @@ -89,106 +20,44 @@ async def get_project_by_id(db: Database, project_id: uuid.UUID): return result -async def get_project_info_by_id(db: Database, project_id: uuid.UUID): - """Get a single project & all associated tasks by ID.""" - query = """ - SELECT - projects.id, - projects.slug, - projects.name, - projects.description, - projects.per_task_instructions, - projects.outline - FROM projects - WHERE projects.id = :project_id - LIMIT 1; - """ - - project_record = await db.fetch_one(query, {"project_id": project_id}) - if not project_record: - return None - query = """ SELECT id, project_task_index, outline FROM tasks WHERE project_id = :project_id;""" - task_records = await db.fetch_all(query, {"project_id": project_id}) - project_record.tasks = task_records if task_records is not None else [] - project_record.task_count = len(task_records) - return project_record - - -async def get_projects( - db: Database, - skip: int = 0, - limit: int = 100, -): - """Get all projects.""" - raw_sql = """ - SELECT id, slug, name, description, per_task_instructions, outline - FROM projects - ORDER BY created_at DESC - OFFSET :skip - LIMIT :limit; - """ - db_projects = await db.fetch_all(raw_sql, {"skip": skip, "limit": limit}) - - return db_projects - - async def create_tasks_from_geojson( - db: Database, + db: Connection, project_id: uuid.UUID, - boundaries: str, + boundaries: FeatureCollection, ): """Create tasks for a project, from provided task boundaries.""" + # TODO this should probably drop existing boundaries before creating new? try: - if isinstance(boundaries, str): - boundaries = json.loads(boundaries) - - # Update the boundary polyon on the database. - if boundaries["type"] == "Feature": - polygons = [boundaries] - else: - polygons = boundaries["features"] + polygons = boundaries["features"] log.debug(f"Processing {len(polygons)} task geometries") - for index, polygon in enumerate(polygons): - try: - # If the polygon is a MultiPolygon, convert it to a Polygon - if polygon["geometry"]["type"] == "MultiPolygon": - log.debug("Converting MultiPolygon to Polygon") - polygon["geometry"]["type"] = "Polygon" - polygon["geometry"]["coordinates"] = polygon["geometry"][ - "coordinates" - ][0] - - task_id = str(uuid.uuid4()) - query = """ - INSERT INTO tasks (id, project_id, outline, project_task_index) - VALUES (:id, :project_id, :outline, :project_task_index);""" - result = await db.execute( - query, - values={ - "id": task_id, - "project_id": project_id, - "outline": wkblib.dumps(shape(polygon["geometry"]), hex=True), - "project_task_index": index + 1, - }, - ) + # Prepare the data for bulk insert + task_data = [ + (project_id, index + 1, json.dumps(polygon["geometry"])) + for index, polygon in enumerate(polygons) + ] + + # Perform bulk insert + async with db.cursor() as cur: + await cur.executemany( + """ + INSERT INTO tasks (id, project_id, project_task_index, outline) + VALUES ( + gen_random_uuid(), + (%s), + (%s), + ST_GeomFromGeoJSON(%s) + ); + """, + task_data, + ) + + log.debug(f"Created database tasks for project ID {project_id}") + return True - if result: - log.debug( - "Created database task | " - f"Project ID {project_id} | " - f"Task index {index}" - ) - log.debug( - "COMPLETE: creating project boundary, based on task boundaries" - ) - return True - except Exception as e: - log.exception(e) - raise HTTPException(e) from e except Exception as e: log.exception(e) - raise HTTPException(e) from e + raise HTTPException(status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=str(e)) async def preview_split_by_square(boundary: str, meters: int): diff --git a/src/backend/app/projects/project_deps.py b/src/backend/app/projects/project_deps.py new file mode 100644 index 00000000..2881b0c6 --- /dev/null +++ b/src/backend/app/projects/project_deps.py @@ -0,0 +1,64 @@ +"""Dependencies for Project endpoints.""" + +import json +from uuid import UUID +from typing import Annotated +from loguru import logger as log +from fastapi import Depends, HTTPException, Path, File, UploadFile +from psycopg import Connection +from geojson import FeatureCollection + +from app.db import database +from app.models.enums import HTTPStatus +from app.projects.project_schemas import DbProject +from app.utils import geojson_to_featcol + + +async def get_project_by_id( + project_id: Annotated[ + UUID, + Path( + description="The project ID in UUID format.", + ), + ], + db: Annotated[Connection, Depends(database.get_db)], +) -> DbProject: + """Get a single project by id.""" + try: + return await DbProject.one(db, project_id) + except KeyError as e: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) from e + + +async def geojson_upload( + geojson: Annotated[ + UploadFile, + File( + description="A GeoJSON file.", + ), + ], +) -> FeatureCollection: + """ + Normalise a geojson upload to a FeatureCollection. + + MultiPolygons will be exploded into Polygon features. + """ + log.debug("Reading geojson data from uploaded file") + bytes_data = await geojson.read() + + try: + geojson_data = json.loads(bytes_data) # + except json.decoder.JSONDecodeError as e: + msg = "Failed to read uploaded GeoJSON file. Is it valid?" + log.warning(msg) + raise HTTPException( + status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=msg + ) from e + + featcol = geojson_to_featcol(geojson_data) + if not isinstance(featcol, FeatureCollection): + msg = "Uploaded GeoJSON could not be parsed to FeatureCollection" + log.warning(msg) + raise HTTPException(status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=msg) + + return featcol diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index 3796784f..4aff1a39 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -1,21 +1,21 @@ import os -import json -import uuid -from app.users.user_deps import login_required -from app.users.user_schemas import AuthUser +from typing import Annotated import geojson from datetime import timedelta from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Form +from geojson_pydantic import FeatureCollection from loguru import logger as log -from app.projects import project_schemas, project_crud +from psycopg import Connection +from shapely.geometry import shape, mapping +from shapely.ops import unary_union + +from app.projects import project_schemas, project_crud, project_deps from app.db import database from app.models.enums import HTTPStatus -from app.utils import multipolygon_to_polygon from app.s3 import s3_client from app.config import settings -from databases import Database -from shapely.geometry import shape, mapping -from shapely.ops import unary_union +from app.users.user_deps import login_required +from app.users.user_schemas import AuthUser router = APIRouter( prefix=f"{settings.API_PREFIX}/projects", @@ -25,9 +25,11 @@ @router.delete("/{project_id}", tags=["Projects"]) async def delete_project_by_id( - project_id: uuid.UUID, - db: Database = Depends(database.get_db), - user: AuthUser = Depends(login_required), + project: Annotated[ + project_schemas.DbProject, Depends(project_deps.get_project_by_id) + ], + db: Annotated[Connection, Depends(database.get_db)], + user_data: Annotated[AuthUser, Depends(login_required)], ): """ Delete a project by its ID, along with all associated tasks. @@ -42,91 +44,48 @@ async def delete_project_by_id( Raises: HTTPException: If the project is not found. """ - delete_query = """ - WITH deleted_project AS ( - DELETE FROM projects - WHERE id = :project_id - RETURNING id - ), deleted_tasks AS ( - DELETE FROM tasks - WHERE project_id = :project_id - RETURNING id - ), deleted_task_events AS ( - DELETE FROM task_events - WHERE project_id = :project_id - RETURNING event_id - ) - SELECT id FROM deleted_project - """ - - result = await db.fetch_one(query=delete_query, values={"project_id": project_id}) + project_id = await project_schemas.DbProject.delete(db, project.id) + return {"message": f"Project successfully deleted {project_id}"} - if not result: - raise HTTPException(status_code=404) - return {"message": f"Project ID: {project_id} is deleted successfully."} - - -@router.post("/create_project", tags=["Projects"]) +@router.post("/", tags=["Projects"]) async def create_project( project_info: project_schemas.ProjectIn, - db: Database = Depends(database.get_db), - user_data: AuthUser = Depends(login_required), + db: Annotated[Connection, Depends(database.get_db)], + user_data: Annotated[AuthUser, Depends(login_required)], ): - """Create a project in database.""" - author_id = user_data.id - project_id = await project_crud.create_project_with_project_info( - db, author_id, project_info - ) - if not project_id: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, detail="Project creation failed" - ) - return {"message": "Project successfully created", "project_id": project_id} + """Create a project in database.""" + project_id = await project_schemas.DbProject.create(db, project_info, user_data.id) + return {"message": f"Project successfully created ({project_id})"} @router.post("/{project_id}/upload-task-boundaries", tags=["Projects"]) async def upload_project_task_boundaries( - project_id: uuid.UUID, - task_geojson: UploadFile = File(...), - db: Database = Depends(database.get_db), - user: AuthUser = Depends(login_required), + project: Annotated[ + project_schemas.DbProject, Depends(project_deps.get_project_by_id) + ], + db: Annotated[Connection, Depends(database.get_db)], + user: Annotated[AuthUser, Depends(login_required)], + task_featcol: Annotated[FeatureCollection, Depends(project_deps.geojson_upload)], ): """Set project task boundaries using split GeoJSON from frontend. Each polygon in the uploaded geojson are made into single task. - Required Parameters: - project_id (id): ID for associated project. - task_geojson (UploadFile): Multi-polygon GeoJSON file. - Returns: dict: JSON containing success message, project ID, and number of tasks. """ - # check the project in Database - raw_sql = f"""SELECT id FROM projects WHERE id = '{project_id}' LIMIT 1;""" - project = await db.fetch_one(query=raw_sql) - if not project: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, detail="Project not found." - ) - # read entire file - content = await task_geojson.read() - task_boundaries = json.loads(content) - task_boundaries = multipolygon_to_polygon(task_boundaries) - log.debug("Creating tasks for each polygon in project") - await project_crud.create_tasks_from_geojson(db, project_id, task_boundaries) - - return {"message": "Project Boundary Uploaded", "project_id": f"{project_id}"} + await project_crud.create_tasks_from_geojson(db, project.id, task_featcol) + return {"message": "Project Boundary Uploaded", "project_id": f"{project.id}"} @router.post("/preview-split-by-square/", tags=["Projects"]) async def preview_split_by_square( + user: Annotated[AuthUser, Depends(login_required)], project_geojson: UploadFile = File(...), no_fly_zones: UploadFile = File(default=None), dimension: int = Form(100), - user: AuthUser = Depends(login_required), ): """Preview splitting by square.""" @@ -164,7 +123,8 @@ async def preview_split_by_square( @router.post("/generate-presigned-url/", tags=["Image Upload"]) async def generate_presigned_url( - data: project_schemas.PresignedUrlRequest, user: AuthUser = Depends(login_required) + data: project_schemas.PresignedUrlRequest, + user: Annotated[AuthUser, Depends(login_required)], ): """ Generate a pre-signed URL for uploading an image to S3 Bucket. @@ -206,26 +166,26 @@ async def generate_presigned_url( @router.get("/", tags=["Projects"], response_model=list[project_schemas.ProjectOut]) async def read_projects( + db: Annotated[Connection, Depends(database.get_db)], + user_data: Annotated[AuthUser, Depends(login_required)], skip: int = 0, limit: int = 100, - db: Database = Depends(database.get_db), - user_data: AuthUser = Depends(login_required), ): - "Return all projects" - projects = await project_crud.get_projects(db, skip, limit) - return projects + "Get all projects with task count." + try: + return await project_schemas.DbProject.all(db, skip, limit) + except KeyError as e: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) from e @router.get( "/{project_id}", tags=["Projects"], response_model=project_schemas.ProjectOut ) async def read_project( - project_id: uuid.UUID, - db: Database = Depends(database.get_db), - user_data: AuthUser = Depends(login_required), + project: Annotated[ + project_schemas.DbProject, Depends(project_deps.get_project_by_id) + ], + user_data: Annotated[AuthUser, Depends(login_required)], ): """Get a specific project and all associated tasks by ID.""" - project = await project_crud.get_project_info_by_id(db, project_id) - if project is None: - raise HTTPException(status_code=404, detail="Project not found") return project diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index 0e737bc1..1235860e 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -5,23 +5,42 @@ from app.models.enums import FinalOutput, ProjectVisibility, State from shapely import wkb from datetime import date +import json +import geojson +from typing import Optional, List, Annotated +from datetime import datetime, date +from loguru import logger as log +from fastapi import HTTPException +from pydantic import BaseModel, computed_field +from pydantic.functional_validators import AfterValidator +from pydantic.functional_serializers import PlainSerializer +from psycopg import Connection +from psycopg.rows import class_row +from geojson_pydantic import Feature, FeatureCollection, Polygon, Point, MultiPolygon +from slugify import slugify + +from app.models.enums import ( + IntEnum, + ProjectVisibility, + ProjectStatus, + State, + HTTPStatus, +) from app.utils import ( - geojson_to_geometry, - multipolygon_to_polygon, - read_wkb, merge_multipolygon, - str_to_geojson, - write_wkb, ) -class ProjectInfo(BaseModel): - """Basic project info.""" +def validate_geojson( + value: FeatureCollection | Feature | Polygon, +) -> geojson.FeatureCollection: + """Convert the upload GeoJSON to standardised FeatureCollection.""" + return merge_multipolygon(value.model_dump()) - id: int - name: str - description: str - per_task_instructions: Optional[str] = None + +def enum_to_str(value: IntEnum) -> str: + """Get the string value of the enum for db insert.""" + return value.name class ProjectIn(BaseModel): @@ -34,13 +53,25 @@ class ProjectIn(BaseModel): dem_url: Optional[str] = None gsd_cm_px: float = None is_terrain_follow: bool = False - outline_no_fly_zones: Optional[Union[FeatureCollection, Feature, Polygon]] = None - outline_geojson: Union[FeatureCollection, Feature, Polygon] + # TODO change all references outline_geojson --> outline + # TODO also no_fly_zones + outline: Annotated[ + FeatureCollection | Feature | Polygon, AfterValidator(validate_geojson) + ] + no_fly_zones: Annotated[ + Optional[FeatureCollection | Feature | Polygon], + AfterValidator(validate_geojson), + ] = None output_orthophoto_url: Optional[str] = None output_pointcloud_url: Optional[str] = None output_raw_url: Optional[str] = None deadline_at: Optional[date] = None - visibility: Optional[ProjectVisibility] = ProjectVisibility.PUBLIC + visibility: Annotated[ProjectVisibility | str, PlainSerializer(enum_to_str)] = ( + ProjectVisibility.PUBLIC + ) + status: Annotated[ProjectStatus | str, PlainSerializer(enum_to_str)] = ( + ProjectStatus.PUBLISHED + ) final_output: List[FinalOutput] = Field( ..., example=[ @@ -56,31 +87,33 @@ class ProjectIn(BaseModel): @computed_field @property - def no_fly_zones(self) -> Optional[Any]: - """Compute WKBElement geom from geojson.""" - if not self.outline_no_fly_zones: - return None + def slug(self) -> str: + """ + Generate a unique slug based on the provided name. - outline = multipolygon_to_polygon(self.outline_no_fly_zones) - return geojson_to_geometry(outline) + The slug is created by converting the given name into a URL-friendly format and appending + the current date and time to ensure uniqueness. The date and time are formatted as + "ddmmyyyyHHMM" to create a timestamp. - @computed_field - @property - def outline(self) -> Optional[Any]: - """Compute WKBElement geom from geojson.""" - if not self.outline_geojson: - return None + Args: + name (str): The name from which the slug will be generated. - outline = merge_multipolygon(self.outline_geojson) - return geojson_to_geometry(outline) + Returns: + str: The generated slug, which includes the URL-friendly version of the name and + a timestamp. If an error occurs during the generation, an empty string is returned. - @computed_field - @property - def centroid(self) -> Optional[Any]: - """Compute centroid for project outline.""" - if not self.outline: - return None - return write_wkb(read_wkb(self.outline).centroid) + Raises: + Exception: If an error occurs during the slug generation process. + """ + try: + slug = slugify(self.name) + now = datetime.now() + date_time_str = now.strftime("%d%m%Y%H%M") + slug_with_date = f"{slug}-{date_time_str}" + return slug_with_date + except Exception as e: + log.error(f"An error occurred while generating the slug: {e}") + return "" class TaskOut(BaseModel): @@ -88,29 +121,198 @@ class TaskOut(BaseModel): id: uuid.UUID project_task_index: int - outline: Any = Field(exclude=True) + outline: Polygon state: Optional[State] = None contributor: Optional[str] = None - @validator("state", pre=True, always=True) - def validate_state(cls, v): - if isinstance(v, str): - try: - v = State[v] - except KeyError: - raise ValueError(f"Invalid state: {v}") - return v - @computed_field - @property - def outline_geojson(self) -> Optional[Feature]: - """Compute the geojson outline from WKBElement outline.""" - if not self.outline: - return None - wkb_data = bytes.fromhex(self.outline) - geom = wkb.loads(wkb_data) - bbox = geom.bounds # Calculate bounding box - return str_to_geojson(self.outline, {"id": self.id, "bbox": bbox}, str(self.id)) +class DbProject(BaseModel): + """Project model for extracting from database.""" + + id: uuid.UUID + name: str + slug: Optional[str] = None + short_description: Optional[str] + description: str + per_task_instructions: Optional[str] = None + organisation_id: Optional[int] + outline: Polygon + centroid: Optional[Point] + no_fly_zones: Optional[MultiPolygon] + task_count: int = 0 + tasks: Optional[list[TaskOut]] = [] + # TODO add all remaining project fields and validators + + @staticmethod + async def one(db: Connection, project_id: uuid.UUID): + """Get a single project by it's ID, including tasks and task count.""" + async with db.cursor(row_factory=class_row(DbProject)) as cur: + # NOTE to wrap Polygon geometry in Feature + # jsonb_build_object( + # 'type', 'Feature', + # 'geometry', ST_AsGeoJSON(p.outline)::jsonb, + # 'id', p.id::varchar, + # 'properties', jsonb_build_object() + # ) AS outline, + await cur.execute( + """ + SELECT + p.*, + ST_AsGeoJSON(p.outline)::jsonb AS outline, + ST_AsGeoJSON(p.centroid)::jsonb AS centroid, + COALESCE(JSON_AGG(t.*) FILTER (WHERE t.id IS NOT NULL), '[]'::json) AS tasks, + COUNT(t.id) AS task_count + FROM + projects p + LEFT JOIN + tasks t ON p.id = t.project_id + WHERE + p.id = %(project_id)s + GROUP BY + p.id; + """, + {"project_id": project_id}, + ) + project = await cur.fetchone() + + if not project: + raise KeyError(f"Project {project_id} not found") + + return project + + @staticmethod + async def all(db: Connection, skip: int = 0, limit: int = 100): + """Get all projects, including tasks and task count.""" + async with db.cursor(row_factory=class_row(DbProject)) as cur: + await cur.execute( + """ + SELECT + p.*, + ST_AsGeoJSON(p.outline)::jsonb AS outline, + ST_AsGeoJSON(p.centroid)::jsonb AS centroid, + COALESCE(JSON_AGG(t.*) FILTER (WHERE t.id IS NOT NULL), '[]'::json) AS tasks, + COUNT(t.id) AS task_count + FROM + projects p + LEFT JOIN + tasks t ON p.id = t.project_id + GROUP BY + p.id + ORDER BY + created_at DESC + OFFSET %(skip)s + LIMIT %(limit)s; + """, + {"skip": skip, "limit": limit}, + ) + projects = await cur.fetchall() + + if not projects: + raise KeyError("No projects found") + + return projects + + @staticmethod + async def create(db: Connection, project: ProjectIn, user_id: str) -> uuid.UUID: + """Create a single project.""" + # NOTE we first check if a project with this name exists + # It is easier to do this than complex upsert logic + async with db.cursor() as cur: + sql = """ + SELECT EXISTS ( + SELECT 1 + FROM projects + WHERE LOWER(name) = %(name)s + ) + """ + await cur.execute(sql, {"name": project.name.lower()}) + project_exists = await cur.fetchone() + if project_exists[0]: + msg = f"Project name ({project.name}) already exists!" + log.warning(f"User ({user_id}) failed project creation: {msg}") + raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=msg) + + # NOTE exclude_none is used over exclude_unset, or default value are not included + model_dump = project.model_dump( + exclude_none=True, exclude=["outline", "centroid"] + ) + columns = ", ".join(model_dump.keys()) + value_placeholders = ", ".join(f"%({key})s" for key in model_dump.keys()) + sql = f""" + INSERT INTO projects ( + id, author_id, outline, centroid, created_at, {columns} + ) + VALUES ( + gen_random_uuid(), + %(author_id)s, + ST_GeomFromGeoJSON(%(outline)s), + ST_Centroid(ST_GeomFromGeoJSON(%(outline)s)), + NOW(), + {value_placeholders} + ) + RETURNING id; + """ + # We only want the first geometry (they should be merged previously) + outline_geometry = json.dumps(project.outline["features"][0]["geometry"]) + # Add required author_id and outline as json + model_dump.update( + { + "author_id": user_id, + "outline": outline_geometry, + } + ) + # Append no fly zones if they are present + # FIXME they are merged to a single geom! + if project.no_fly_zones: + no_fly_geoms = json.dumps(project.no_fly_zones["features"][0]["geometry"]) + model_dump.update( + { + "no_fly_zones": no_fly_geoms, + } + ) + + async with db.cursor() as cur: + await cur.execute(sql, model_dump) + new_project_id = await cur.fetchone() + + if not new_project_id: + msg = f"Unknown SQL error for data: {model_dump}" + log.warning(f"User ({user_id}) failed project creation: {msg}") + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=msg + ) + + return new_project_id[0] + + @staticmethod + async def delete(db: Connection, project_id: uuid.UUID) -> uuid.UUID: + """Delete a single project.""" + sql = """ + WITH deleted_project AS ( + DELETE FROM projects + WHERE id = %(project_id)s + RETURNING id + ), deleted_tasks AS ( + DELETE FROM tasks + WHERE project_id = %(project_id)s + RETURNING project_id + ), deleted_task_events AS ( + DELETE FROM task_events + WHERE project_id = %(project_id)s + RETURNING project_id + ) + SELECT id FROM deleted_project + """ + + async with db.cursor() as cur: + await cur.execute(sql, {"project_id": project_id}) + deleted_project_id = await cur.fetchone() + + if not deleted_project_id: + log.warning(f"Failed to delete project ({project_id})") + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) + + return deleted_project_id[0] class ProjectOut(BaseModel): @@ -121,20 +323,9 @@ class ProjectOut(BaseModel): name: str description: str per_task_instructions: Optional[str] = None - outline: Any = Field(exclude=True) - task_count: int = None - tasks: list[TaskOut] = [] - - @computed_field - @property - def outline_geojson(self) -> Optional[Feature]: - """Compute the geojson outline from WKBElement outline.""" - if not self.outline: - return None - wkb_data = bytes.fromhex(self.outline) - geom = wkb.loads(wkb_data) - bbox = geom.bounds # Calculate bounding box - return str_to_geojson(self.outline, {"id": self.id, "bbox": bbox}, str(self.id)) + outline: Polygon + task_count: int = 0 + tasks: Optional[list[TaskOut]] = [] class PresignedUrlRequest(BaseModel): From 492a9d9af959a9a3ba9da76959ed160c53a664fa Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Wed, 7 Aug 2024 00:50:52 +0100 Subject: [PATCH 11/66] refactor: rename create_project route to POST on /projects --- src/frontend/src/services/createproject.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/services/createproject.ts b/src/frontend/src/services/createproject.ts index bb5c4012..5de0cbb7 100644 --- a/src/frontend/src/services/createproject.ts +++ b/src/frontend/src/services/createproject.ts @@ -7,7 +7,7 @@ export const getProjectDetail = (id: string) => authenticated(api).get(`/projects/${id}`); export const postCreateProject = (data: any) => - authenticated(api).post('/projects/create_project', data, { + authenticated(api).post('/projects', data, { headers: { 'Content-Type': 'application/json' }, }); From 8ebec650024e9f28ac0f06bfcece1ee2a81b5b3d Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Wed, 7 Aug 2024 00:53:02 +0100 Subject: [PATCH 12/66] refactor: rename outline_geojson & outline_no_fly_zones --- .../CreateprojectLayout/index.tsx | 16 ++++++++-------- .../FormContents/DefineAOI/index.tsx | 18 +++++++++--------- .../IndividualProject/MapSection/index.tsx | 4 ++-- .../components/Projects/MapSection/index.tsx | 2 +- .../src/views/IndividualProject/index.tsx | 2 +- src/frontend/src/views/Projects/index.tsx | 2 +- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx b/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx index 6f0c7062..a08b3787 100644 --- a/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx +++ b/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx @@ -78,8 +78,8 @@ export default function CreateprojectLayout() { name: '', short_description: '', description: '', - outline_geojson: null, - outline_no_fly_zones: null, + outline: null, + no_fly_zones: null, gsd_cm_px: null, dimension: null, is_terrain_follow: null, @@ -160,17 +160,17 @@ export default function CreateprojectLayout() { const onSubmit = (data: any) => { if (activeStep === 2) { if ( - !data?.outline_geojson || - (Array.isArray(data?.outline_geojson) && - data?.outline_geojson?.length === 0) + !data?.outline || + (Array.isArray(data?.outline) && + data?.outline?.length === 0) ) { toast.error('Please upload or draw and save project area'); return; } if ( isNoflyzonePresent === 'yes' && - (!data?.outline_no_fly_zones || - data?.outline_no_fly_zones?.length === 0) + (!data?.no_fly_zones || + data?.no_fly_zones?.length === 0) ) { toast.error('Please upload or draw and save No Fly zone area'); return; @@ -187,7 +187,7 @@ export default function CreateprojectLayout() { is_terrain_follow: isTerrainFollow === 'hilly', }; // remove key - if (isNoflyzonePresent === 'no') delete payload?.outline_no_fly_zones; + if (isNoflyzonePresent === 'no') delete payload?.no_fly_zones; createProject(payload); }; diff --git a/src/frontend/src/components/CreateProject/FormContents/DefineAOI/index.tsx b/src/frontend/src/components/CreateProject/FormContents/DefineAOI/index.tsx index 7e8498c7..949bf93c 100644 --- a/src/frontend/src/components/CreateProject/FormContents/DefineAOI/index.tsx +++ b/src/frontend/src/components/CreateProject/FormContents/DefineAOI/index.tsx @@ -82,7 +82,7 @@ export default function DefineAOI({ drawProjectAreaEnable: false, }), ); - setValue('outline_geojson', drawnProjectArea); + setValue('outline', drawnProjectArea); if (resetDrawTool) { resetDrawTool(); } @@ -114,7 +114,7 @@ export default function DefineAOI({ drawNoFlyZoneEnable: false, }), ); - setValue('outline_no_fly_zones', drawnNoFlyZone); + setValue('no_fly_zones', drawnNoFlyZone); if (resetDrawTool) { resetDrawTool(); } @@ -133,7 +133,7 @@ export default function DefineAOI({ if (typeof z === 'object' && !Array.isArray(z) && z !== null) { const convertedGeojson = flatten(z); dispatch(setCreateProjectState({ projectArea: convertedGeojson })); - setValue('outline_geojson', convertedGeojson); + setValue('outline', convertedGeojson); } }); } catch (err: any) { @@ -177,7 +177,7 @@ export default function DefineAOI({ if (typeof z === 'object' && !Array.isArray(z) && z !== null) { const convertedGeojson = flatten(z); dispatch(setCreateProjectState({ noFlyZone: convertedGeojson })); - setValue('outline_no_fly_zones', convertedGeojson); + setValue('no_fly_zones', convertedGeojson); } }); } catch (err: any) { @@ -232,7 +232,7 @@ export default function DefineAOI({ @@ -352,10 +352,10 @@ export default function DefineAOI({ ( { return { ...acc, - features: [...acc.features, curr.outline_geojson], + features: [...acc.features, curr.outline], }; }, { @@ -136,7 +136,7 @@ export default function MapSection() { map={map as Map} id={`tasks-layer-${task?.id}-${taskStatusObj?.[task?.id]}`} visibleOnMap={task?.id && taskStatusObj} - geojson={task.outline_geojson as GeojsonType} + geojson={task.outline as GeojsonType} interactions={['feature']} layerOptions={ taskStatusObj?.[`${task?.id}`] === 'LOCKED_FOR_MAPPING' diff --git a/src/frontend/src/components/Projects/MapSection/index.tsx b/src/frontend/src/components/Projects/MapSection/index.tsx index b8ddd07d..8b7fd410 100644 --- a/src/frontend/src/components/Projects/MapSection/index.tsx +++ b/src/frontend/src/components/Projects/MapSection/index.tsx @@ -22,7 +22,7 @@ export default function ProjectsMapSection() { (acc: Record, current: Record) => { return { ...acc, - features: [...acc.features, centroid(current.outline_geojson)], + features: [...acc.features, centroid(current.outline)], }; }, { diff --git a/src/frontend/src/views/IndividualProject/index.tsx b/src/frontend/src/views/IndividualProject/index.tsx index 2cbaf8ac..16d7b741 100644 --- a/src/frontend/src/views/IndividualProject/index.tsx +++ b/src/frontend/src/views/IndividualProject/index.tsx @@ -47,7 +47,7 @@ export default function IndividualProject() { dispatch( setProjectState({ tasksData: res.tasks, - projectArea: res.outline_geojson, + projectArea: res.outline, }), ), }); diff --git a/src/frontend/src/views/Projects/index.tsx b/src/frontend/src/views/Projects/index.tsx index 04266d1b..ba10a04d 100644 --- a/src/frontend/src/views/Projects/index.tsx +++ b/src/frontend/src/views/Projects/index.tsx @@ -51,7 +51,7 @@ export default function Projects() { containerId={`map-libre-map-${singleproject.id}`} title={singleproject.name} description={singleproject.description} - geojson={singleproject.outline_geojson} + geojson={singleproject.outline} /> ), ) From 5aa3787cfe1038e59e9cbb048e9a87589ed3da27 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Wed, 7 Aug 2024 00:54:38 +0100 Subject: [PATCH 13/66] refactor: replace all refs to Database --> Connection (psycopg) --- src/backend/app/drones/drone_crud.py | 25 +++++++++++---------- src/backend/app/drones/drone_routes.py | 21 ++++++++--------- src/backend/app/projects/project_schemas.py | 2 -- src/backend/app/tasks/task_crud.py | 22 ++++++++++-------- src/backend/app/tasks/task_routes.py | 19 +++++++++------- src/backend/app/users/user_crud.py | 14 ++++++------ src/backend/app/users/user_routes.py | 14 ++++++------ 7 files changed, 62 insertions(+), 55 deletions(-) diff --git a/src/backend/app/drones/drone_crud.py b/src/backend/app/drones/drone_crud.py index 0bb50783..1494091f 100644 --- a/src/backend/app/drones/drone_crud.py +++ b/src/backend/app/drones/drone_crud.py @@ -1,14 +1,15 @@ from app.drones import drone_schemas from app.models.enums import HTTPStatus -from databases import Database from loguru import logger as log from fastapi import HTTPException -from asyncpg import UniqueViolationError +from psycopg import Connection + +# from asyncpg import UniqueViolationError from typing import List from app.drones.drone_schemas import DroneOut -async def read_all_drones(db: Database) -> List[DroneOut]: +async def read_all_drones(db: Connection) -> List[DroneOut]: """ Retrieves all drone records from the database. @@ -32,7 +33,7 @@ async def read_all_drones(db: Database) -> List[DroneOut]: ) from e -async def delete_drone(db: Database, drone_id: int) -> bool: +async def delete_drone(db: Connection, drone_id: int) -> bool: """ Deletes a drone record from the database, along with associated drone flights. @@ -63,7 +64,7 @@ async def delete_drone(db: Database, drone_id: int) -> bool: ) from e -async def get_drone(db: Database, drone_id: int): +async def get_drone(db: Connection, drone_id: int): """ Retrieves a drone record from the database. @@ -89,7 +90,7 @@ async def get_drone(db: Database, drone_id: int): ) from e -async def create_drone(db: Database, drone_info: drone_schemas.DroneIn): +async def create_drone(db: Connection, drone_info: drone_schemas.DroneIn): """ Creates a new drone record in the database. @@ -116,12 +117,12 @@ async def create_drone(db: Database, drone_info: drone_schemas.DroneIn): result = await db.execute(insert_query, drone_info.__dict__) return result - except UniqueViolationError as e: - log.exception("Unique constraint violation: %s", e) - raise HTTPException( - status_code=HTTPStatus.CONFLICT, - detail="A drone with this model already exists", - ) + # except UniqueViolationError as e: + # log.exception("Unique constraint violation: %s", e) + # raise HTTPException( + # status_code=HTTPStatus.CONFLICT, + # detail="A drone with this model already exists", + # ) except Exception as e: log.exception(e) diff --git a/src/backend/app/drones/drone_routes.py b/src/backend/app/drones/drone_routes.py index a3fc232e..4fee8b4f 100644 --- a/src/backend/app/drones/drone_routes.py +++ b/src/backend/app/drones/drone_routes.py @@ -1,11 +1,12 @@ +from typing import Annotated from app.users.user_deps import login_required from app.users.user_schemas import AuthUser from app.models.enums import HTTPStatus from fastapi import APIRouter, Depends, HTTPException -from app.db.database import get_db +from app.db import database from app.config import settings from app.drones import drone_schemas -from databases import Database +from psycopg import Connection from app.drones import drone_crud from typing import List @@ -18,8 +19,8 @@ @router.get("/", tags=["Drones"], response_model=List[drone_schemas.DroneOut]) async def read_drones( - db: Database = Depends(get_db), - user_data: AuthUser = Depends(login_required), + db: Annotated[Connection, Depends(database.get_db)], + user_data: Annotated[AuthUser, Depends(login_required)], ): """ Retrieves all drone records from the database. @@ -38,8 +39,8 @@ async def read_drones( @router.delete("/{drone_id}", tags=["Drones"]) async def delete_drone( drone_id: int, - db: Database = Depends(get_db), - user_data: AuthUser = Depends(login_required), + db: Annotated[Connection, Depends(database.get_db)], + user_data: Annotated[AuthUser, Depends(login_required)], ): """ Deletes a drone record from the database. @@ -61,8 +62,8 @@ async def delete_drone( @router.post("/create_drone", tags=["Drones"]) async def create_drone( drone_info: drone_schemas.DroneIn, - db: Database = Depends(get_db), - user_data: AuthUser = Depends(login_required), + db: Annotated[Connection, Depends(database.get_db)], + user_data: Annotated[AuthUser, Depends(login_required)], ): """ Creates a new drone record in the database. @@ -86,8 +87,8 @@ async def create_drone( @router.get("/{drone_id}", tags=["Drones"], response_model=drone_schemas.DroneOut) async def read_drone( drone_id: int, - db: Database = Depends(get_db), - user_data: AuthUser = Depends(login_required), + db: Annotated[Connection, Depends(database.get_db)], + user_data: Annotated[AuthUser, Depends(login_required)], ): """ Retrieves a drone record from the database. diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index 1235860e..f185f31c 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -53,8 +53,6 @@ class ProjectIn(BaseModel): dem_url: Optional[str] = None gsd_cm_px: float = None is_terrain_follow: bool = False - # TODO change all references outline_geojson --> outline - # TODO also no_fly_zones outline: Annotated[ FeatureCollection | Feature | Polygon, AfterValidator(validate_geojson) ] diff --git a/src/backend/app/tasks/task_crud.py b/src/backend/app/tasks/task_crud.py index 160ddcab..4bbddb8f 100644 --- a/src/backend/app/tasks/task_crud.py +++ b/src/backend/app/tasks/task_crud.py @@ -1,11 +1,11 @@ import uuid -from databases import Database from app.models.enums import HTTPStatus, State from fastapi import HTTPException from loguru import logger as log +from psycopg import Connection -async def get_tasks_by_user(user_id: str, db: Database): +async def get_tasks_by_user(user_id: str, db: Connection): try: query = """WITH task_details AS ( SELECT @@ -42,7 +42,7 @@ async def get_tasks_by_user(user_id: str, db: Database): ) from e -async def get_all_tasks(db: Database, project_id: uuid.UUID): +async def get_all_tasks(db: Connection, project_id: uuid.UUID): query = """ SELECT id FROM tasks WHERE project_id = :project_id """ @@ -56,7 +56,7 @@ async def get_all_tasks(db: Database, project_id: uuid.UUID): return task_ids -async def all_tasks_states(db: Database, project_id: uuid.UUID): +async def all_tasks_states(db: Connection, project_id: uuid.UUID): query = """ SELECT DISTINCT ON (task_id) project_id, task_id, state FROM task_events @@ -95,7 +95,11 @@ async def all_tasks_states(db: Database, project_id: uuid.UUID): async def request_mapping( - db: Database, project_id: uuid.UUID, task_id: uuid.UUID, user_id: str, comment: str + db: Connection, + project_id: uuid.UUID, + task_id: uuid.UUID, + user_id: str, + comment: str, ): query = """ WITH last AS ( @@ -139,8 +143,8 @@ async def request_mapping( return {"project_id": project_id, "task_id": task_id, "comment": comment} -async def update_or_create_task_state( - db: Database, +async def update_task_state( + db: Connection, project_id: uuid.UUID, task_id: uuid.UUID, user_id: str, @@ -195,7 +199,7 @@ async def update_or_create_task_state( async def get_requested_user_id( - db: Database, project_id: uuid.UUID, task_id: uuid.UUID + db: Connection, project_id: uuid.UUID, task_id: uuid.UUID ): query = """ SELECT user_id @@ -216,7 +220,7 @@ async def get_requested_user_id( return result["user_id"] -async def get_project_task_by_id(db: Database, user_id: str): +async def get_project_task_by_id(db: Connection, user_id: str): """Get a list of pending tasks created by a specific user (project creator).""" _sql = """ SELECT id FROM projects WHERE author_id = :user_id diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 27b6a860..7bcb00ba 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -1,4 +1,5 @@ import uuid +from typing import Annotated from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from app.config import settings from app.models.enums import EventType, State, UserRole @@ -6,7 +7,7 @@ from app.users.user_deps import login_required from app.users.user_schemas import AuthUser from app.users.user_crud import get_user_by_id -from databases import Database +from psycopg import Connection from app.db import database from app.utils import send_notification_email, render_email_template from app.projects.project_crud import get_project_by_id @@ -21,8 +22,8 @@ @router.get("/", response_model=list[task_schemas.UserTasksStatsOut]) async def list_tasks( - db: Database = Depends(database.get_db), - user_data: AuthUser = Depends(login_required), + db: Annotated[Connection, Depends(database.get_db)], + user_data: Annotated[AuthUser, Depends(login_required)], ): """Get all tasks for a drone user.""" @@ -31,7 +32,9 @@ async def list_tasks( @router.get("/states/{project_id}") -async def task_states(project_id: uuid.UUID, db: Database = Depends(database.get_db)): +async def task_states( + db: Annotated[Connection, Depends(database.get_db)], project_id: uuid.UUID +): """Get all tasks states for a project.""" return await task_crud.all_tasks_states(db, project_id) @@ -39,12 +42,12 @@ async def task_states(project_id: uuid.UUID, db: Database = Depends(database.get @router.post("/event/{project_id}/{task_id}") async def new_event( + db: Annotated[Connection, Depends(database.get_db)], background_tasks: BackgroundTasks, project_id: uuid.UUID, task_id: uuid.UUID, detail: task_schemas.NewEvent, - user_data: AuthUser = Depends(login_required), - db: Database = Depends(database.get_db), + user_data: Annotated[AuthUser, Depends(login_required)], ): user_id = user_data.id @@ -223,8 +226,8 @@ async def new_event( @router.get("/requested_tasks/pending") async def get_pending_tasks( - user_data: AuthUser = Depends(login_required), - db: Database = Depends(database.get_db), + db: Annotated[Connection, Depends(database.get_db)], + user_data: Annotated[AuthUser, Depends(login_required)], ): """Get a list of pending tasks for a project creator.""" user_id = user_data.id diff --git a/src/backend/app/users/user_crud.py b/src/backend/app/users/user_crud.py index af521924..ec649102 100644 --- a/src/backend/app/users/user_crud.py +++ b/src/backend/app/users/user_crud.py @@ -5,9 +5,9 @@ from passlib.context import CryptContext from app.db import db_models from app.users.user_schemas import AuthUser, ProfileUpdate -from databases import Database from fastapi import HTTPException from pydantic import EmailStr +from psycopg import Connection pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") @@ -61,26 +61,26 @@ def get_password_hash(password: str) -> str: return pwd_context.hash(password) -async def get_user_by_id(db: Database, id: str): +async def get_user_by_id(db: Connection, id: str): query = "SELECT * FROM users WHERE id = :id LIMIT 1;" result = await db.fetch_one(query, {"id": id}) return result -async def get_userprofile_by_userid(db: Database, user_id: str): +async def get_userprofile_by_userid(db: Connection, user_id: str): query = "SELECT * FROM user_profile WHERE user_id = :user_id LIMIT 1;" result = await db.fetch_one(query, {"user_id": user_id}) return result -async def get_user_by_email(db: Database, email: str): +async def get_user_by_email(db: Connection, email: str): query = "SELECT * FROM users WHERE email_address = :email LIMIT 1;" result = await db.fetch_one(query, {"email": email}) return result async def authenticate( - db: Database, email: EmailStr, password: str + db: Connection, email: EmailStr, password: str ) -> db_models.DbUser | None: db_user = await get_user_by_email(db, email) if not db_user: @@ -91,7 +91,7 @@ async def authenticate( async def get_or_create_user( - db: Database, + db: Connection, user_data: AuthUser, ): """Get user from User table if exists, else create.""" @@ -132,7 +132,7 @@ async def get_or_create_user( async def update_user_profile( - db: Database, user_id: int, profile_update: ProfileUpdate + db: Connection, user_id: int, profile_update: ProfileUpdate ): """ Update user profile in the database. diff --git a/src/backend/app/users/user_routes.py b/src/backend/app/users/user_routes.py index 5bef1d91..eae74869 100644 --- a/src/backend/app/users/user_routes.py +++ b/src/backend/app/users/user_routes.py @@ -12,7 +12,7 @@ from app.users import user_crud from app.db import database from app.models.enums import HTTPStatus -from databases import Database +from psycopg import Connection from fastapi.responses import JSONResponse from loguru import logger as log @@ -31,7 +31,7 @@ @router.post("/login/") async def login_access_token( form_data: Annotated[OAuth2PasswordRequestForm, Depends()], - db: Database = Depends(database.get_db), + db: Annotated[Connection, Depends(database.get_db)], ) -> Token: """ OAuth2 compatible token login, get an access token for future requests @@ -60,8 +60,8 @@ async def login_access_token( async def update_user_profile( user_id: str, profile_update: ProfileUpdate, - db: Database = Depends(database.get_db), - user_data: AuthUser = Depends(login_required), + db: Annotated[Connection, Depends(database.get_db)], + user_data: Annotated[AuthUser, Depends(login_required)], ): """ Update user profile based on provided user_id and profile_update data. @@ -124,7 +124,7 @@ async def callback(request: Request, google_auth=Depends(init_google_auth)): @router.get("/refresh-token", response_model=Token) -async def update_token(user_data: AuthUser = Depends(login_required)): +async def update_token(user_data: Annotated[AuthUser, Depends(login_required)]): """Refresh access token""" access_token, refresh_token = await user_crud.create_access_token( @@ -135,8 +135,8 @@ async def update_token(user_data: AuthUser = Depends(login_required)): @router.get("/my-info/") async def my_data( - db: Database = Depends(database.get_db), - user_data: AuthUser = Depends(login_required), + db: Annotated[Connection, Depends(database.get_db)], + user_data: Annotated[AuthUser, Depends(login_required)], ): """Read access token and get user details from Google""" From b5a4b7708c44e7554a93d4ddf254f0d7d38bc9a2 Mon Sep 17 00:00:00 2001 From: spwoodcock Date: Wed, 7 Aug 2024 10:50:04 +0100 Subject: [PATCH 14/66] style: update import sorting for project_schemas --- src/backend/app/projects/project_schemas.py | 22 ++++++++------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index f185f31c..f010ae76 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -1,29 +1,23 @@ -import uuid -from pydantic import BaseModel, computed_field, Field, validator -from typing import Any, Optional, Union, List -from geojson_pydantic import Feature, FeatureCollection, Polygon -from app.models.enums import FinalOutput, ProjectVisibility, State -from shapely import wkb -from datetime import date import json -import geojson -from typing import Optional, List, Annotated +import uuid +from typing import Annotated, Optional, List from datetime import datetime, date + +import geojson from loguru import logger as log -from fastapi import HTTPException -from pydantic import BaseModel, computed_field +from pydantic import BaseModel, computed_field, Field from pydantic.functional_validators import AfterValidator from pydantic.functional_serializers import PlainSerializer +from geojson_pydantic import Feature, FeatureCollection, Polygon, Point, MultiPolygon +from fastapi import HTTPException from psycopg import Connection from psycopg.rows import class_row -from geojson_pydantic import Feature, FeatureCollection, Polygon, Point, MultiPolygon from slugify import slugify +from app.models.enums import FinalOutput, ProjectVisibility, State from app.models.enums import ( IntEnum, - ProjectVisibility, ProjectStatus, - State, HTTPStatus, ) from app.utils import ( From c0d5ede4ad270fe5429b9e50781e9ac31406970b Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Fri, 9 Aug 2024 15:49:16 +0545 Subject: [PATCH 15/66] refractor: Refactor database interactions to use psycopg. Updated query placeholders, result fetching, and exception handling. Adjusted token expiration handling and password hashing. Updated user and profile operations to align with psycopg syntax. --- src/backend/app/users/user_crud.py | 5 - src/backend/app/users/user_deps.py | 232 ++++++++++++++++++++++---- src/backend/app/users/user_routes.py | 12 +- src/backend/app/users/user_schemas.py | 54 +++++- 4 files changed, 256 insertions(+), 47 deletions(-) diff --git a/src/backend/app/users/user_crud.py b/src/backend/app/users/user_crud.py index ec649102..165aff21 100644 --- a/src/backend/app/users/user_crud.py +++ b/src/backend/app/users/user_crud.py @@ -67,11 +67,6 @@ async def get_user_by_id(db: Connection, id: str): return result -async def get_userprofile_by_userid(db: Connection, user_id: str): - query = "SELECT * FROM user_profile WHERE user_id = :user_id LIMIT 1;" - result = await db.fetch_one(query, {"user_id": user_id}) - return result - async def get_user_by_email(db: Connection, email: str): query = "SELECT * FROM users WHERE email_address = :email LIMIT 1;" diff --git a/src/backend/app/users/user_deps.py b/src/backend/app/users/user_deps.py index 0ba83475..22e95cef 100644 --- a/src/backend/app/users/user_deps.py +++ b/src/backend/app/users/user_deps.py @@ -2,46 +2,18 @@ from app.config import settings from app.users import user_crud from app.users.auth import Auth -from app.users.user_schemas import AuthUser +from app.users.user_schemas import AuthUser, ProfileUpdate from loguru import logger as log +import time +import jwt +from typing import Any, Union +from passlib.context import CryptContext +from app.db import db_models +from pydantic import EmailStr +import psycopg +from psycopg import Connection -# TODO do we need this code anymore? -# reusable_oauth2 = OAuth2PasswordBearer(tokenUrl=f"{settings.API_PREFIX}/users/login") -# # SessionDep = Annotated[ -# # Database, -# # Depends(database.get_db), -# # ] -# SessionDep = Annotated[ -# Session, -# Depends(database.get_sqlalchemy_db), -# ] -# TokenDep = Annotated[str, Depends(reusable_oauth2)] -# def get_current_user(session: SessionDep, token: TokenDep): -# try: -# payload = jwt.decode( -# token, settings.SECRET_KEY, algorithms=[user_crud.ALGORITHM] -# ) -# token_data = user_schemas.TokenPayload(**payload) -# except (InvalidTokenError, ValidationError): -# raise HTTPException( -# status_code=status.HTTP_403_FORBIDDEN, -# detail="Could not validate credentials", -# ) -# user = session.get(DbUser, token_data.sub) -# if not user: -# raise HTTPException(status_code=404, detail="User not found") -# if not user.is_active: -# raise HTTPException(status_code=400, detail="Inactive user") -# return user -# CurrentUser = Annotated[DbUser, Depends(get_current_user)] -# def get_current_active_superuser(current_user: CurrentUser): -# if not current_user.is_superuser: -# raise HTTPException( -# status_code=403, detail="The user doesn't have enough privileges" -# ) -# return current_user - async def init_google_auth(): """Initialise Auth object for google login""" @@ -87,3 +59,189 @@ async def login_required( raise HTTPException(status_code=401, detail="Access token not valid") from e return AuthUser(**user) + + +async def get_userprofile_by_userid(db: Connection, user_id: str): + """Fetch the user profile by user ID.""" + query = """ + SELECT * FROM user_profile + WHERE user_id = %(user_id)s + LIMIT 1; + """ + async with db.cursor() as cur: + await cur.execute(query, {"user_id": user_id}) + result = await cur.fetchone() + log.info(f"Fetched user profile data: {result}") + return result + + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +async def create_access_token(subject: dict[str, Any]) -> tuple[str, str]: + expire = int(time.time()) + settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60 + refresh_expire = int(time.time()) + settings.REFRESH_TOKEN_EXPIRE_MINUTES * 60 + + # Access token + subject["exp"] = expire + access_token = jwt.encode(subject, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + + # Refresh token + subject["exp"] = refresh_expire + refresh_token = jwt.encode(subject, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + + return access_token, refresh_token + +def verify_token(token: str) -> dict[str, Any]: + """Verifies the access token and returns the payload if valid. + + Args: + token (str): The access token to be verified. + + Returns: + dict: The payload of the access token if verification is successful. + + Raises: + HTTPException: If the token has expired or credentials could not be validated. + """ + try: + return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + except jwt.ExpiredSignatureError as e: + raise HTTPException(status_code=401, detail="Token has expired") from e + except Exception as e: + raise HTTPException(status_code=401, detail="Could not validate token") from e + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) + +async def get_user_by_id(db: Connection, id: str) -> dict[str, Any] | None: + query = "SELECT * FROM users WHERE id = %s LIMIT 1;" + async with db.cursor() as cur: + await cur.execute(query, (id,)) + result = await cur.fetchone() + return dict(result) if result else None + +async def get_user_by_email(db: Connection, email: str) -> dict[str, Any] | None: + query = "SELECT * FROM users WHERE email_address = %s LIMIT 1;" + async with db.cursor() as cur: + await cur.execute(query, (email,)) + result = await cur.fetchone() + return dict(result) if result else None + +async def authenticate(db: Connection, email: EmailStr, password: str) -> db_models.DbUser | None: + db_user = await get_user_by_email(db, email) + if not db_user: + return None + if not verify_password(password, db_user["password"]): + return None + return db_user + +async def get_or_create_user(db: Connection, user_data: AuthUser) -> AuthUser: + """Get user from User table if exists, else create.""" + try: + update_sql = """ + INSERT INTO users ( + id, name, email_address, profile_img, is_active, is_superuser, date_registered + ) + VALUES (%s, %s, %s, %s, True, False, now()) + ON CONFLICT (id) + DO UPDATE SET profile_img = EXCLUDED.profile_img; + """ + + async with db.cursor() as cur: + await cur.execute( + update_sql, + ( + str(user_data.id), + user_data.name, + user_data.email, + user_data.img_url + ), + ) + return user_data + + except psycopg.errors.UniqueViolation as e: + if 'users_email_address_key' in str(e): + raise HTTPException( + status_code=400, + detail=f"User with this email {user_data.email} already exists.", + ) from e + else: + raise HTTPException(status_code=400, detail=str(e)) from e + +async def update_user_profile(db: Connection, user_id: int, profile_update: ProfileUpdate) -> bool: + """ + Update user profile in the database. + Args: + db (Connection): Database connection object. + user_id (int): ID of the user whose profile is being updated. + profile_update (ProfileUpdate): Instance of ProfileUpdate containing fields to update. + Returns: + bool: True if update operation succeeds. + Raises: + HTTPException: If the update operation fails. + """ + try: + profile_query = """ + INSERT INTO user_profile ( + user_id, role, phone_number, country, city, organization_name, + organization_address, job_title, notify_for_projects_within_km, + experience_years, drone_you_own, certified_drone_operator + ) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (user_id) + DO UPDATE SET + role = EXCLUDED.role, + phone_number = EXCLUDED.phone_number, + country = EXCLUDED.country, + city = EXCLUDED.city, + organization_name = EXCLUDED.organization_name, + organization_address = EXCLUDED.organization_address, + job_title = EXCLUDED.job_title, + notify_for_projects_within_km = EXCLUDED.notify_for_projects_within_km, + experience_years = EXCLUDED.experience_years, + drone_you_own = EXCLUDED.drone_you_own, + certified_drone_operator = EXCLUDED.certified_drone_operator; + """ + + async with db.cursor() as cur: + await cur.execute( + profile_query, + ( + user_id, + profile_update.role, + profile_update.phone_number, + profile_update.country, + profile_update.city, + profile_update.organization_name, + profile_update.organization_address, + profile_update.job_title, + profile_update.notify_for_projects_within_km, + profile_update.experience_years, + profile_update.drone_you_own, + profile_update.certified_drone_operator + ), + ) + + # If password is provided, update the users table + if profile_update.password: + password_update_query = """ + UPDATE users + SET password = %s + WHERE id = %s; + """ + async with db.cursor() as cur: + await cur.execute( + password_update_query, + ( + get_password_hash(profile_update.password), + user_id + ), + ) + + return True + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) from e diff --git a/src/backend/app/users/user_routes.py b/src/backend/app/users/user_routes.py index eae74869..e66bb5e0 100644 --- a/src/backend/app/users/user_routes.py +++ b/src/backend/app/users/user_routes.py @@ -1,4 +1,6 @@ import os +from app.users import user_schemas +from app.users import user_deps from fastapi import APIRouter, Response, HTTPException, Depends, Request from typing import Annotated from fastapi.security import OAuth2PasswordRequestForm @@ -139,10 +141,12 @@ async def my_data( user_data: Annotated[AuthUser, Depends(login_required)], ): """Read access token and get user details from Google""" - - user_info = await user_crud.get_or_create_user(db, user_data) - has_user_profile = await user_crud.get_userprofile_by_userid(db, user_info.id) - + print("*"*100, user_data) + + user_info = await user_schemas.DbUser.get_or_create_user(db, user_data) + # user_info = await user_crud.get_or_create_user(db, user_data) + # has_user_profile = await user_crud.get_userprofile_by_userid(db, user_info.id) + has_user_profile = await user_deps.get_userprofile_by_userid(db, user_info.id) user_info_dict = user_info.model_dump() user_info_dict["has_user_profile"] = bool(has_user_profile) return user_info_dict diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index 0f5e6238..a985ffa2 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -2,7 +2,11 @@ from pydantic.functional_validators import field_validator from typing import Optional from app.models.enums import UserRole - +from psycopg import Connection +import uuid +from psycopg.rows import class_row +import psycopg +from fastapi import HTTPException class AuthUser(BaseModel): """The user model returned from Google OAuth2.""" @@ -93,3 +97,51 @@ class ProfileUpdate(BaseModel): @classmethod def integer_role_to_string(cls, value: UserRole) -> str: return str(value.name) + + +class DbUser(BaseModel): + id: str + email_address: EmailStr + is_active: bool + is_superuser: bool + name: str + profile_img: Optional[str] = None + + @staticmethod + async def get_or_create_user(db: Connection, user_data: AuthUser): + """Get user from User table if exists, else create.""" + async with db.cursor(row_factory=class_row(DbUser)) as cur: + try: + await cur.execute( + """ + INSERT INTO users ( + id, name, email_address, profile_img, is_active, is_superuser, date_registered + ) + VALUES ( + %(user_id)s, %(name)s, %(email_address)s, %(profile_img)s, True, False, now() + ) + ON CONFLICT (id) + DO UPDATE SET profile_img = EXCLUDED.profile_img + RETURNING *; + """, + { + "user_id": str(user_data.id), + "name": user_data.name, + "email_address": user_data.email, + "profile_img": user_data.img_url, + }, + ) + user = await cur.fetchone() + return user + + except psycopg.IntegrityError as e: + if ( + 'duplicate key value violates unique constraint "users_email_address_key"' + in str(e) + ): + raise HTTPException( + status_code=400, + detail=f"User with this email {user_data.email} already exists.", + ) from e + else: + raise HTTPException(status_code=400, detail=str(e)) from e \ No newline at end of file From fd803f8d180932dea49509b5dc016166d7a42d8c Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Fri, 9 Aug 2024 16:20:05 +0545 Subject: [PATCH 16/66] refractor: refractor the user profile update --- src/backend/app/users/user_crud.py | 7 +- src/backend/app/users/user_deps.py | 49 +++++++----- src/backend/app/users/user_routes.py | 20 ++--- src/backend/app/users/user_schemas.py | 109 +++++++++++++++++++++++++- 4 files changed, 146 insertions(+), 39 deletions(-) diff --git a/src/backend/app/users/user_crud.py b/src/backend/app/users/user_crud.py index 165aff21..870856eb 100644 --- a/src/backend/app/users/user_crud.py +++ b/src/backend/app/users/user_crud.py @@ -4,7 +4,7 @@ from typing import Any from passlib.context import CryptContext from app.db import db_models -from app.users.user_schemas import AuthUser, ProfileUpdate +from app.users.user_schemas import AuthUser, UserProfileIn from fastapi import HTTPException from pydantic import EmailStr from psycopg import Connection @@ -67,7 +67,6 @@ async def get_user_by_id(db: Connection, id: str): return result - async def get_user_by_email(db: Connection, email: str): query = "SELECT * FROM users WHERE email_address = :email LIMIT 1;" result = await db.fetch_one(query, {"email": email}) @@ -127,14 +126,14 @@ async def get_or_create_user( async def update_user_profile( - db: Connection, user_id: int, profile_update: ProfileUpdate + db: Connection, user_id: int, profile_update: UserProfileIn ): """ Update user profile in the database. Args: db (Database): Database connection object. user_id (int): ID of the user whose profile is being updated. - profile_update (ProfileUpdate): Instance of ProfileUpdate containing fields to update. + profile_update (UserProfileIn): Instance of UserProfileIn containing fields to update. Returns: bool: True if update operation succeeds. Raises: diff --git a/src/backend/app/users/user_deps.py b/src/backend/app/users/user_deps.py index 22e95cef..c0cd6e2c 100644 --- a/src/backend/app/users/user_deps.py +++ b/src/backend/app/users/user_deps.py @@ -2,11 +2,11 @@ from app.config import settings from app.users import user_crud from app.users.auth import Auth -from app.users.user_schemas import AuthUser, ProfileUpdate +from app.users.user_schemas import AuthUser, UserProfileIn from loguru import logger as log import time import jwt -from typing import Any, Union +from typing import Any from passlib.context import CryptContext from app.db import db_models from pydantic import EmailStr @@ -14,7 +14,6 @@ from psycopg import Connection - async def init_google_auth(): """Initialise Auth object for google login""" @@ -75,23 +74,28 @@ async def get_userprofile_by_userid(db: Connection, user_id: str): return result - pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + async def create_access_token(subject: dict[str, Any]) -> tuple[str, str]: expire = int(time.time()) + settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60 refresh_expire = int(time.time()) + settings.REFRESH_TOKEN_EXPIRE_MINUTES * 60 # Access token subject["exp"] = expire - access_token = jwt.encode(subject, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + access_token = jwt.encode( + subject, settings.SECRET_KEY, algorithm=settings.ALGORITHM + ) # Refresh token subject["exp"] = refresh_expire - refresh_token = jwt.encode(subject, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + refresh_token = jwt.encode( + subject, settings.SECRET_KEY, algorithm=settings.ALGORITHM + ) return access_token, refresh_token + def verify_token(token: str) -> dict[str, Any]: """Verifies the access token and returns the payload if valid. @@ -111,18 +115,22 @@ def verify_token(token: str) -> dict[str, Any]: except Exception as e: raise HTTPException(status_code=401, detail="Could not validate token") from e + def verify_password(plain_password: str, hashed_password: str) -> bool: return pwd_context.verify(plain_password, hashed_password) + def get_password_hash(password: str) -> str: return pwd_context.hash(password) + async def get_user_by_id(db: Connection, id: str) -> dict[str, Any] | None: query = "SELECT * FROM users WHERE id = %s LIMIT 1;" async with db.cursor() as cur: await cur.execute(query, (id,)) result = await cur.fetchone() - return dict(result) if result else None + return result if result else None + async def get_user_by_email(db: Connection, email: str) -> dict[str, Any] | None: query = "SELECT * FROM users WHERE email_address = %s LIMIT 1;" @@ -131,7 +139,10 @@ async def get_user_by_email(db: Connection, email: str) -> dict[str, Any] | None result = await cur.fetchone() return dict(result) if result else None -async def authenticate(db: Connection, email: EmailStr, password: str) -> db_models.DbUser | None: + +async def authenticate( + db: Connection, email: EmailStr, password: str +) -> db_models.DbUser | None: db_user = await get_user_by_email(db, email) if not db_user: return None @@ -139,6 +150,7 @@ async def authenticate(db: Connection, email: EmailStr, password: str) -> db_mod return None return db_user + async def get_or_create_user(db: Connection, user_data: AuthUser) -> AuthUser: """Get user from User table if exists, else create.""" try: @@ -154,17 +166,12 @@ async def get_or_create_user(db: Connection, user_data: AuthUser) -> AuthUser: async with db.cursor() as cur: await cur.execute( update_sql, - ( - str(user_data.id), - user_data.name, - user_data.email, - user_data.img_url - ), + (str(user_data.id), user_data.name, user_data.email, user_data.img_url), ) return user_data except psycopg.errors.UniqueViolation as e: - if 'users_email_address_key' in str(e): + if "users_email_address_key" in str(e): raise HTTPException( status_code=400, detail=f"User with this email {user_data.email} already exists.", @@ -172,7 +179,10 @@ async def get_or_create_user(db: Connection, user_data: AuthUser) -> AuthUser: else: raise HTTPException(status_code=400, detail=str(e)) from e -async def update_user_profile(db: Connection, user_id: int, profile_update: ProfileUpdate) -> bool: + +async def update_user_profile( + db: Connection, user_id: int, profile_update: UserProfileIn +) -> bool: """ Update user profile in the database. Args: @@ -222,7 +232,7 @@ async def update_user_profile(db: Connection, user_id: int, profile_update: Prof profile_update.notify_for_projects_within_km, profile_update.experience_years, profile_update.drone_you_own, - profile_update.certified_drone_operator + profile_update.certified_drone_operator, ), ) @@ -236,10 +246,7 @@ async def update_user_profile(db: Connection, user_id: int, profile_update: Prof async with db.cursor() as cur: await cur.execute( password_update_query, - ( - get_password_hash(profile_update.password), - user_id - ), + (get_password_hash(profile_update.password), user_id), ) return True diff --git a/src/backend/app/users/user_routes.py b/src/backend/app/users/user_routes.py index e66bb5e0..37dcd2f4 100644 --- a/src/backend/app/users/user_routes.py +++ b/src/backend/app/users/user_routes.py @@ -6,7 +6,7 @@ from fastapi.security import OAuth2PasswordRequestForm from app.users.user_schemas import ( Token, - ProfileUpdate, + UserProfileIn, AuthUser, ) from app.users.user_deps import login_required, init_google_auth @@ -61,7 +61,7 @@ async def login_access_token( @router.post("/{user_id}/profile") async def update_user_profile( user_id: str, - profile_update: ProfileUpdate, + profile_update: UserProfileIn, db: Annotated[Connection, Depends(database.get_db)], user_data: Annotated[AuthUser, Depends(login_required)], ): @@ -69,14 +69,16 @@ async def update_user_profile( Update user profile based on provided user_id and profile_update data. Args: user_id (int): The ID of the user whose profile is being updated. - profile_update (UserProfileUpdate): Updated profile data to apply. + profile_update (UserUserProfileIn): Updated profile data to apply. Returns: dict: Updated user profile information. Raises: HTTPException: If user with given user_id is not found in the database. """ - user = await user_crud.get_user_by_id(db, user_id) + # user = await user_crud.get_user_by_id(db, user_id) + user = await user_deps.get_user_by_id(db, user_id) + if user_data.id != user_id: raise HTTPException( status_code=HTTPStatus.FORBIDDEN, @@ -85,8 +87,9 @@ async def update_user_profile( if not user: raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="User not found") + user = await user_schemas.DbUserProfile.update(db, user_id, profile_update) - user = await user_crud.update_user_profile(db, user_id, profile_update) + # user = await user_crud.update_user_profile(db, user_id, profile_update) return Response(status_code=HTTPStatus.OK) @@ -141,12 +144,9 @@ async def my_data( user_data: Annotated[AuthUser, Depends(login_required)], ): """Read access token and get user details from Google""" - print("*"*100, user_data) - + user_info = await user_schemas.DbUser.get_or_create_user(db, user_data) - # user_info = await user_crud.get_or_create_user(db, user_data) - # has_user_profile = await user_crud.get_userprofile_by_userid(db, user_info.id) - has_user_profile = await user_deps.get_userprofile_by_userid(db, user_info.id) + has_user_profile = await user_deps.get_userprofile_by_userid(db, user_info.id) user_info_dict = user_info.model_dump() user_info_dict["has_user_profile"] = bool(has_user_profile) return user_info_dict diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index a985ffa2..05eb98ec 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -1,12 +1,14 @@ +from app.users import user_deps +from app.models.enums import HTTPStatus, UserRole from pydantic import BaseModel, EmailStr, ValidationInfo, Field from pydantic.functional_validators import field_validator from typing import Optional -from app.models.enums import UserRole from psycopg import Connection -import uuid from psycopg.rows import class_row import psycopg from fastapi import HTTPException +from loguru import logger as log + class AuthUser(BaseModel): """The user model returned from Google OAuth2.""" @@ -79,7 +81,7 @@ class UserCreate(UserBase): password: str -class ProfileUpdate(BaseModel): +class UserProfileIn(BaseModel): phone_number: Optional[str] = None country: Optional[str] = None city: Optional[str] = None @@ -99,6 +101,105 @@ def integer_role_to_string(cls, value: UserRole) -> str: return str(value.name) +class DbUserProfile(BaseModel): + """UserProfile model for interacting with the user_profile table.""" + + user_id: int + role: Optional[str] = None + phone_number: Optional[str] = None + country: Optional[str] = None + city: Optional[str] = None + organization_name: Optional[str] = None + organization_address: Optional[str] = None + job_title: Optional[str] = None + notify_for_projects_within_km: Optional[int] = None + experience_years: Optional[int] = None + drone_you_own: Optional[str] = None + certified_drone_operator: Optional[bool] = None + role: Optional[UserRole] = None + password: Optional[str] = None + # TODO add all remaining user profile fields and validators + + @field_validator("role", mode="after") + @classmethod + def integer_role_to_string(cls, value: UserRole) -> str: + return str(value.name) + + @staticmethod + async def update(db: Connection, user_id: int, profile_update: UserProfileIn): + """Update or insert a user profile.""" + + # Check if the user profile exists + async with db.cursor() as cur: + sql = """ + SELECT EXISTS ( + SELECT 1 + FROM user_profile + WHERE user_id = %(user_id)s + ) + """ + await cur.execute(sql, {"user_id": user_id}) + profile_exists = await cur.fetchone() + if not profile_exists[0]: + msg = f"User profile with ID ({user_id}) does not exist!" + log.warning(f"User ({user_id}) failed profile update: {msg}") + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail=msg) + + # Prepare data for insert or update + model_dump = profile_update.model_dump(exclude_none=True, exclude=["password"]) + columns = ", ".join(model_dump.keys()) + print(columns) + value_placeholders = ", ".join(f"%({key})s" for key in model_dump.keys()) + sql = f""" + INSERT INTO user_profile ( + user_id, {columns} + ) + VALUES ( + %(user_id)s, {value_placeholders} + ) + ON CONFLICT (user_id) + DO UPDATE SET + {', '.join(f"{key} = EXCLUDED.{key}" for key in model_dump.keys())}; + """ + + # Prepare password update query if a new password is provided + password_update_query = """ + UPDATE users + SET password = %(password)s + WHERE id = %(user_id)s; + """ + + model_dump["user_id"] = user_id + + async with db.cursor() as cur: + await cur.execute(sql, model_dump) + + if profile_update.password: + # Update password if provided + await cur.execute( + password_update_query, + { + "password": user_deps.get_password_hash( + profile_update.password + ), + "user_id": user_id, + }, + ) + + # Check if the profile was updated successfully + await cur.execute("SELECT 1") + result = await cur.fetchone() + + if not result: + msg = f"Unknown SQL error for data: {model_dump}" + log.warning(f"User ({user_id}) failed profile update: {msg}") + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=msg + ) + + return True + + class DbUser(BaseModel): id: str email_address: EmailStr @@ -144,4 +245,4 @@ async def get_or_create_user(db: Connection, user_data: AuthUser): detail=f"User with this email {user_data.email} already exists.", ) from e else: - raise HTTPException(status_code=400, detail=str(e)) from e \ No newline at end of file + raise HTTPException(status_code=400, detail=str(e)) from e From e91599f433b7e5a512495a65fb8106c220817aa6 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Fri, 9 Aug 2024 16:50:56 +0545 Subject: [PATCH 17/66] refractor: refractor over user module --- src/backend/app/tasks/task_routes.py | 2 +- src/backend/app/users/user_crud.py | 199 -------------------------- src/backend/app/users/user_deps.py | 79 +--------- src/backend/app/users/user_routes.py | 12 +- src/backend/app/users/user_schemas.py | 16 ++- 5 files changed, 16 insertions(+), 292 deletions(-) delete mode 100644 src/backend/app/users/user_crud.py diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 7bcb00ba..3c31f4ed 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -6,7 +6,7 @@ from app.tasks import task_schemas, task_crud from app.users.user_deps import login_required from app.users.user_schemas import AuthUser -from app.users.user_crud import get_user_by_id +from app.users.user_deps import get_user_by_id from psycopg import Connection from app.db import database from app.utils import send_notification_email, render_email_template diff --git a/src/backend/app/users/user_crud.py b/src/backend/app/users/user_crud.py deleted file mode 100644 index 870856eb..00000000 --- a/src/backend/app/users/user_crud.py +++ /dev/null @@ -1,199 +0,0 @@ -import time -import jwt -from app.config import settings -from typing import Any -from passlib.context import CryptContext -from app.db import db_models -from app.users.user_schemas import AuthUser, UserProfileIn -from fastapi import HTTPException -from pydantic import EmailStr -from psycopg import Connection - - -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - - -async def create_access_token(subject: str | Any): - expire = int(time.time()) + settings.ACCESS_TOKEN_EXPIRE_MINUTES - refresh_expire = int(time.time()) + settings.REFRESH_TOKEN_EXPIRE_MINUTES - - # access token - subject["exp"] = expire - access_token = jwt.encode( - subject, settings.SECRET_KEY, algorithm=settings.ALGORITHM - ) - - # refresh token - subject["exp"] = refresh_expire - refresh_token = jwt.encode( - subject, settings.SECRET_KEY, algorithm=settings.ALGORITHM - ) - - return access_token, refresh_token - - -def verify_token(token: str): - """Verifies the access token and returns the payload if valid. - - Args: - token (str): The access token to be verified. - - Returns: - dict: The payload of the access token if verification is successful. - - Raises: - HTTPException: If the token has expired or credentials could not be validated. - """ - secret_key = settings.SECRET_KEY - try: - return jwt.decode(token, str(secret_key), algorithms=[settings.ALGORITHM]) - except jwt.ExpiredSignatureError as e: - raise HTTPException(status_code=401, detail="Token has expired") from e - except Exception as e: - raise HTTPException(status_code=401, detail="Could not validate token") from e - - -def verify_password(plain_password: str, hashed_password: str) -> bool: - return pwd_context.verify(plain_password, hashed_password) - - -def get_password_hash(password: str) -> str: - return pwd_context.hash(password) - - -async def get_user_by_id(db: Connection, id: str): - query = "SELECT * FROM users WHERE id = :id LIMIT 1;" - result = await db.fetch_one(query, {"id": id}) - return result - - -async def get_user_by_email(db: Connection, email: str): - query = "SELECT * FROM users WHERE email_address = :email LIMIT 1;" - result = await db.fetch_one(query, {"email": email}) - return result - - -async def authenticate( - db: Connection, email: EmailStr, password: str -) -> db_models.DbUser | None: - db_user = await get_user_by_email(db, email) - if not db_user: - return None - if not verify_password(password, db_user["password"]): - return None - return db_user - - -async def get_or_create_user( - db: Connection, - user_data: AuthUser, -): - """Get user from User table if exists, else create.""" - try: - update_sql = """ - INSERT INTO users ( - id, name, email_address, profile_img, is_active, is_superuser, date_registered - ) - VALUES ( - :user_id, :name, :email_address, :profile_img, True, False, now() - ) - ON CONFLICT (id) - DO UPDATE SET profile_img = :profile_img; - """ - - await db.execute( - update_sql, - { - "user_id": str(user_data.id), - "name": user_data.name, - "email_address": user_data.email, - "profile_img": user_data.img_url, - }, - ) - return user_data - - except Exception as e: - if ( - 'duplicate key value violates unique constraint "users_email_address_key"' - in str(e) - ): - raise HTTPException( - status_code=400, - detail=f"User with this email {user_data.email} already exists.", - ) from e - else: - raise HTTPException(status_code=400, detail=str(e)) from e - - -async def update_user_profile( - db: Connection, user_id: int, profile_update: UserProfileIn -): - """ - Update user profile in the database. - Args: - db (Database): Database connection object. - user_id (int): ID of the user whose profile is being updated. - profile_update (UserProfileIn): Instance of UserProfileIn containing fields to update. - Returns: - bool: True if update operation succeeds. - Raises: - Any exceptions thrown during database operations. - """ - - try: - profile_query = """ - INSERT INTO user_profile (user_id, role, phone_number, country, city, organization_name, organization_address, job_title, notify_for_projects_within_km, - experience_years, drone_you_own, certified_drone_operator) - VALUES (:user_id, :role, :phone_number, :country, :city, :organization_name, :organization_address, :job_title, :notify_for_projects_within_km , - :experience_years, :drone_you_own, :certified_drone_operator) - ON CONFLICT (user_id) - DO UPDATE SET - role = :role, - phone_number = :phone_number, - country = :country, - city = :city, - organization_name = :organization_name, - organization_address = :organization_address, - job_title = :job_title, - notify_for_projects_within_km = :notify_for_projects_within_km, - experience_years = :experience_years, - drone_you_own = :drone_you_own, - certified_drone_operator = :certified_drone_operator; - """ - - await db.execute( - profile_query, - { - "user_id": user_id, - "role": profile_update.role, - "phone_number": profile_update.phone_number, - "country": profile_update.country, - "city": profile_update.city, - "organization_name": profile_update.organization_name, - "organization_address": profile_update.organization_address, - "job_title": profile_update.job_title, - "notify_for_projects_within_km": profile_update.notify_for_projects_within_km, - "experience_years": profile_update.experience_years, - "drone_you_own": profile_update.drone_you_own, - "certified_drone_operator": profile_update.certified_drone_operator, - }, - ) - - # If password is provided, update the users table - if profile_update.password: - password_update_query = """ - UPDATE users - SET password = :password - WHERE id = :user_id; - """ - await db.execute( - password_update_query, - { - "password": get_password_hash(profile_update.password), - "user_id": user_id, - }, - ) - - return True - except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) from e diff --git a/src/backend/app/users/user_deps.py b/src/backend/app/users/user_deps.py index c0cd6e2c..03d63f66 100644 --- a/src/backend/app/users/user_deps.py +++ b/src/backend/app/users/user_deps.py @@ -1,8 +1,7 @@ from fastapi import HTTPException, Request, Header from app.config import settings -from app.users import user_crud from app.users.auth import Auth -from app.users.user_schemas import AuthUser, UserProfileIn +from app.users.user_schemas import AuthUser from loguru import logger as log import time import jwt @@ -51,7 +50,7 @@ async def login_required( raise HTTPException(status_code=401, detail="No access token provided") try: - user = user_crud.verify_token(access_token) + user = verify_token(access_token) except HTTPException as e: log.error(e) log.error("Failed to verify access token") @@ -178,77 +177,3 @@ async def get_or_create_user(db: Connection, user_data: AuthUser) -> AuthUser: ) from e else: raise HTTPException(status_code=400, detail=str(e)) from e - - -async def update_user_profile( - db: Connection, user_id: int, profile_update: UserProfileIn -) -> bool: - """ - Update user profile in the database. - Args: - db (Connection): Database connection object. - user_id (int): ID of the user whose profile is being updated. - profile_update (ProfileUpdate): Instance of ProfileUpdate containing fields to update. - Returns: - bool: True if update operation succeeds. - Raises: - HTTPException: If the update operation fails. - """ - try: - profile_query = """ - INSERT INTO user_profile ( - user_id, role, phone_number, country, city, organization_name, - organization_address, job_title, notify_for_projects_within_km, - experience_years, drone_you_own, certified_drone_operator - ) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) - ON CONFLICT (user_id) - DO UPDATE SET - role = EXCLUDED.role, - phone_number = EXCLUDED.phone_number, - country = EXCLUDED.country, - city = EXCLUDED.city, - organization_name = EXCLUDED.organization_name, - organization_address = EXCLUDED.organization_address, - job_title = EXCLUDED.job_title, - notify_for_projects_within_km = EXCLUDED.notify_for_projects_within_km, - experience_years = EXCLUDED.experience_years, - drone_you_own = EXCLUDED.drone_you_own, - certified_drone_operator = EXCLUDED.certified_drone_operator; - """ - - async with db.cursor() as cur: - await cur.execute( - profile_query, - ( - user_id, - profile_update.role, - profile_update.phone_number, - profile_update.country, - profile_update.city, - profile_update.organization_name, - profile_update.organization_address, - profile_update.job_title, - profile_update.notify_for_projects_within_km, - profile_update.experience_years, - profile_update.drone_you_own, - profile_update.certified_drone_operator, - ), - ) - - # If password is provided, update the users table - if profile_update.password: - password_update_query = """ - UPDATE users - SET password = %s - WHERE id = %s; - """ - async with db.cursor() as cur: - await cur.execute( - password_update_query, - (get_password_hash(profile_update.password), user_id), - ) - - return True - except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) from e diff --git a/src/backend/app/users/user_routes.py b/src/backend/app/users/user_routes.py index 37dcd2f4..803555d6 100644 --- a/src/backend/app/users/user_routes.py +++ b/src/backend/app/users/user_routes.py @@ -11,7 +11,6 @@ ) from app.users.user_deps import login_required, init_google_auth from app.config import settings -from app.users import user_crud from app.db import database from app.models.enums import HTTPStatus from psycopg import Connection @@ -38,7 +37,7 @@ async def login_access_token( """ OAuth2 compatible token login, get an access token for future requests """ - user = await user_crud.authenticate(db, form_data.username, form_data.password) + user = await user_deps.authenticate(db, form_data.username, form_data.password) if not user: raise HTTPException(status_code=400, detail="Incorrect email or password") @@ -52,7 +51,7 @@ async def login_access_token( "img_url": user.profile_img, } - access_token, refresh_token = await user_crud.create_access_token(user_info) + access_token, refresh_token = await user_deps.create_access_token(user_info) return Token(access_token=access_token, refresh_token=refresh_token) @@ -76,7 +75,6 @@ async def update_user_profile( HTTPException: If user with given user_id is not found in the database. """ - # user = await user_crud.get_user_by_id(db, user_id) user = await user_deps.get_user_by_id(db, user_id) if user_data.id != user_id: @@ -88,8 +86,6 @@ async def update_user_profile( if not user: raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="User not found") user = await user_schemas.DbUserProfile.update(db, user_id, profile_update) - - # user = await user_crud.update_user_profile(db, user_id, profile_update) return Response(status_code=HTTPStatus.OK) @@ -123,7 +119,7 @@ async def callback(request: Request, google_auth=Depends(init_google_auth)): access_token = google_auth.callback(callback_url).get("access_token") user_data = google_auth.deserialize_access_token(access_token) - access_token, refresh_token = await user_crud.create_access_token(user_data) + access_token, refresh_token = await user_deps.create_access_token(user_data) return Token(access_token=access_token, refresh_token=refresh_token) @@ -132,7 +128,7 @@ async def callback(request: Request, google_auth=Depends(init_google_auth)): async def update_token(user_data: Annotated[AuthUser, Depends(login_required)]): """Refresh access token""" - access_token, refresh_token = await user_crud.create_access_token( + access_token, refresh_token = await user_deps.create_access_token( user_data.model_dump() ) return Token(access_token=access_token, refresh_token=refresh_token) diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index 05eb98ec..93a72930 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -118,7 +118,6 @@ class DbUserProfile(BaseModel): certified_drone_operator: Optional[bool] = None role: Optional[UserRole] = None password: Optional[str] = None - # TODO add all remaining user profile fields and validators @field_validator("role", mode="after") @classmethod @@ -187,17 +186,20 @@ async def update(db: Connection, user_id: int, profile_update: UserProfileIn): ) # Check if the profile was updated successfully - await cur.execute("SELECT 1") - result = await cur.fetchone() + fetch_sql = """ + SELECT * FROM user_profile WHERE user_id = %(user_id)s; + """ + await cur.execute(fetch_sql, {"user_id": user_id}) + updated_profile = await cur.fetchone() - if not result: - msg = f"Unknown SQL error for data: {model_dump}" - log.warning(f"User ({user_id}) failed profile update: {msg}") + if not updated_profile: + msg = f"Failed to fetch updated profile for user ID: {user_id}" + log.warning(f"User ({user_id}) failed profile fetch: {msg}") raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=msg ) - return True + return True class DbUser(BaseModel): From 388b5ccb027a30d6d8e2597ca02bf49404e055e3 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 17:07:50 +0545 Subject: [PATCH 18/66] feat: DbDrone schema with crud functions --- src/backend/app/drones/drone_schemas.py | 97 +++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/backend/app/drones/drone_schemas.py b/src/backend/app/drones/drone_schemas.py index f92cb136..181b33f0 100644 --- a/src/backend/app/drones/drone_schemas.py +++ b/src/backend/app/drones/drone_schemas.py @@ -1,4 +1,8 @@ from pydantic import BaseModel +from fastapi import HTTPException +from app.models.enums import HTTPStatus +from psycopg import Connection +from psycopg.rows import class_row class DroneIn(BaseModel): @@ -19,3 +23,96 @@ class DroneIn(BaseModel): class DroneOut(BaseModel): id: int model: str + + +class DbDrone(BaseModel): + id: int + model: str + manufacturer: str + camera_model: str + sensor_width: float + sensor_height: float + max_battery_health: int + focal_length: float + image_width: int + image_height: int + max_altitude: int + max_speed: float + weight: int + + @staticmethod + async def one(db: Connection, drone_id: int): + """Get a single project by it's ID, including tasks and task count.""" + async with db.cursor(row_factory=class_row(DbDrone)) as cur: + await cur.execute( + """ + SELECT * FROM drones d + WHERE + d.id = %(drone_id)s + GROUP BY + p.id; + """, + {"drone_id": drone_id}, + ) + drone = await cur.fetchone() + + if not drone: + raise KeyError(f"Drone {drone_id} not found") + + return drone + + @staticmethod + async def all(db: Connection): + """Get all projects, including tasks and task count.""" + async with db.cursor(row_factory=class_row(DbDrone)) as cur: + await cur.execute( + """ + SELECT * FROM drones d + GROUP BY d.id; + """ + ) + drones = await cur.fetchall() + + if not drones: + raise KeyError("No drones found") + return drones + + @staticmethod + async def create(db: Connection, drone: DroneIn): + """Create a single drone.""" + # NOTE we first check if a drone with this model name exists + async with db.cursor() as cur: + sql = """ + SELECT EXISTS ( + SELECT 1 + FROM drones + WHERE LOWER(model) = %(model_name)s + ) + """ + await cur.execute(sql, {"model_name": drone.model.lower()}) + project_exists = await cur.fetchone() + if project_exists[0]: + msg = f"Drone ({drone.model}) already exists!" + raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=msg) + + # If drone with the same model does not already exists, add a new one. + model_dump = drone.model_dump() + columns = ", ".join(model_dump.keys()) + value_placeholders = ", ".join(f"%({key})s" for key in model_dump.keys()) + + sql = f""" + INSERT INTO drones ({columns}, created) + VALUES ({value_placeholders}, NOW()) + RETURNING id; + """ + + async with db.cursor() as cur: + await cur.execute(sql, model_dump) + new_drone_id = await cur.fetchone() + + if not new_drone_id: + msg = f"Unknown SQL error for data: {model_dump}" + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=msg + ) + return new_drone_id[0] From 326da39a4b6acd9f4a21c797efc059842b3781b3 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 17:13:45 +0545 Subject: [PATCH 19/66] drone crud with psycopg and pydantic schemas --- src/backend/app/drones/drone_routes.py | 59 +++++++++----------------- 1 file changed, 20 insertions(+), 39 deletions(-) diff --git a/src/backend/app/drones/drone_routes.py b/src/backend/app/drones/drone_routes.py index 4fee8b4f..0ac631fb 100644 --- a/src/backend/app/drones/drone_routes.py +++ b/src/backend/app/drones/drone_routes.py @@ -8,35 +8,38 @@ from app.drones import drone_schemas from psycopg import Connection from app.drones import drone_crud -from typing import List router = APIRouter( prefix=f"{settings.API_PREFIX}/drones", + tags=["Drones"], responses={404: {"description": "Not found"}}, ) -@router.get("/", tags=["Drones"], response_model=List[drone_schemas.DroneOut]) +@router.get("/", response_model=list[drone_schemas.DroneOut]) async def read_drones( db: Annotated[Connection, Depends(database.get_db)], - user_data: Annotated[AuthUser, Depends(login_required)], ): - """ - Retrieves all drone records from the database. + """Get all drones.""" + try: + return await drone_schemas.DbDrone.all(db) + except KeyError as e: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) from e - Args: - db (Database, optional): The database session object. - user_data (AuthUser, optional): The authenticated user data. - Returns: - List[drone_schemas.DroneOut]: A list of all drone records. - """ - drones = await drone_crud.read_all_drones(db) - return drones +@router.post("/create_drone") +async def create_drone( + drone_info: drone_schemas.DroneIn, + db: Annotated[Connection, Depends(database.get_db)], + user_data: Annotated[AuthUser, Depends(login_required)], +): + """Create a new drone in database""" + drone_id = await drone_schemas.DbDrone.create(db, drone_info) + return {"message": "Drone created successfully", "drone_id": drone_id} -@router.delete("/{drone_id}", tags=["Drones"]) +@router.delete("/{drone_id}") async def delete_drone( drone_id: int, db: Annotated[Connection, Depends(database.get_db)], @@ -53,37 +56,15 @@ async def delete_drone( Returns: dict: A success message if the drone was deleted. """ + + # TODO: Check user role, Admin can only do this + success = await drone_crud.delete_drone(db, drone_id) if not success: raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Drone not found") return {"message": "Drone deleted successfully"} -@router.post("/create_drone", tags=["Drones"]) -async def create_drone( - drone_info: drone_schemas.DroneIn, - db: Annotated[Connection, Depends(database.get_db)], - user_data: Annotated[AuthUser, Depends(login_required)], -): - """ - Creates a new drone record in the database. - - Args: - drone_info (drone_schemas.DroneIn): The schema object containing drone details. - db (Database, optional): The database session object. - user_data (AuthUser, optional): The authenticated user data. - - Returns: - dict: A dictionary containing a success message and the ID of the newly created drone. - """ - drone_id = await drone_crud.create_drone(db, drone_info) - if not drone_id: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, detail="Drone creation failed" - ) - return {"message": "Drone created successfully", "drone_id": drone_id} - - @router.get("/{drone_id}", tags=["Drones"], response_model=drone_schemas.DroneOut) async def read_drone( drone_id: int, From 52c863e15c6a12ccc5c46fb41754b8c4ef75dc18 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 17:17:17 +0545 Subject: [PATCH 20/66] remove create_drone function from drones --- src/backend/app/drones/drone_crud.py | 42 ---------------------------- 1 file changed, 42 deletions(-) diff --git a/src/backend/app/drones/drone_crud.py b/src/backend/app/drones/drone_crud.py index 1494091f..b99c875f 100644 --- a/src/backend/app/drones/drone_crud.py +++ b/src/backend/app/drones/drone_crud.py @@ -1,4 +1,3 @@ -from app.drones import drone_schemas from app.models.enums import HTTPStatus from loguru import logger as log from fastapi import HTTPException @@ -88,44 +87,3 @@ async def get_drone(db: Connection, drone_id: int): raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Retrieval failed" ) from e - - -async def create_drone(db: Connection, drone_info: drone_schemas.DroneIn): - """ - Creates a new drone record in the database. - - Args: - db (Database): The database connection object. - drone (drone_schemas.DroneIn): The schema object containing drone details. - - Returns: - The ID of the newly created drone record. - """ - try: - insert_query = """ - INSERT INTO drones ( - model, manufacturer, camera_model, sensor_width, sensor_height, - max_battery_health, focal_length, image_width, image_height, - max_altitude, max_speed, weight, created - ) VALUES ( - :model, :manufacturer, :camera_model, :sensor_width, :sensor_height, - :max_battery_health, :focal_length, :image_width, :image_height, - :max_altitude, :max_speed, :weight, CURRENT_TIMESTAMP - ) - RETURNING id - """ - result = await db.execute(insert_query, drone_info.__dict__) - return result - - # except UniqueViolationError as e: - # log.exception("Unique constraint violation: %s", e) - # raise HTTPException( - # status_code=HTTPStatus.CONFLICT, - # detail="A drone with this model already exists", - # ) - - except Exception as e: - log.exception(e) - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Drone creation failed" - ) from e From e402893b1263b2aade50802f916103674da7d159 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 17:25:03 +0545 Subject: [PATCH 21/66] feat: drone deps to get one drone --- src/backend/app/drones/drone_deps.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/backend/app/drones/drone_deps.py diff --git a/src/backend/app/drones/drone_deps.py b/src/backend/app/drones/drone_deps.py new file mode 100644 index 00000000..bb42dc0e --- /dev/null +++ b/src/backend/app/drones/drone_deps.py @@ -0,0 +1,20 @@ +from typing import Annotated +from fastapi import Depends, HTTPException, Path +from psycopg import Connection +from app.db import database +from app.drones.drone_schemas import DbDrone +from app.models.enums import HTTPStatus + + +async def get_drone_by_id( + project_id: Annotated[ + id, + Path(description="Drone ID."), + ], + db: Annotated[Connection, Depends(database.get_db)], +) -> DbDrone: + """Get a single project by id.""" + try: + return await DbDrone.one(db, project_id) + except KeyError as e: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) from e From 04e92842e596f26d55e60f56115ca3f2709cc8c1 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 18:04:02 +0545 Subject: [PATCH 22/66] fix: drone schemas to delete drone --- src/backend/app/drones/drone_deps.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/app/drones/drone_deps.py b/src/backend/app/drones/drone_deps.py index bb42dc0e..7060acfa 100644 --- a/src/backend/app/drones/drone_deps.py +++ b/src/backend/app/drones/drone_deps.py @@ -7,14 +7,14 @@ async def get_drone_by_id( - project_id: Annotated[ - id, + drone_id: Annotated[ + int, Path(description="Drone ID."), ], db: Annotated[Connection, Depends(database.get_db)], ) -> DbDrone: """Get a single project by id.""" try: - return await DbDrone.one(db, project_id) + return await DbDrone.one(db, drone_id) except KeyError as e: raise HTTPException(status_code=HTTPStatus.NOT_FOUND) from e From 538a0f42c538911ec3854ca6cd1cf0602c497c5a Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 18:04:43 +0545 Subject: [PATCH 23/66] delete drone schemas --- src/backend/app/drones/drone_schemas.py | 31 +++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/backend/app/drones/drone_schemas.py b/src/backend/app/drones/drone_schemas.py index 181b33f0..fecbdedc 100644 --- a/src/backend/app/drones/drone_schemas.py +++ b/src/backend/app/drones/drone_schemas.py @@ -42,15 +42,13 @@ class DbDrone(BaseModel): @staticmethod async def one(db: Connection, drone_id: int): - """Get a single project by it's ID, including tasks and task count.""" + """Get a single drone by it's ID""" + print("drone_id = ", drone_id) async with db.cursor(row_factory=class_row(DbDrone)) as cur: await cur.execute( """ - SELECT * FROM drones d - WHERE - d.id = %(drone_id)s - GROUP BY - p.id; + SELECT * FROM drones + WHERE id = %(drone_id)s; """, {"drone_id": drone_id}, ) @@ -63,7 +61,7 @@ async def one(db: Connection, drone_id: int): @staticmethod async def all(db: Connection): - """Get all projects, including tasks and task count.""" + """Get all drones""" async with db.cursor(row_factory=class_row(DbDrone)) as cur: await cur.execute( """ @@ -77,6 +75,25 @@ async def all(db: Connection): raise KeyError("No drones found") return drones + @staticmethod + async def delete(db: Connection, drone_id: int): + """Delete a single drone by its ID.""" + async with db.cursor() as cur: + await cur.execute( + """ + DELETE FROM drones + WHERE id = %(drone_id)s + RETURNING id; + """, + {"drone_id": drone_id}, + ) + deleted_drone_id = await cur.fetchone() + + if not deleted_drone_id: + raise KeyError(f"Drone {drone_id} not found or could not be deleted") + + return deleted_drone_id[0] + @staticmethod async def create(db: Connection, drone: DroneIn): """Create a single drone.""" From 11f0a4ef5f4b400e1747567cfed7ea34355ae9a6 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 18:05:41 +0545 Subject: [PATCH 24/66] update: drone cruds --- src/backend/app/drones/drone_routes.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/backend/app/drones/drone_routes.py b/src/backend/app/drones/drone_routes.py index 0ac631fb..137bc16d 100644 --- a/src/backend/app/drones/drone_routes.py +++ b/src/backend/app/drones/drone_routes.py @@ -5,7 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException from app.db import database from app.config import settings -from app.drones import drone_schemas +from app.drones import drone_schemas, drone_deps from psycopg import Connection from app.drones import drone_crud @@ -41,7 +41,7 @@ async def create_drone( @router.delete("/{drone_id}") async def delete_drone( - drone_id: int, + drone: Annotated[drone_schemas.DbDrone, Depends(drone_deps.get_drone_by_id)], db: Annotated[Connection, Depends(database.get_db)], user_data: Annotated[AuthUser, Depends(login_required)], ): @@ -57,12 +57,10 @@ async def delete_drone( dict: A success message if the drone was deleted. """ - # TODO: Check user role, Admin can only do this - - success = await drone_crud.delete_drone(db, drone_id) - if not success: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Drone not found") - return {"message": "Drone deleted successfully"} + # TODO: Check user role, Admin can only do this. + # After user roles introduction + drone_id = await drone_schemas.DbDrone.delete(db, drone.id) + return {"message": f"Drone successfully deleted {drone_id}"} @router.get("/{drone_id}", tags=["Drones"], response_model=drone_schemas.DroneOut) From 62ff03bb6682c9a13c842737660e1fb9b6139c59 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 18:10:14 +0545 Subject: [PATCH 25/66] get a single drone --- src/backend/app/drones/drone_routes.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/backend/app/drones/drone_routes.py b/src/backend/app/drones/drone_routes.py index 137bc16d..0f3611da 100644 --- a/src/backend/app/drones/drone_routes.py +++ b/src/backend/app/drones/drone_routes.py @@ -7,7 +7,6 @@ from app.config import settings from app.drones import drone_schemas, drone_deps from psycopg import Connection -from app.drones import drone_crud router = APIRouter( @@ -63,9 +62,9 @@ async def delete_drone( return {"message": f"Drone successfully deleted {drone_id}"} -@router.get("/{drone_id}", tags=["Drones"], response_model=drone_schemas.DroneOut) +@router.get("/{drone_id}", response_model=drone_schemas.DroneOut) async def read_drone( - drone_id: int, + drone: Annotated[drone_schemas.DbDrone, Depends(drone_deps.get_drone_by_id)], db: Annotated[Connection, Depends(database.get_db)], user_data: Annotated[AuthUser, Depends(login_required)], ): @@ -80,7 +79,4 @@ async def read_drone( Returns: dict: The drone record if found. """ - drone = await drone_crud.get_drone(db, drone_id) - if not drone: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Drone not found") return drone From 81b6d7d679833fd32b80348d09f7a1cae3b1a111 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 18:12:50 +0545 Subject: [PATCH 26/66] update: drone retrieve api output model --- src/backend/app/drones/drone_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/app/drones/drone_routes.py b/src/backend/app/drones/drone_routes.py index 0f3611da..8da0e4f9 100644 --- a/src/backend/app/drones/drone_routes.py +++ b/src/backend/app/drones/drone_routes.py @@ -62,7 +62,7 @@ async def delete_drone( return {"message": f"Drone successfully deleted {drone_id}"} -@router.get("/{drone_id}", response_model=drone_schemas.DroneOut) +@router.get("/{drone_id}", response_model=drone_schemas.DbDrone) async def read_drone( drone: Annotated[drone_schemas.DbDrone, Depends(drone_deps.get_drone_by_id)], db: Annotated[Connection, Depends(database.get_db)], From 2685d9b2b0482c28aeec3c4676bb7e650a40e7b6 Mon Sep 17 00:00:00 2001 From: Niraj Adhikari Date: Fri, 9 Aug 2024 18:17:41 +0545 Subject: [PATCH 27/66] remove redundancy in drone schemas --- src/backend/app/drones/drone_schemas.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/backend/app/drones/drone_schemas.py b/src/backend/app/drones/drone_schemas.py index fecbdedc..0abaec2e 100644 --- a/src/backend/app/drones/drone_schemas.py +++ b/src/backend/app/drones/drone_schemas.py @@ -5,7 +5,7 @@ from psycopg.rows import class_row -class DroneIn(BaseModel): +class BaseDrone(BaseModel): model: str manufacturer: str camera_model: str @@ -20,25 +20,17 @@ class DroneIn(BaseModel): weight: float +class DroneIn(BaseDrone): + """Model for drone creation""" + + class DroneOut(BaseModel): id: int model: str -class DbDrone(BaseModel): +class DbDrone(BaseDrone): id: int - model: str - manufacturer: str - camera_model: str - sensor_width: float - sensor_height: float - max_battery_health: int - focal_length: float - image_width: int - image_height: int - max_altitude: int - max_speed: float - weight: int @staticmethod async def one(db: Connection, drone_id: int): From 4ac534ecf3561a4f77044cddb7303d2c107aec57 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Sat, 10 Aug 2024 09:09:52 +0545 Subject: [PATCH 28/66] refractor: refactor UserProfile schemas by introducing a shared BaseUserProfile class --- src/backend/app/users/user_deps.py | 2 +- src/backend/app/users/user_schemas.py | 26 +++++--------------------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/backend/app/users/user_deps.py b/src/backend/app/users/user_deps.py index 03d63f66..84de1e7d 100644 --- a/src/backend/app/users/user_deps.py +++ b/src/backend/app/users/user_deps.py @@ -136,7 +136,7 @@ async def get_user_by_email(db: Connection, email: str) -> dict[str, Any] | None async with db.cursor() as cur: await cur.execute(query, (email,)) result = await cur.fetchone() - return dict(result) if result else None + return result if result else None async def authenticate( diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index 93a72930..f76c4c50 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -81,7 +81,7 @@ class UserCreate(UserBase): password: str -class UserProfileIn(BaseModel): +class BaseUserProfile(BaseModel): phone_number: Optional[str] = None country: Optional[str] = None city: Optional[str] = None @@ -93,7 +93,6 @@ class UserProfileIn(BaseModel): experience_years: Optional[int] = None certified_drone_operator: Optional[bool] = False role: Optional[UserRole] = None - password: Optional[str] = None @field_validator("role", mode="after") @classmethod @@ -101,28 +100,14 @@ def integer_role_to_string(cls, value: UserRole) -> str: return str(value.name) +class UserProfileIn(BaseModel): + password: Optional[str] = None + + class DbUserProfile(BaseModel): """UserProfile model for interacting with the user_profile table.""" user_id: int - role: Optional[str] = None - phone_number: Optional[str] = None - country: Optional[str] = None - city: Optional[str] = None - organization_name: Optional[str] = None - organization_address: Optional[str] = None - job_title: Optional[str] = None - notify_for_projects_within_km: Optional[int] = None - experience_years: Optional[int] = None - drone_you_own: Optional[str] = None - certified_drone_operator: Optional[bool] = None - role: Optional[UserRole] = None - password: Optional[str] = None - - @field_validator("role", mode="after") - @classmethod - def integer_role_to_string(cls, value: UserRole) -> str: - return str(value.name) @staticmethod async def update(db: Connection, user_id: int, profile_update: UserProfileIn): @@ -147,7 +132,6 @@ async def update(db: Connection, user_id: int, profile_update: UserProfileIn): # Prepare data for insert or update model_dump = profile_update.model_dump(exclude_none=True, exclude=["password"]) columns = ", ".join(model_dump.keys()) - print(columns) value_placeholders = ", ".join(f"%({key})s" for key in model_dump.keys()) sql = f""" INSERT INTO user_profile ( From f82fc175f9523452965a053e49b42bf18b571598 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Sat, 10 Aug 2024 09:23:19 +0545 Subject: [PATCH 29/66] fix: added base class UserProfileIn & DbUserProfile --- src/backend/app/users/user_schemas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index f76c4c50..1e3b3d9f 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -100,11 +100,11 @@ def integer_role_to_string(cls, value: UserRole) -> str: return str(value.name) -class UserProfileIn(BaseModel): +class UserProfileIn(BaseUserProfile): password: Optional[str] = None -class DbUserProfile(BaseModel): +class DbUserProfile(BaseUserProfile): """UserProfile model for interacting with the user_profile table.""" user_id: int From 4b32fbe0e2b0a6c7583205a6f07b20491f44569b Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Sat, 10 Aug 2024 15:31:07 +0545 Subject: [PATCH 30/66] refractor: refactor user update profile functions --- src/backend/app/users/user_routes.py | 1 - src/backend/app/users/user_schemas.py | 86 ++++++++++++++++----------- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/src/backend/app/users/user_routes.py b/src/backend/app/users/user_routes.py index 803555d6..578143b4 100644 --- a/src/backend/app/users/user_routes.py +++ b/src/backend/app/users/user_routes.py @@ -140,7 +140,6 @@ async def my_data( user_data: Annotated[AuthUser, Depends(login_required)], ): """Read access token and get user details from Google""" - user_info = await user_schemas.DbUser.get_or_create_user(db, user_data) has_user_profile = await user_deps.get_userprofile_by_userid(db, user_info.id) user_info_dict = user_info.model_dump() diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index 1e3b3d9f..019a0335 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -1,5 +1,5 @@ from app.users import user_deps -from app.models.enums import HTTPStatus, UserRole +from app.models.enums import UserRole from pydantic import BaseModel, EmailStr, ValidationInfo, Field from pydantic.functional_validators import field_validator from typing import Optional @@ -7,7 +7,6 @@ from psycopg.rows import class_row import psycopg from fastapi import HTTPException -from loguru import logger as log class AuthUser(BaseModel): @@ -112,22 +111,21 @@ class DbUserProfile(BaseUserProfile): @staticmethod async def update(db: Connection, user_id: int, profile_update: UserProfileIn): """Update or insert a user profile.""" - # Check if the user profile exists - async with db.cursor() as cur: - sql = """ - SELECT EXISTS ( - SELECT 1 - FROM user_profile - WHERE user_id = %(user_id)s - ) - """ - await cur.execute(sql, {"user_id": user_id}) - profile_exists = await cur.fetchone() - if not profile_exists[0]: - msg = f"User profile with ID ({user_id}) does not exist!" - log.warning(f"User ({user_id}) failed profile update: {msg}") - raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail=msg) + # FIXME: Is it necessary to check the profile here? We are making a PUT or PATCH request to update it. + # async with db.cursor() as cur: + # sql = """ + # SELECT EXISTS ( + # SELECT user_id + # FROM user_profile + # WHERE user_id = %(user_id)s + # ) + # """ + # await cur.execute(sql, {"user_id": user_id}) + # profile_exists = await cur.fetchone() + # if profile_exists[0] is True: + # log.warning(f"User ({user_id}) already profile exit") + # return True # Prepare data for insert or update model_dump = profile_update.model_dump(exclude_none=True, exclude=["password"]) @@ -170,18 +168,19 @@ async def update(db: Connection, user_id: int, profile_update: UserProfileIn): ) # Check if the profile was updated successfully - fetch_sql = """ - SELECT * FROM user_profile WHERE user_id = %(user_id)s; - """ - await cur.execute(fetch_sql, {"user_id": user_id}) - updated_profile = await cur.fetchone() - - if not updated_profile: - msg = f"Failed to fetch updated profile for user ID: {user_id}" - log.warning(f"User ({user_id}) failed profile fetch: {msg}") - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=msg - ) + # FIXME: We do not need to check. + # fetch_sql = """ + # SELECT * FROM user_profile WHERE user_id = %(user_id)s; + # """ + # await cur.execute(fetch_sql, {"user_id": user_id}) + # updated_profile = await cur.fetchone() + # print("*"*100, updated_profile) + # if not updated_profile: + # msg = f"Failed to fetch updated profile for user ID: {user_id}" + # log.warning(f"User ({user_id}) failed profile fetch: {msg}") + # raise HTTPException( + # status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=msg + # ) return True @@ -195,8 +194,20 @@ class DbUser(BaseModel): profile_img: Optional[str] = None @staticmethod - async def get_or_create_user(db: Connection, user_data: AuthUser): - """Get user from User table if exists, else create.""" + async def one(db: Connection, user_id: str): + """Fetch user from the database by user_id.""" + async with db.cursor(row_factory=class_row(DbUser)) as cur: + await cur.execute( + """ + SELECT * FROM users WHERE id = %(user_id)s; + """, + {"user_id": user_id}, + ) + return await cur.fetchone() + + @staticmethod + async def create(db: Connection, user_data: AuthUser): + """Create a new user in the database.""" async with db.cursor(row_factory=class_row(DbUser)) as cur: try: await cur.execute( @@ -207,8 +218,6 @@ async def get_or_create_user(db: Connection, user_data: AuthUser): VALUES ( %(user_id)s, %(name)s, %(email_address)s, %(profile_img)s, True, False, now() ) - ON CONFLICT (id) - DO UPDATE SET profile_img = EXCLUDED.profile_img RETURNING *; """, { @@ -218,8 +227,7 @@ async def get_or_create_user(db: Connection, user_data: AuthUser): "profile_img": user_data.img_url, }, ) - user = await cur.fetchone() - return user + return await cur.fetchone() except psycopg.IntegrityError as e: if ( @@ -232,3 +240,11 @@ async def get_or_create_user(db: Connection, user_data: AuthUser): ) from e else: raise HTTPException(status_code=400, detail=str(e)) from e + + @staticmethod + async def get_or_create_user(db: Connection, user_data: AuthUser): + """Get user from User table if exists, else create.""" + user = await DbUser.one(db, str(user_data.id)) + if user: + return user + return await DbUser.create(db, user_data) From 5c2820d234b8c590418ce7e6587df47e1ce1b23a Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Sun, 11 Aug 2024 13:01:52 +0545 Subject: [PATCH 31/66] refactor: Moved dependency functions to _deps.py and retained CRUD logic in _crud.py --- src/backend/app/tasks/task_routes.py | 2 +- src/backend/app/users/user_crud.py | 192 +++++++++++++++++++++++++++ src/backend/app/users/user_deps.py | 79 ----------- src/backend/app/users/user_routes.py | 9 +- 4 files changed, 198 insertions(+), 84 deletions(-) create mode 100644 src/backend/app/users/user_crud.py diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 3c31f4ed..7bcb00ba 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -6,7 +6,7 @@ from app.tasks import task_schemas, task_crud from app.users.user_deps import login_required from app.users.user_schemas import AuthUser -from app.users.user_deps import get_user_by_id +from app.users.user_crud import get_user_by_id from psycopg import Connection from app.db import database from app.utils import send_notification_email, render_email_template diff --git a/src/backend/app/users/user_crud.py b/src/backend/app/users/user_crud.py new file mode 100644 index 00000000..affc8093 --- /dev/null +++ b/src/backend/app/users/user_crud.py @@ -0,0 +1,192 @@ +import time +import jwt +from app.config import settings +from typing import Any +from passlib.context import CryptContext +from app.db import db_models +from app.users.user_schemas import AuthUser, UserProfileIn +from fastapi import HTTPException +from pydantic import EmailStr +from psycopg import Connection +import psycopg + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +async def create_access_token(subject: str | Any): + expire = int(time.time()) + settings.ACCESS_TOKEN_EXPIRE_MINUTES + refresh_expire = int(time.time()) + settings.REFRESH_TOKEN_EXPIRE_MINUTES + + # access token + subject["exp"] = expire + access_token = jwt.encode( + subject, settings.SECRET_KEY, algorithm=settings.ALGORITHM + ) + + # refresh token + subject["exp"] = refresh_expire + refresh_token = jwt.encode( + subject, settings.SECRET_KEY, algorithm=settings.ALGORITHM + ) + + return access_token, refresh_token + + +def verify_token(token: str): + """Verifies the access token and returns the payload if valid. + + Args: + token (str): The access token to be verified. + + Returns: + dict: The payload of the access token if verification is successful. + + Raises: + HTTPException: If the token has expired or credentials could not be validated. + """ + secret_key = settings.SECRET_KEY + try: + return jwt.decode(token, str(secret_key), algorithms=[settings.ALGORITHM]) + except jwt.ExpiredSignatureError as e: + raise HTTPException(status_code=401, detail="Token has expired") from e + except Exception as e: + raise HTTPException(status_code=401, detail="Could not validate token") from e + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) + + +async def get_user_by_id(db: Connection, id: str) -> dict[str, Any] | None: + query = "SELECT * FROM users WHERE id = %s LIMIT 1;" + async with db.cursor() as cur: + await cur.execute(query, (id,)) + result = await cur.fetchone() + return result if result else None + + +async def get_user_by_email(db: Connection, email: str) -> dict[str, Any] | None: + query = "SELECT * FROM users WHERE email_address = %s LIMIT 1;" + async with db.cursor() as cur: + await cur.execute(query, (email,)) + result = await cur.fetchone() + return result if result else None + + +async def authenticate( + db: Connection, email: EmailStr, password: str +) -> db_models.DbUser | None: + db_user = await get_user_by_email(db, email) + if not db_user: + return None + if not verify_password(password, db_user["password"]): + return None + return db_user + + +async def get_or_create_user(db: Connection, user_data: AuthUser) -> AuthUser: + """Get user from User table if exists, else create.""" + try: + update_sql = """ + INSERT INTO users ( + id, name, email_address, profile_img, is_active, is_superuser, date_registered + ) + VALUES (%s, %s, %s, %s, True, False, now()) + ON CONFLICT (id) + DO UPDATE SET profile_img = EXCLUDED.profile_img; + """ + + async with db.cursor() as cur: + await cur.execute( + update_sql, + (str(user_data.id), user_data.name, user_data.email, user_data.img_url), + ) + return user_data + + except psycopg.errors.UniqueViolation as e: + if "users_email_address_key" in str(e): + raise HTTPException( + status_code=400, + detail=f"User with this email {user_data.email} already exists.", + ) from e + else: + raise HTTPException(status_code=400, detail=str(e)) from e + + +async def update_user_profile( + db: Connection, user_id: int, profile_update: UserProfileIn +): + """ + Update user profile in the database. + Args: + db (Database): Database connection object. + user_id (int): ID of the user whose profile is being updated. + profile_update (UserProfileIn): Instance of UserProfileIn containing fields to update. + Returns: + bool: True if update operation succeeds. + Raises: + Any exceptions thrown during database operations. + """ + + try: + profile_query = """ + INSERT INTO user_profile (user_id, role, phone_number, country, city, organization_name, organization_address, job_title, notify_for_projects_within_km, + experience_years, drone_you_own, certified_drone_operator) + VALUES (:user_id, :role, :phone_number, :country, :city, :organization_name, :organization_address, :job_title, :notify_for_projects_within_km , + :experience_years, :drone_you_own, :certified_drone_operator) + ON CONFLICT (user_id) + DO UPDATE SET + role = :role, + phone_number = :phone_number, + country = :country, + city = :city, + organization_name = :organization_name, + organization_address = :organization_address, + job_title = :job_title, + notify_for_projects_within_km = :notify_for_projects_within_km, + experience_years = :experience_years, + drone_you_own = :drone_you_own, + certified_drone_operator = :certified_drone_operator; + """ + + await db.execute( + profile_query, + { + "user_id": user_id, + "role": profile_update.role, + "phone_number": profile_update.phone_number, + "country": profile_update.country, + "city": profile_update.city, + "organization_name": profile_update.organization_name, + "organization_address": profile_update.organization_address, + "job_title": profile_update.job_title, + "notify_for_projects_within_km": profile_update.notify_for_projects_within_km, + "experience_years": profile_update.experience_years, + "drone_you_own": profile_update.drone_you_own, + "certified_drone_operator": profile_update.certified_drone_operator, + }, + ) + + # If password is provided, update the users table + if profile_update.password: + password_update_query = """ + UPDATE users + SET password = :password + WHERE id = :user_id; + """ + await db.execute( + password_update_query, + { + "password": get_password_hash(profile_update.password), + "user_id": user_id, + }, + ) + + return True + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) from e diff --git a/src/backend/app/users/user_deps.py b/src/backend/app/users/user_deps.py index 84de1e7d..0d1dc285 100644 --- a/src/backend/app/users/user_deps.py +++ b/src/backend/app/users/user_deps.py @@ -3,13 +3,9 @@ from app.users.auth import Auth from app.users.user_schemas import AuthUser from loguru import logger as log -import time import jwt from typing import Any from passlib.context import CryptContext -from app.db import db_models -from pydantic import EmailStr -import psycopg from psycopg import Connection @@ -76,25 +72,6 @@ async def get_userprofile_by_userid(db: Connection, user_id: str): pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -async def create_access_token(subject: dict[str, Any]) -> tuple[str, str]: - expire = int(time.time()) + settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60 - refresh_expire = int(time.time()) + settings.REFRESH_TOKEN_EXPIRE_MINUTES * 60 - - # Access token - subject["exp"] = expire - access_token = jwt.encode( - subject, settings.SECRET_KEY, algorithm=settings.ALGORITHM - ) - - # Refresh token - subject["exp"] = refresh_expire - refresh_token = jwt.encode( - subject, settings.SECRET_KEY, algorithm=settings.ALGORITHM - ) - - return access_token, refresh_token - - def verify_token(token: str) -> dict[str, Any]: """Verifies the access token and returns the payload if valid. @@ -121,59 +98,3 @@ def verify_password(plain_password: str, hashed_password: str) -> bool: def get_password_hash(password: str) -> str: return pwd_context.hash(password) - - -async def get_user_by_id(db: Connection, id: str) -> dict[str, Any] | None: - query = "SELECT * FROM users WHERE id = %s LIMIT 1;" - async with db.cursor() as cur: - await cur.execute(query, (id,)) - result = await cur.fetchone() - return result if result else None - - -async def get_user_by_email(db: Connection, email: str) -> dict[str, Any] | None: - query = "SELECT * FROM users WHERE email_address = %s LIMIT 1;" - async with db.cursor() as cur: - await cur.execute(query, (email,)) - result = await cur.fetchone() - return result if result else None - - -async def authenticate( - db: Connection, email: EmailStr, password: str -) -> db_models.DbUser | None: - db_user = await get_user_by_email(db, email) - if not db_user: - return None - if not verify_password(password, db_user["password"]): - return None - return db_user - - -async def get_or_create_user(db: Connection, user_data: AuthUser) -> AuthUser: - """Get user from User table if exists, else create.""" - try: - update_sql = """ - INSERT INTO users ( - id, name, email_address, profile_img, is_active, is_superuser, date_registered - ) - VALUES (%s, %s, %s, %s, True, False, now()) - ON CONFLICT (id) - DO UPDATE SET profile_img = EXCLUDED.profile_img; - """ - - async with db.cursor() as cur: - await cur.execute( - update_sql, - (str(user_data.id), user_data.name, user_data.email, user_data.img_url), - ) - return user_data - - except psycopg.errors.UniqueViolation as e: - if "users_email_address_key" in str(e): - raise HTTPException( - status_code=400, - detail=f"User with this email {user_data.email} already exists.", - ) from e - else: - raise HTTPException(status_code=400, detail=str(e)) from e diff --git a/src/backend/app/users/user_routes.py b/src/backend/app/users/user_routes.py index 578143b4..4efd0089 100644 --- a/src/backend/app/users/user_routes.py +++ b/src/backend/app/users/user_routes.py @@ -1,6 +1,7 @@ import os from app.users import user_schemas from app.users import user_deps +from app.users import user_crud from fastapi import APIRouter, Response, HTTPException, Depends, Request from typing import Annotated from fastapi.security import OAuth2PasswordRequestForm @@ -51,7 +52,7 @@ async def login_access_token( "img_url": user.profile_img, } - access_token, refresh_token = await user_deps.create_access_token(user_info) + access_token, refresh_token = await user_crud.create_access_token(user_info) return Token(access_token=access_token, refresh_token=refresh_token) @@ -75,7 +76,7 @@ async def update_user_profile( HTTPException: If user with given user_id is not found in the database. """ - user = await user_deps.get_user_by_id(db, user_id) + user = await user_crud.get_user_by_id(db, user_id) if user_data.id != user_id: raise HTTPException( @@ -119,7 +120,7 @@ async def callback(request: Request, google_auth=Depends(init_google_auth)): access_token = google_auth.callback(callback_url).get("access_token") user_data = google_auth.deserialize_access_token(access_token) - access_token, refresh_token = await user_deps.create_access_token(user_data) + access_token, refresh_token = await user_crud.create_access_token(user_data) return Token(access_token=access_token, refresh_token=refresh_token) @@ -128,7 +129,7 @@ async def callback(request: Request, google_auth=Depends(init_google_auth)): async def update_token(user_data: Annotated[AuthUser, Depends(login_required)]): """Refresh access token""" - access_token, refresh_token = await user_deps.create_access_token( + access_token, refresh_token = await user_crud.create_access_token( user_data.model_dump() ) return Token(access_token=access_token, refresh_token=refresh_token) From 6272c05506d4676525792cc44444c91149dc9464 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Sun, 11 Aug 2024 13:26:36 +0545 Subject: [PATCH 32/66] refactor: Moved dependency functions to _crud.py & _deps.py --- src/backend/app/users/user_crud.py | 161 +++-------------------------- src/backend/app/users/user_deps.py | 33 +++--- 2 files changed, 28 insertions(+), 166 deletions(-) diff --git a/src/backend/app/users/user_crud.py b/src/backend/app/users/user_crud.py index affc8093..a7ee9c2a 100644 --- a/src/backend/app/users/user_crud.py +++ b/src/backend/app/users/user_crud.py @@ -2,16 +2,8 @@ import jwt from app.config import settings from typing import Any -from passlib.context import CryptContext -from app.db import db_models -from app.users.user_schemas import AuthUser, UserProfileIn -from fastapi import HTTPException -from pydantic import EmailStr from psycopg import Connection -import psycopg - - -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +from loguru import logger as log async def create_access_token(subject: str | Any): @@ -33,35 +25,6 @@ async def create_access_token(subject: str | Any): return access_token, refresh_token -def verify_token(token: str): - """Verifies the access token and returns the payload if valid. - - Args: - token (str): The access token to be verified. - - Returns: - dict: The payload of the access token if verification is successful. - - Raises: - HTTPException: If the token has expired or credentials could not be validated. - """ - secret_key = settings.SECRET_KEY - try: - return jwt.decode(token, str(secret_key), algorithms=[settings.ALGORITHM]) - except jwt.ExpiredSignatureError as e: - raise HTTPException(status_code=401, detail="Token has expired") from e - except Exception as e: - raise HTTPException(status_code=401, detail="Could not validate token") from e - - -def verify_password(plain_password: str, hashed_password: str) -> bool: - return pwd_context.verify(plain_password, hashed_password) - - -def get_password_hash(password: str) -> str: - return pwd_context.hash(password) - - async def get_user_by_id(db: Connection, id: str) -> dict[str, Any] | None: query = "SELECT * FROM users WHERE id = %s LIMIT 1;" async with db.cursor() as cur: @@ -78,115 +41,15 @@ async def get_user_by_email(db: Connection, email: str) -> dict[str, Any] | None return result if result else None -async def authenticate( - db: Connection, email: EmailStr, password: str -) -> db_models.DbUser | None: - db_user = await get_user_by_email(db, email) - if not db_user: - return None - if not verify_password(password, db_user["password"]): - return None - return db_user - - -async def get_or_create_user(db: Connection, user_data: AuthUser) -> AuthUser: - """Get user from User table if exists, else create.""" - try: - update_sql = """ - INSERT INTO users ( - id, name, email_address, profile_img, is_active, is_superuser, date_registered - ) - VALUES (%s, %s, %s, %s, True, False, now()) - ON CONFLICT (id) - DO UPDATE SET profile_img = EXCLUDED.profile_img; - """ - - async with db.cursor() as cur: - await cur.execute( - update_sql, - (str(user_data.id), user_data.name, user_data.email, user_data.img_url), - ) - return user_data - - except psycopg.errors.UniqueViolation as e: - if "users_email_address_key" in str(e): - raise HTTPException( - status_code=400, - detail=f"User with this email {user_data.email} already exists.", - ) from e - else: - raise HTTPException(status_code=400, detail=str(e)) from e - - -async def update_user_profile( - db: Connection, user_id: int, profile_update: UserProfileIn -): - """ - Update user profile in the database. - Args: - db (Database): Database connection object. - user_id (int): ID of the user whose profile is being updated. - profile_update (UserProfileIn): Instance of UserProfileIn containing fields to update. - Returns: - bool: True if update operation succeeds. - Raises: - Any exceptions thrown during database operations. +async def get_userprofile_by_userid(db: Connection, user_id: str): + """Fetch the user profile by user ID.""" + query = """ + SELECT * FROM user_profile + WHERE user_id = %(user_id)s + LIMIT 1; """ - - try: - profile_query = """ - INSERT INTO user_profile (user_id, role, phone_number, country, city, organization_name, organization_address, job_title, notify_for_projects_within_km, - experience_years, drone_you_own, certified_drone_operator) - VALUES (:user_id, :role, :phone_number, :country, :city, :organization_name, :organization_address, :job_title, :notify_for_projects_within_km , - :experience_years, :drone_you_own, :certified_drone_operator) - ON CONFLICT (user_id) - DO UPDATE SET - role = :role, - phone_number = :phone_number, - country = :country, - city = :city, - organization_name = :organization_name, - organization_address = :organization_address, - job_title = :job_title, - notify_for_projects_within_km = :notify_for_projects_within_km, - experience_years = :experience_years, - drone_you_own = :drone_you_own, - certified_drone_operator = :certified_drone_operator; - """ - - await db.execute( - profile_query, - { - "user_id": user_id, - "role": profile_update.role, - "phone_number": profile_update.phone_number, - "country": profile_update.country, - "city": profile_update.city, - "organization_name": profile_update.organization_name, - "organization_address": profile_update.organization_address, - "job_title": profile_update.job_title, - "notify_for_projects_within_km": profile_update.notify_for_projects_within_km, - "experience_years": profile_update.experience_years, - "drone_you_own": profile_update.drone_you_own, - "certified_drone_operator": profile_update.certified_drone_operator, - }, - ) - - # If password is provided, update the users table - if profile_update.password: - password_update_query = """ - UPDATE users - SET password = :password - WHERE id = :user_id; - """ - await db.execute( - password_update_query, - { - "password": get_password_hash(profile_update.password), - "user_id": user_id, - }, - ) - - return True - except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) from e + async with db.cursor() as cur: + await cur.execute(query, {"user_id": user_id}) + result = await cur.fetchone() + log.info(f"Fetched user profile data: {result}") + return result diff --git a/src/backend/app/users/user_deps.py b/src/backend/app/users/user_deps.py index 0d1dc285..f2db0f52 100644 --- a/src/backend/app/users/user_deps.py +++ b/src/backend/app/users/user_deps.py @@ -1,3 +1,4 @@ +from app.users.user_crud import get_user_by_email from fastapi import HTTPException, Request, Header from app.config import settings from app.users.auth import Auth @@ -7,6 +8,10 @@ from typing import Any from passlib.context import CryptContext from psycopg import Connection +from app.db import db_models +from pydantic import EmailStr + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") async def init_google_auth(): @@ -55,23 +60,6 @@ async def login_required( return AuthUser(**user) -async def get_userprofile_by_userid(db: Connection, user_id: str): - """Fetch the user profile by user ID.""" - query = """ - SELECT * FROM user_profile - WHERE user_id = %(user_id)s - LIMIT 1; - """ - async with db.cursor() as cur: - await cur.execute(query, {"user_id": user_id}) - result = await cur.fetchone() - log.info(f"Fetched user profile data: {result}") - return result - - -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - - def verify_token(token: str) -> dict[str, Any]: """Verifies the access token and returns the payload if valid. @@ -98,3 +86,14 @@ def verify_password(plain_password: str, hashed_password: str) -> bool: def get_password_hash(password: str) -> str: return pwd_context.hash(password) + + +async def authenticate( + db: Connection, email: EmailStr, password: str +) -> db_models.DbUser | None: + db_user = await get_user_by_email(db, email) + if not db_user: + return None + if not verify_password(password, db_user["password"]): + return None + return db_user From fb8e19c3f10525572532c145d518f4da7353fdc0 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Mon, 12 Aug 2024 10:28:53 +0545 Subject: [PATCH 33/66] refactor: user schams & remove fixme code --- src/backend/app/tasks/task_routes.py | 12 +++-- src/backend/app/users/user_crud.py | 66 +++++++++++++++++---------- src/backend/app/users/user_deps.py | 49 +------------------- src/backend/app/users/user_routes.py | 4 +- src/backend/app/users/user_schemas.py | 62 ++++++++++++------------- 5 files changed, 84 insertions(+), 109 deletions(-) diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 7bcb00ba..98061ec8 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -6,7 +6,7 @@ from app.tasks import task_schemas, task_crud from app.users.user_deps import login_required from app.users.user_schemas import AuthUser -from app.users.user_crud import get_user_by_id +from app.users import user_schemas from psycopg import Connection from app.db import database from app.utils import send_notification_email, render_email_template @@ -74,7 +74,7 @@ async def new_event( "Request for mapping", ) # email notification - author = await get_user_by_id(db, project.author_id) + author = await user_schemas.DbUser.get_user_by_id(db, project.author_id) html_content = render_email_template( template_name="mapping_requests.html", @@ -105,7 +105,9 @@ async def new_event( requested_user_id = await task_crud.get_requested_user_id( db, project_id, task_id ) - drone_operator = await get_user_by_id(db, requested_user_id) + drone_operator = await user_schemas.DbUser.get_user_by_id( + db, requested_user_id + ) html_content = render_email_template( template_name="mapping_approved_or_rejected.html", context={ @@ -148,7 +150,9 @@ async def new_event( requested_user_id = await task_crud.get_requested_user_id( db, project_id, task_id ) - drone_operator = await get_user_by_id(db, requested_user_id) + drone_operator = await user_schemas.DbUser.get_user_by_id( + db, requested_user_id + ) html_content = render_email_template( template_name="mapping_approved_or_rejected.html", context={ diff --git a/src/backend/app/users/user_crud.py b/src/backend/app/users/user_crud.py index a7ee9c2a..3bf6cb1f 100644 --- a/src/backend/app/users/user_crud.py +++ b/src/backend/app/users/user_crud.py @@ -3,7 +3,14 @@ from app.config import settings from typing import Any from psycopg import Connection -from loguru import logger as log +from app.db import db_models +from pydantic import EmailStr +from fastapi import HTTPException +from passlib.context import CryptContext +from app.users import user_schemas + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") async def create_access_token(subject: str | Any): @@ -25,31 +32,40 @@ async def create_access_token(subject: str | Any): return access_token, refresh_token -async def get_user_by_id(db: Connection, id: str) -> dict[str, Any] | None: - query = "SELECT * FROM users WHERE id = %s LIMIT 1;" - async with db.cursor() as cur: - await cur.execute(query, (id,)) - result = await cur.fetchone() - return result if result else None - +def verify_token(token: str) -> dict[str, Any]: + """Verifies the access token and returns the payload if valid. -async def get_user_by_email(db: Connection, email: str) -> dict[str, Any] | None: - query = "SELECT * FROM users WHERE email_address = %s LIMIT 1;" - async with db.cursor() as cur: - await cur.execute(query, (email,)) - result = await cur.fetchone() - return result if result else None + Args: + token (str): The access token to be verified. + Returns: + dict: The payload of the access token if verification is successful. -async def get_userprofile_by_userid(db: Connection, user_id: str): - """Fetch the user profile by user ID.""" - query = """ - SELECT * FROM user_profile - WHERE user_id = %(user_id)s - LIMIT 1; + Raises: + HTTPException: If the token has expired or credentials could not be validated. """ - async with db.cursor() as cur: - await cur.execute(query, {"user_id": user_id}) - result = await cur.fetchone() - log.info(f"Fetched user profile data: {result}") - return result + try: + return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + except jwt.ExpiredSignatureError as e: + raise HTTPException(status_code=401, detail="Token has expired") from e + except Exception as e: + raise HTTPException(status_code=401, detail="Could not validate token") from e + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) + + +async def authenticate( + db: Connection, email: EmailStr, password: str +) -> db_models.DbUser | None: + db_user = await user_schemas.DbUser.get_user_by_email(db, email) + if not db_user: + return None + if not verify_password(password, db_user["password"]): + return None + return db_user diff --git a/src/backend/app/users/user_deps.py b/src/backend/app/users/user_deps.py index f2db0f52..a6ce339e 100644 --- a/src/backend/app/users/user_deps.py +++ b/src/backend/app/users/user_deps.py @@ -1,17 +1,9 @@ -from app.users.user_crud import get_user_by_email +from app.users.user_crud import verify_token from fastapi import HTTPException, Request, Header from app.config import settings from app.users.auth import Auth from app.users.user_schemas import AuthUser from loguru import logger as log -import jwt -from typing import Any -from passlib.context import CryptContext -from psycopg import Connection -from app.db import db_models -from pydantic import EmailStr - -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") async def init_google_auth(): @@ -58,42 +50,3 @@ async def login_required( raise HTTPException(status_code=401, detail="Access token not valid") from e return AuthUser(**user) - - -def verify_token(token: str) -> dict[str, Any]: - """Verifies the access token and returns the payload if valid. - - Args: - token (str): The access token to be verified. - - Returns: - dict: The payload of the access token if verification is successful. - - Raises: - HTTPException: If the token has expired or credentials could not be validated. - """ - try: - return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) - except jwt.ExpiredSignatureError as e: - raise HTTPException(status_code=401, detail="Token has expired") from e - except Exception as e: - raise HTTPException(status_code=401, detail="Could not validate token") from e - - -def verify_password(plain_password: str, hashed_password: str) -> bool: - return pwd_context.verify(plain_password, hashed_password) - - -def get_password_hash(password: str) -> str: - return pwd_context.hash(password) - - -async def authenticate( - db: Connection, email: EmailStr, password: str -) -> db_models.DbUser | None: - db_user = await get_user_by_email(db, email) - if not db_user: - return None - if not verify_password(password, db_user["password"]): - return None - return db_user diff --git a/src/backend/app/users/user_routes.py b/src/backend/app/users/user_routes.py index 4efd0089..a9673bfe 100644 --- a/src/backend/app/users/user_routes.py +++ b/src/backend/app/users/user_routes.py @@ -142,7 +142,9 @@ async def my_data( ): """Read access token and get user details from Google""" user_info = await user_schemas.DbUser.get_or_create_user(db, user_data) - has_user_profile = await user_deps.get_userprofile_by_userid(db, user_info.id) + has_user_profile = await user_schemas.DbUserProfile.get_userprofile_by_userid( + db, user_info.id + ) user_info_dict = user_info.model_dump() user_info_dict["has_user_profile"] = bool(has_user_profile) return user_info_dict diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index 019a0335..a209320f 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -7,6 +7,8 @@ from psycopg.rows import class_row import psycopg from fastapi import HTTPException +from typing import Any +from loguru import logger as log class AuthUser(BaseModel): @@ -111,21 +113,6 @@ class DbUserProfile(BaseUserProfile): @staticmethod async def update(db: Connection, user_id: int, profile_update: UserProfileIn): """Update or insert a user profile.""" - # Check if the user profile exists - # FIXME: Is it necessary to check the profile here? We are making a PUT or PATCH request to update it. - # async with db.cursor() as cur: - # sql = """ - # SELECT EXISTS ( - # SELECT user_id - # FROM user_profile - # WHERE user_id = %(user_id)s - # ) - # """ - # await cur.execute(sql, {"user_id": user_id}) - # profile_exists = await cur.fetchone() - # if profile_exists[0] is True: - # log.warning(f"User ({user_id}) already profile exit") - # return True # Prepare data for insert or update model_dump = profile_update.model_dump(exclude_none=True, exclude=["password"]) @@ -166,24 +153,21 @@ async def update(db: Connection, user_id: int, profile_update: UserProfileIn): "user_id": user_id, }, ) - - # Check if the profile was updated successfully - # FIXME: We do not need to check. - # fetch_sql = """ - # SELECT * FROM user_profile WHERE user_id = %(user_id)s; - # """ - # await cur.execute(fetch_sql, {"user_id": user_id}) - # updated_profile = await cur.fetchone() - # print("*"*100, updated_profile) - # if not updated_profile: - # msg = f"Failed to fetch updated profile for user ID: {user_id}" - # log.warning(f"User ({user_id}) failed profile fetch: {msg}") - # raise HTTPException( - # status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=msg - # ) - return True + async def get_userprofile_by_userid(db: Connection, user_id: str): + """Fetch the user profile by user ID.""" + query = """ + SELECT * FROM user_profile + WHERE user_id = %(user_id)s + LIMIT 1; + """ + async with db.cursor() as cur: + await cur.execute(query, {"user_id": user_id}) + result = await cur.fetchone() + log.info(f"Fetched user profile data: {result}") + return result + class DbUser(BaseModel): id: str @@ -248,3 +232,19 @@ async def get_or_create_user(db: Connection, user_data: AuthUser): if user: return user return await DbUser.create(db, user_data) + + @staticmethod + async def get_user_by_id(db: Connection, id: str) -> dict[str, Any] | None: + query = "SELECT * FROM users WHERE id = %s LIMIT 1;" + async with db.cursor() as cur: + await cur.execute(query, (id,)) + result = await cur.fetchone() + return result if result else None + + @staticmethod + async def get_user_by_email(db: Connection, email: str) -> dict[str, Any] | None: + query = "SELECT * FROM users WHERE email_address = %s LIMIT 1;" + async with db.cursor() as cur: + await cur.execute(query, (email,)) + result = await cur.fetchone() + return result if result else None From 76f2120953eb52c4289713958857fd1ce269233a Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Mon, 12 Aug 2024 10:33:44 +0545 Subject: [PATCH 34/66] fix: import error from user_crud * user_routes --- src/backend/app/users/user_routes.py | 2 +- src/backend/app/users/user_schemas.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backend/app/users/user_routes.py b/src/backend/app/users/user_routes.py index a9673bfe..59c387c4 100644 --- a/src/backend/app/users/user_routes.py +++ b/src/backend/app/users/user_routes.py @@ -76,7 +76,7 @@ async def update_user_profile( HTTPException: If user with given user_id is not found in the database. """ - user = await user_crud.get_user_by_id(db, user_id) + user = await user_schemas.DbUser.get_user_by_id(db, user_id) if user_data.id != user_id: raise HTTPException( diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index a209320f..c0554c6f 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -9,7 +9,7 @@ from fastapi import HTTPException from typing import Any from loguru import logger as log - +from app.users import user_crud class AuthUser(BaseModel): """The user model returned from Google OAuth2.""" @@ -147,7 +147,7 @@ async def update(db: Connection, user_id: int, profile_update: UserProfileIn): await cur.execute( password_update_query, { - "password": user_deps.get_password_hash( + "password": user_crud.get_password_hash( profile_update.password ), "user_id": user_id, From 3fbe4cd28e9b98c7fc8a0c08a1c63a7e6ee3f259 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 04:49:02 +0000 Subject: [PATCH 35/66] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/backend/app/users/user_schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index c0554c6f..69984206 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -1,4 +1,3 @@ -from app.users import user_deps from app.models.enums import UserRole from pydantic import BaseModel, EmailStr, ValidationInfo, Field from pydantic.functional_validators import field_validator @@ -11,6 +10,7 @@ from loguru import logger as log from app.users import user_crud + class AuthUser(BaseModel): """The user model returned from Google OAuth2.""" From 648ed149260ef5d05141c30e926766df3ebeae38 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Mon, 12 Aug 2024 14:48:31 +0545 Subject: [PATCH 36/66] refactor: refractor task module using pyscopg and pydantic --- src/backend/app/projects/project_crud.py | 13 +- src/backend/app/tasks/task_crud.py | 247 -------------------- src/backend/app/tasks/task_routes.py | 62 ++--- src/backend/app/tasks/task_schemas.py | 278 ++++++++++++++++++++++- src/backend/app/users/user_schemas.py | 28 ++- 5 files changed, 346 insertions(+), 282 deletions(-) diff --git a/src/backend/app/projects/project_crud.py b/src/backend/app/projects/project_crud.py index 4c157bb1..3e4b7630 100644 --- a/src/backend/app/projects/project_crud.py +++ b/src/backend/app/projects/project_crud.py @@ -6,18 +6,21 @@ from fastapi.concurrency import run_in_threadpool from psycopg import Connection from geojson_pydantic import FeatureCollection +from psycopg.rows import dict_row from app.models.enums import HTTPStatus from app.utils import merge_multipolygon -# TODO delete me async def get_project_by_id(db: Connection, project_id: uuid.UUID): "Get a single database project object by project_id" - - query = """ select * from projects where id=:project_id""" - result = await db.fetch_one(query, {"project_id": project_id}) - return result + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute( + """ select * from projects where id=%(project_id)s""", + {"project_id": project_id}, + ) + result = await cur.fetchone() + return result async def create_tasks_from_geojson( diff --git a/src/backend/app/tasks/task_crud.py b/src/backend/app/tasks/task_crud.py index 4bbddb8f..e69de29b 100644 --- a/src/backend/app/tasks/task_crud.py +++ b/src/backend/app/tasks/task_crud.py @@ -1,247 +0,0 @@ -import uuid -from app.models.enums import HTTPStatus, State -from fastapi import HTTPException -from loguru import logger as log -from psycopg import Connection - - -async def get_tasks_by_user(user_id: str, db: Connection): - try: - query = """WITH task_details AS ( - SELECT - tasks.id AS task_id, - ST_Area(ST_Transform(tasks.outline, 4326)) / 1000000 AS task_area, - task_events.created_at, - task_events.state - FROM - task_events - JOIN - tasks ON task_events.task_id = tasks.id - WHERE - task_events.user_id = :user_id - ) - SELECT - task_details.task_id, - task_details.task_area, - task_details.created_at, - CASE - WHEN task_details.state = 'REQUEST_FOR_MAPPING' THEN 'ongoing' - WHEN task_details.state = 'UNLOCKED_DONE' THEN 'completed' - WHEN task_details.state IN ('UNLOCKED_TO_VALIDATE', 'LOCKED_FOR_VALIDATION') THEN 'mapped' - ELSE 'unknown' - END AS state - FROM task_details - """ - records = await db.fetch_all(query, values={"user_id": user_id}) - return records - - except Exception as e: - log.exception(e) - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Retrieval failed" - ) from e - - -async def get_all_tasks(db: Connection, project_id: uuid.UUID): - query = """ - SELECT id FROM tasks WHERE project_id = :project_id - """ - values = {"project_id": str(project_id)} - - data = await db.fetch_all(query, values) - - # Extracting the list of IDs from the data - task_ids = [task["id"] for task in data] - - return task_ids - - -async def all_tasks_states(db: Connection, project_id: uuid.UUID): - query = """ - SELECT DISTINCT ON (task_id) project_id, task_id, state - FROM task_events - WHERE project_id = :project_id - ORDER BY task_id, created_at DESC - """ - - r = await db.fetch_all(query, {"project_id": str(project_id)}) - - # Extract task_ids and corresponding states from the query result - existing_tasks = [dict(r) for r in r] - - # Get all task_ids from the tasks table - task_ids = await get_all_tasks(db, project_id) - - # Create a set of existing task_ids for quick lookup - existing_task_ids = {task["task_id"] for task in existing_tasks} - - # task ids that are not in task_events table - remaining_task_ids = [x for x in task_ids if x not in existing_task_ids] - - # Add missing tasks with state as "UNLOCKED_FOR_MAPPING" - remaining_tasks = [ - { - "project_id": str(project_id), - "task_id": task_id, - "state": State.UNLOCKED_TO_MAP.name, - } - for task_id in remaining_task_ids - ] - - # Combine both existing tasks and remaining tasks - combined_tasks = existing_tasks + remaining_tasks - - return combined_tasks - - -async def request_mapping( - db: Connection, - project_id: uuid.UUID, - task_id: uuid.UUID, - user_id: str, - comment: str, -): - query = """ - WITH last AS ( - SELECT * - FROM task_events - WHERE project_id= :project_id AND task_id= :task_id - ORDER BY created_at DESC - LIMIT 1 - ), - released AS ( - SELECT COUNT(*) = 0 AS no_record - FROM task_events - WHERE project_id= :project_id AND task_id= :task_id AND state = :unlocked_to_map_state - ) - INSERT INTO task_events (event_id, project_id, task_id, user_id, comment, state, created_at) - - SELECT - gen_random_uuid(), - :project_id, - :task_id, - :user_id, - :comment, - :request_for_map_state, - now() - FROM last - RIGHT JOIN released ON true - WHERE (last.state = :unlocked_to_map_state OR released.no_record = true); - """ - - values = { - "project_id": str(project_id), - "task_id": str(task_id), - "user_id": str(user_id), - "comment": comment, - "unlocked_to_map_state": State.UNLOCKED_TO_MAP.name, - "request_for_map_state": State.REQUEST_FOR_MAPPING.name, - } - - await db.fetch_one(query, values) - - return {"project_id": project_id, "task_id": task_id, "comment": comment} - - -async def update_task_state( - db: Connection, - project_id: uuid.UUID, - task_id: uuid.UUID, - user_id: str, - comment: str, - initial_state: State, - final_state: State, -): - # Update or insert task event - query = """ - WITH last AS ( - SELECT * - FROM task_events - WHERE project_id = :project_id AND task_id = :task_id - ORDER BY created_at DESC - LIMIT 1 - ), - updated AS ( - UPDATE task_events - SET state = :final_state, comment = :comment, created_at = now() - WHERE EXISTS ( - SELECT 1 - FROM last - WHERE user_id = :user_id AND state = :initial_state - ) - RETURNING project_id, task_id, user_id, state - ) - INSERT INTO task_events (event_id, project_id, task_id, user_id, state, comment, created_at) - SELECT gen_random_uuid(), :project_id, :task_id, :user_id, :final_state, :comment, now() - WHERE NOT EXISTS ( - SELECT 1 - FROM updated - ) - RETURNING project_id, task_id, user_id, state; - """ - - values = { - "project_id": str(project_id), - "task_id": str(task_id), - "user_id": str(user_id), - "comment": comment, - "initial_state": initial_state.name, - "final_state": final_state.name, - } - - result = await db.fetch_one(query, values) - - return { - "project_id": result["project_id"], - "task_id": result["task_id"], - "comment": comment, - } - - -async def get_requested_user_id( - db: Connection, project_id: uuid.UUID, task_id: uuid.UUID -): - query = """ - SELECT user_id - FROM task_events - WHERE project_id = :project_id AND task_id = :task_id and state = :request_for_map_state - ORDER BY created_at DESC - LIMIT 1 - """ - values = { - "project_id": str(project_id), - "task_id": str(task_id), - "request_for_map_state": State.REQUEST_FOR_MAPPING.name, - } - - result = await db.fetch_one(query, values) - if result is None: - raise ValueError("No user requested for mapping") - return result["user_id"] - - -async def get_project_task_by_id(db: Connection, user_id: str): - """Get a list of pending tasks created by a specific user (project creator).""" - _sql = """ - SELECT id FROM projects WHERE author_id = :user_id - """ - project_ids_result = await db.fetch_all(query=_sql, values={"user_id": user_id}) - project_ids = [row["id"] for row in project_ids_result] - raw_sql = """ - SELECT t.id AS task_id, te.event_id, te.user_id, te.project_id, te.comment, te.state, te.created_at - FROM tasks t - LEFT JOIN task_events te ON t.id = te.task_id - WHERE t.project_id = ANY(:project_ids) - AND te.state = :state - ORDER BY t.project_task_index; - """ - values = {"project_ids": project_ids, "state": "REQUEST_FOR_MAPPING"} - try: - db_tasks = await db.fetch_all(query=raw_sql, values=values) - except Exception as e: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=f"Failed to fetch project tasks. {e}", - ) - - return db_tasks diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 98061ec8..fba08f4f 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -11,7 +11,7 @@ from app.db import database from app.utils import send_notification_email, render_email_template from app.projects.project_crud import get_project_by_id - +from psycopg.rows import dict_row router = APIRouter( prefix=f"{settings.API_PREFIX}/tasks", @@ -28,7 +28,7 @@ async def list_tasks( """Get all tasks for a drone user.""" user_id = user_data.id - return await task_crud.get_tasks_by_user(user_id, db) + return await task_schemas.UserTasksStatsOut.get_tasks_by_user(db, user_id) @router.get("/states/{project_id}") @@ -36,8 +36,9 @@ async def task_states( db: Annotated[Connection, Depends(database.get_db)], project_id: uuid.UUID ): """Get all tasks states for a project.""" - - return await task_crud.all_tasks_states(db, project_id) + return await task_schemas.UserTasksStatsOut.get_all_tasks_with_states( + db, project_id + ) @router.post("/event/{project_id}/{task_id}") @@ -53,9 +54,8 @@ async def new_event( match detail.event: case EventType.REQUESTS: - # TODO: Combine the logic of `update_or_create_task_state` and `request_mapping` functions into a single function if possible. Will do later. project = await get_project_by_id(db, project_id) - if project["requires_approval_from_manager_for_locking"] == "true": + if project["requires_approval_from_manager_for_locking"] is False: data = await task_crud.update_or_create_task_state( db, project_id, @@ -66,7 +66,7 @@ async def new_event( State.LOCKED_FOR_MAPPING, ) else: - data = await task_crud.request_mapping( + data = await task_schemas.Task.request_mapping( db, project_id, task_id, @@ -102,7 +102,7 @@ async def new_event( detail="Only the project creator can approve the mapping.", ) - requested_user_id = await task_crud.get_requested_user_id( + requested_user_id = await user_schemas.DbUser.get_requested_user_id( db, project_id, task_id ) drone_operator = await user_schemas.DbUser.get_user_by_id( @@ -129,7 +129,7 @@ async def new_event( html_content, ) - return await task_crud.update_task_state( + return await task_schemas.Task.update( db, project_id, task_id, @@ -147,7 +147,7 @@ async def new_event( detail="Only the project creator can approve the mapping.", ) - requested_user_id = await task_crud.get_requested_user_id( + requested_user_id = await user_schemas.DbUser.get_requested_user_id( db, project_id, task_id ) drone_operator = await user_schemas.DbUser.get_user_by_id( @@ -174,7 +174,7 @@ async def new_event( html_content, ) - return await task_crud.update_task_state( + return await task_schemas.Task.update( db, project_id, task_id, @@ -184,7 +184,7 @@ async def new_event( State.UNLOCKED_TO_MAP, ) case EventType.FINISH: - return await task_crud.update_task_state( + return await task_schemas.Task.update( db, project_id, task_id, @@ -194,7 +194,7 @@ async def new_event( State.UNLOCKED_TO_VALIDATE, ) case EventType.VALIDATE: - return await task_crud.update_task_state( + return task_schemas.Task.update( db, project_id, task_id, @@ -204,7 +204,7 @@ async def new_event( State.LOCKED_FOR_VALIDATION, ) case EventType.GOOD: - return await task_crud.update_task_state( + return await task_schemas.Task.update( db, project_id, task_id, @@ -215,7 +215,7 @@ async def new_event( ) case EventType.BAD: - return await task_crud.update_task_state( + return await task_schemas.Task.update( db, project_id, task_id, @@ -235,17 +235,23 @@ async def get_pending_tasks( ): """Get a list of pending tasks for a project creator.""" user_id = user_data.id - query = """SELECT role FROM user_profile WHERE user_id = :user_id""" - records = await db.fetch_all(query, {"user_id": user_id}) - if not records: - raise HTTPException(status_code=404, detail="User profile not found") - - roles = [record["role"] for record in records] - if UserRole.PROJECT_CREATOR.name not in roles: - raise HTTPException( - status_code=403, detail="Access forbidden for non-Project Creator users" + + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute( + """SELECT role FROM user_profile WHERE user_id = %(user_id)s""", + {"user_id": user_id}, ) - pending_tasks = await task_crud.get_project_task_by_id(db, user_id) - if pending_tasks is None: - raise HTTPException(status_code=404, detail="Project not found") - return pending_tasks + records = await cur.fetchall() + print("*" * 100, records) + if not records: + raise HTTPException(status_code=404, detail="User profile not found") + + roles = [record["role"] for record in records] + if UserRole.PROJECT_CREATOR.name not in roles: + raise HTTPException( + status_code=403, detail="Access forbidden for non-Project Creator users" + ) + pending_tasks = await task_schemas.Task.get_project_task_by_id(db, user_id) + if pending_tasks is None: + raise HTTPException(status_code=404, detail="Project not found") + return pending_tasks diff --git a/src/backend/app/tasks/task_schemas.py b/src/backend/app/tasks/task_schemas.py index 3f60671a..b4337a6a 100644 --- a/src/backend/app/tasks/task_schemas.py +++ b/src/backend/app/tasks/task_schemas.py @@ -1,15 +1,291 @@ from pydantic import BaseModel -from app.models.enums import EventType +from app.models.enums import EventType, HTTPStatus, State import uuid from datetime import datetime +from psycopg import Connection +from loguru import logger as log +from fastapi import HTTPException +from psycopg.rows import class_row +from psycopg.rows import dict_row class NewEvent(BaseModel): event: EventType +class Task(BaseModel): + @staticmethod + async def update( + db: Connection, + project_id: uuid.UUID, + task_id: uuid.UUID, + user_id: str, + comment: str, + initial_state: State, + final_state: State, + ): + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute( + """ + WITH last AS ( + SELECT * + FROM task_events + WHERE project_id = :%(project_id)s AND task_id = :%(task_id)s + ORDER BY created_at DESC + LIMIT 1 + ), + updated AS ( + UPDATE task_events + SET state = :%(final_state)s, comment = :%(comment)s, created_at = now() + WHERE EXISTS ( + SELECT 1 + FROM last + WHERE user_id = :%(user_id)s AND state = :%(initial_state)s + ) + RETURNING project_id, task_id, user_id, state + ) + INSERT INTO task_events (event_id, project_id, task_id, user_id, state, comment, created_at) + SELECT gen_random_uuid(), :%(project_id)s, :%(task_id)s, :%(user_id)s, :%(final_state)s, :%(comment)s, now() + WHERE NOT EXISTS ( + SELECT 1 + FROM updated + ) + RETURNING project_id, task_id, user_id, state; + """, + { + "project_id": str(project_id), + "task_id": str(task_id), + "user_id": str(user_id), + "comment": comment, + "initial_state": initial_state.name, + "final_state": final_state.name, + }, + ) + + result = await db.fetchone() + + return { + "project_id": result["project_id"], + "task_id": result["task_id"], + "comment": comment, + } + + @staticmethod + async def get_project_task_by_id(db: Connection, user_id: str): + """Get a list of pending tasks created by a specific user (project creator).""" + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute( + """SELECT id FROM projects WHERE author_id = %(user_id)s""", + {"user_id": user_id}, + ) + + project_ids_result = await cur.fetchall() + + project_ids = [row["id"] for row in project_ids_result] + await cur.execute( + """ + SELECT t.id AS task_id, te.event_id, te.user_id, te.project_id, te.comment, te.state, te.created_at + FROM tasks t + LEFT JOIN task_events te ON t.id = te.task_id + WHERE t.project_id = ANY(%(project_ids)s) + AND te.state = %(state)s + ORDER BY t.project_task_index;""", + {"project_ids": project_ids, "state": "REQUEST_FOR_MAPPING"}, + ) + + try: + db_tasks = await cur.fetchall() + except Exception as e: + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail=f"Failed to fetch project tasks. {e}", + ) + return db_tasks + + @staticmethod + async def request_mapping( + db: Connection, + project_id: uuid.UUID, + task_id: uuid.UUID, + user_id: str, + comment: str, + ): + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute( + """ + WITH last AS ( + SELECT * + FROM task_events + WHERE project_id= %(project_id)s AND task_id= %(task_id)s + ORDER BY created_at DESC + LIMIT 1 + ), + released AS ( + SELECT COUNT(*) = 0 AS no_record + FROM task_events + WHERE project_id= %(project_id)s AND task_id= %(task_id)s AND state = %(unlocked_to_map_state)s + ) + INSERT INTO task_events (event_id, project_id, task_id, user_id, comment, state, created_at) + + SELECT + gen_random_uuid(), + %(project_id)s, + %(task_id)s, + %(user_id)s, + %(comment)s, + %(request_for_map_state)s, + now() + FROM last + RIGHT JOIN released ON true + WHERE (last.state = %(unlocked_to_map_state)s OR released.no_record = true); + """, + { + "project_id": str(project_id), + "task_id": str(task_id), + "user_id": str(user_id), + "comment": comment, + "unlocked_to_map_state": State.UNLOCKED_TO_MAP.name, + "request_for_map_state": State.REQUEST_FOR_MAPPING.name, + }, + ) + + await cur.fetchone() + + return {"project_id": project_id, "task_id": task_id, "comment": comment} + + @staticmethod + async def update_or_create_task_state( + db: Connection, + project_id: uuid.UUID, + task_id: uuid.UUID, + user_id: str, + comment: str, + initial_state: State, + final_state: State, + ): + # Update or insert task event + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute( + """ + WITH last AS ( + SELECT * + FROM task_events + WHERE project_id = %(project_id)s AND task_id = %(task_id)s + ORDER BY created_at DESC + LIMIT 1 + ), + updated AS ( + UPDATE task_events + SET state = %(final_state)s, comment = %(comment)s, created_at = now() + WHERE EXISTS ( + SELECT 1 + FROM last + WHERE user_id = %(user_id)s AND state = %(initial_state)s + ) + RETURNING project_id, task_id, user_id, state + ) + INSERT INTO task_events (event_id, project_id, task_id, user_id, state, comment, created_at) + SELECT gen_random_uuid(), %(project_id)s, %(task_id)s, %(user_id)s, %(final_state)s, %(comment)s, now() + WHERE NOT EXISTS ( + SELECT 1 + FROM updated + ) + RETURNING project_id, task_id, user_id, state; + """, + { + "project_id": str(project_id), + "task_id": str(task_id), + "user_id": str(user_id), + "comment": comment, + "initial_state": initial_state.name, + "final_state": final_state.name, + }, + ) + + result = await cur.fetchone() + return { + "project_id": result["project_id"], + "task_id": result["task_id"], + "comment": comment, + } + + class UserTasksStatsOut(BaseModel): task_id: uuid.UUID task_area: float created_at: datetime state: str + project_id: uuid.UUID + + @staticmethod + async def get_tasks_by_user(db: Connection, user_id: str): + async with db.cursor(row_factory=class_row(UserTasksStatsOut)) as cur: + await cur.execute( + """WITH task_details AS ( + SELECT + tasks.id AS task_id, + task_events.project_id AS project_id, + ST_Area(ST_Transform(tasks.outline, 4326)) / 1000000 AS task_area, + task_events.created_at, + task_events.state + FROM + task_events + JOIN + tasks ON task_events.task_id = tasks.id + WHERE + task_events.user_id = %(user_id)s + ) + SELECT + task_details.task_id, + task_details.project_id, + task_details.task_area, + task_details.created_at, + CASE + WHEN task_details.state = 'REQUEST_FOR_MAPPING' THEN 'request logs' + WHEN task_details.state = 'LOCKED_FOR_MAPPING' THEN 'ongoing' + WHEN task_details.state = 'UNLOCKED_DONE' THEN 'completed' + WHEN task_details.state = 'UNFLYABLE_TASK' THEN 'unflyable task' + ELSE 'UNLOCKED_TO_MAP' -- Default case if the state does not match any expected values + END AS state + FROM task_details;""", + {"user_id": user_id}, + ) + try: + return await cur.fetchall() + + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail="Retrieval failed", + ) from e + + @staticmethod + async def get_all_tasks_with_states(db: Connection, project_id: uuid.UUID): + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute( + """ + WITH all_tasks AS ( + SELECT id AS task_id + FROM tasks + WHERE project_id = %(project_id)s + ), + latest_task_events AS ( + SELECT DISTINCT ON (task_id) task_id, state + FROM task_events + WHERE project_id = %(project_id)s + ORDER BY task_id, created_at DESC + ) + SELECT + %(project_id)s AS project_id, + all_tasks.task_id, + COALESCE(latest_task_events.state, %(default_state)s) AS state + FROM all_tasks + LEFT JOIN latest_task_events + ON all_tasks.task_id = latest_task_events.task_id + """, + {"project_id": project_id, "default_state": State.UNLOCKED_TO_MAP.name}, + ) + + tasks_with_states = await cur.fetchall() + return tasks_with_states diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index 69984206..d28e904b 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -1,4 +1,5 @@ -from app.models.enums import UserRole +import uuid +from app.models.enums import State, UserRole from pydantic import BaseModel, EmailStr, ValidationInfo, Field from pydantic.functional_validators import field_validator from typing import Optional @@ -248,3 +249,28 @@ async def get_user_by_email(db: Connection, email: str) -> dict[str, Any] | None await cur.execute(query, (email,)) result = await cur.fetchone() return result if result else None + + @staticmethod + async def get_requested_user_id( + db: Connection, project_id: uuid.UUID, task_id: uuid.UUID + ): + async with db.cursor() as cur: + await cur.execute( + """ + SELECT user_id + FROM task_events + WHERE project_id = %(project_id)s AND task_id = %(task_id)s and state = %(request_for_map_state)s + ORDER BY created_at DESC + LIMIT 1 + """, + { + "project_id": str(project_id), + "task_id": str(task_id), + "request_for_map_state": State.REQUEST_FOR_MAPPING.name, + }, + ) + + result = await cur.fetchone() + if result is None: + raise ValueError("No user requested for mapping") + return result["user_id"] From 5fc9afdec707466923e5e8c4108e12b692f9c64d Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Mon, 12 Aug 2024 16:50:23 +0545 Subject: [PATCH 37/66] feat: shift the _shemas to _crud.py --- src/backend/app/tasks/task_crud.py | 230 ++++++++++++++++++++++++++ src/backend/app/tasks/task_routes.py | 20 +-- src/backend/app/tasks/task_schemas.py | 230 +------------------------- 3 files changed, 240 insertions(+), 240 deletions(-) diff --git a/src/backend/app/tasks/task_crud.py b/src/backend/app/tasks/task_crud.py index e69de29b..06dbe643 100644 --- a/src/backend/app/tasks/task_crud.py +++ b/src/backend/app/tasks/task_crud.py @@ -0,0 +1,230 @@ +from psycopg import Connection +from fastapi import HTTPException +from psycopg.rows import dict_row +import uuid +from app.models.enums import HTTPStatus, State + + +async def update( + db: Connection, + project_id: uuid.UUID, + task_id: uuid.UUID, + user_id: str, + comment: str, + initial_state: State, + final_state: State, +): + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute( + """ + WITH last AS ( + SELECT * + FROM task_events + WHERE project_id = :%(project_id)s AND task_id = :%(task_id)s + ORDER BY created_at DESC + LIMIT 1 + ), + updated AS ( + UPDATE task_events + SET state = :%(final_state)s, comment = :%(comment)s, created_at = now() + WHERE EXISTS ( + SELECT 1 + FROM last + WHERE user_id = :%(user_id)s AND state = :%(initial_state)s + ) + RETURNING project_id, task_id, user_id, state + ) + INSERT INTO task_events (event_id, project_id, task_id, user_id, state, comment, created_at) + SELECT gen_random_uuid(), :%(project_id)s, :%(task_id)s, :%(user_id)s, :%(final_state)s, :%(comment)s, now() + WHERE NOT EXISTS ( + SELECT 1 + FROM updated + ) + RETURNING project_id, task_id, user_id, state; + """, + { + "project_id": str(project_id), + "task_id": str(task_id), + "user_id": str(user_id), + "comment": comment, + "initial_state": initial_state.name, + "final_state": final_state.name, + }, + ) + + result = await db.fetchone() + + return { + "project_id": result["project_id"], + "task_id": result["task_id"], + "comment": comment, + } + + +async def get_project_task_by_id(db: Connection, user_id: str): + """Get a list of pending tasks created by a specific user (project creator).""" + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute( + """SELECT id FROM projects WHERE author_id = %(user_id)s""", + {"user_id": user_id}, + ) + + project_ids_result = await cur.fetchall() + + project_ids = [row["id"] for row in project_ids_result] + await cur.execute( + """ + SELECT t.id AS task_id, te.event_id, te.user_id, te.project_id, te.comment, te.state, te.created_at + FROM tasks t + LEFT JOIN task_events te ON t.id = te.task_id + WHERE t.project_id = ANY(%(project_ids)s) + AND te.state = %(state)s + ORDER BY t.project_task_index;""", + {"project_ids": project_ids, "state": "REQUEST_FOR_MAPPING"}, + ) + + try: + db_tasks = await cur.fetchall() + except Exception as e: + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, + detail=f"Failed to fetch project tasks. {e}", + ) + return db_tasks + + +async def request_mapping( + db: Connection, + project_id: uuid.UUID, + task_id: uuid.UUID, + user_id: str, + comment: str, +): + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute( + """ + WITH last AS ( + SELECT * + FROM task_events + WHERE project_id= %(project_id)s AND task_id= %(task_id)s + ORDER BY created_at DESC + LIMIT 1 + ), + released AS ( + SELECT COUNT(*) = 0 AS no_record + FROM task_events + WHERE project_id= %(project_id)s AND task_id= %(task_id)s AND state = %(unlocked_to_map_state)s + ) + INSERT INTO task_events (event_id, project_id, task_id, user_id, comment, state, created_at) + + SELECT + gen_random_uuid(), + %(project_id)s, + %(task_id)s, + %(user_id)s, + %(comment)s, + %(request_for_map_state)s, + now() + FROM last + RIGHT JOIN released ON true + WHERE (last.state = %(unlocked_to_map_state)s OR released.no_record = true); + """, + { + "project_id": str(project_id), + "task_id": str(task_id), + "user_id": str(user_id), + "comment": comment, + "unlocked_to_map_state": State.UNLOCKED_TO_MAP.name, + "request_for_map_state": State.REQUEST_FOR_MAPPING.name, + }, + ) + + await cur.fetchone() + + return {"project_id": project_id, "task_id": task_id, "comment": comment} + + +async def update_or_create_task_state( + db: Connection, + project_id: uuid.UUID, + task_id: uuid.UUID, + user_id: str, + comment: str, + initial_state: State, + final_state: State, +): + # Update or insert task event + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute( + """ + WITH last AS ( + SELECT * + FROM task_events + WHERE project_id = %(project_id)s AND task_id = %(task_id)s + ORDER BY created_at DESC + LIMIT 1 + ), + updated AS ( + UPDATE task_events + SET state = %(final_state)s, comment = %(comment)s, created_at = now() + WHERE EXISTS ( + SELECT 1 + FROM last + WHERE user_id = %(user_id)s AND state = %(initial_state)s + ) + RETURNING project_id, task_id, user_id, state + ) + INSERT INTO task_events (event_id, project_id, task_id, user_id, state, comment, created_at) + SELECT gen_random_uuid(), %(project_id)s, %(task_id)s, %(user_id)s, %(final_state)s, %(comment)s, now() + WHERE NOT EXISTS ( + SELECT 1 + FROM updated + ) + RETURNING project_id, task_id, user_id, state; + """, + { + "project_id": str(project_id), + "task_id": str(task_id), + "user_id": str(user_id), + "comment": comment, + "initial_state": initial_state.name, + "final_state": final_state.name, + }, + ) + + result = await cur.fetchone() + return { + "project_id": result["project_id"], + "task_id": result["task_id"], + "comment": comment, + } + + +async def get_all_tasks_with_states(db: Connection, project_id: uuid.UUID): + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute( + """ + WITH all_tasks AS ( + SELECT id AS task_id + FROM tasks + WHERE project_id = %(project_id)s + ), + latest_task_events AS ( + SELECT DISTINCT ON (task_id) task_id, state + FROM task_events + WHERE project_id = %(project_id)s + ORDER BY task_id, created_at DESC + ) + SELECT + %(project_id)s AS project_id, + all_tasks.task_id, + COALESCE(latest_task_events.state, %(default_state)s) AS state + FROM all_tasks + LEFT JOIN latest_task_events + ON all_tasks.task_id = latest_task_events.task_id + """, + {"project_id": project_id, "default_state": State.UNLOCKED_TO_MAP.name}, + ) + + tasks_with_states = await cur.fetchall() + return tasks_with_states diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index fba08f4f..394d843a 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -36,9 +36,7 @@ async def task_states( db: Annotated[Connection, Depends(database.get_db)], project_id: uuid.UUID ): """Get all tasks states for a project.""" - return await task_schemas.UserTasksStatsOut.get_all_tasks_with_states( - db, project_id - ) + return await task_crud.get_all_tasks_with_states(db, project_id) @router.post("/event/{project_id}/{task_id}") @@ -66,7 +64,7 @@ async def new_event( State.LOCKED_FOR_MAPPING, ) else: - data = await task_schemas.Task.request_mapping( + data = await task_crud.request_mapping( db, project_id, task_id, @@ -129,7 +127,7 @@ async def new_event( html_content, ) - return await task_schemas.Task.update( + return await task_crud.update( db, project_id, task_id, @@ -174,7 +172,7 @@ async def new_event( html_content, ) - return await task_schemas.Task.update( + return await task_crud.update( db, project_id, task_id, @@ -184,7 +182,7 @@ async def new_event( State.UNLOCKED_TO_MAP, ) case EventType.FINISH: - return await task_schemas.Task.update( + return await task_crud.update( db, project_id, task_id, @@ -194,7 +192,7 @@ async def new_event( State.UNLOCKED_TO_VALIDATE, ) case EventType.VALIDATE: - return task_schemas.Task.update( + return task_crud.update( db, project_id, task_id, @@ -204,7 +202,7 @@ async def new_event( State.LOCKED_FOR_VALIDATION, ) case EventType.GOOD: - return await task_schemas.Task.update( + return await task_crud.update( db, project_id, task_id, @@ -215,7 +213,7 @@ async def new_event( ) case EventType.BAD: - return await task_schemas.Task.update( + return await task_crud.update( db, project_id, task_id, @@ -251,7 +249,7 @@ async def get_pending_tasks( raise HTTPException( status_code=403, detail="Access forbidden for non-Project Creator users" ) - pending_tasks = await task_schemas.Task.get_project_task_by_id(db, user_id) + pending_tasks = await task_crud.get_project_task_by_id(db, user_id) if pending_tasks is None: raise HTTPException(status_code=404, detail="Project not found") return pending_tasks diff --git a/src/backend/app/tasks/task_schemas.py b/src/backend/app/tasks/task_schemas.py index b4337a6a..d63cb847 100644 --- a/src/backend/app/tasks/task_schemas.py +++ b/src/backend/app/tasks/task_schemas.py @@ -1,215 +1,17 @@ from pydantic import BaseModel -from app.models.enums import EventType, HTTPStatus, State +from app.models.enums import EventType, HTTPStatus import uuid from datetime import datetime from psycopg import Connection from loguru import logger as log from fastapi import HTTPException from psycopg.rows import class_row -from psycopg.rows import dict_row class NewEvent(BaseModel): event: EventType -class Task(BaseModel): - @staticmethod - async def update( - db: Connection, - project_id: uuid.UUID, - task_id: uuid.UUID, - user_id: str, - comment: str, - initial_state: State, - final_state: State, - ): - async with db.cursor(row_factory=dict_row) as cur: - await cur.execute( - """ - WITH last AS ( - SELECT * - FROM task_events - WHERE project_id = :%(project_id)s AND task_id = :%(task_id)s - ORDER BY created_at DESC - LIMIT 1 - ), - updated AS ( - UPDATE task_events - SET state = :%(final_state)s, comment = :%(comment)s, created_at = now() - WHERE EXISTS ( - SELECT 1 - FROM last - WHERE user_id = :%(user_id)s AND state = :%(initial_state)s - ) - RETURNING project_id, task_id, user_id, state - ) - INSERT INTO task_events (event_id, project_id, task_id, user_id, state, comment, created_at) - SELECT gen_random_uuid(), :%(project_id)s, :%(task_id)s, :%(user_id)s, :%(final_state)s, :%(comment)s, now() - WHERE NOT EXISTS ( - SELECT 1 - FROM updated - ) - RETURNING project_id, task_id, user_id, state; - """, - { - "project_id": str(project_id), - "task_id": str(task_id), - "user_id": str(user_id), - "comment": comment, - "initial_state": initial_state.name, - "final_state": final_state.name, - }, - ) - - result = await db.fetchone() - - return { - "project_id": result["project_id"], - "task_id": result["task_id"], - "comment": comment, - } - - @staticmethod - async def get_project_task_by_id(db: Connection, user_id: str): - """Get a list of pending tasks created by a specific user (project creator).""" - async with db.cursor(row_factory=dict_row) as cur: - await cur.execute( - """SELECT id FROM projects WHERE author_id = %(user_id)s""", - {"user_id": user_id}, - ) - - project_ids_result = await cur.fetchall() - - project_ids = [row["id"] for row in project_ids_result] - await cur.execute( - """ - SELECT t.id AS task_id, te.event_id, te.user_id, te.project_id, te.comment, te.state, te.created_at - FROM tasks t - LEFT JOIN task_events te ON t.id = te.task_id - WHERE t.project_id = ANY(%(project_ids)s) - AND te.state = %(state)s - ORDER BY t.project_task_index;""", - {"project_ids": project_ids, "state": "REQUEST_FOR_MAPPING"}, - ) - - try: - db_tasks = await cur.fetchall() - except Exception as e: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=f"Failed to fetch project tasks. {e}", - ) - return db_tasks - - @staticmethod - async def request_mapping( - db: Connection, - project_id: uuid.UUID, - task_id: uuid.UUID, - user_id: str, - comment: str, - ): - async with db.cursor(row_factory=dict_row) as cur: - await cur.execute( - """ - WITH last AS ( - SELECT * - FROM task_events - WHERE project_id= %(project_id)s AND task_id= %(task_id)s - ORDER BY created_at DESC - LIMIT 1 - ), - released AS ( - SELECT COUNT(*) = 0 AS no_record - FROM task_events - WHERE project_id= %(project_id)s AND task_id= %(task_id)s AND state = %(unlocked_to_map_state)s - ) - INSERT INTO task_events (event_id, project_id, task_id, user_id, comment, state, created_at) - - SELECT - gen_random_uuid(), - %(project_id)s, - %(task_id)s, - %(user_id)s, - %(comment)s, - %(request_for_map_state)s, - now() - FROM last - RIGHT JOIN released ON true - WHERE (last.state = %(unlocked_to_map_state)s OR released.no_record = true); - """, - { - "project_id": str(project_id), - "task_id": str(task_id), - "user_id": str(user_id), - "comment": comment, - "unlocked_to_map_state": State.UNLOCKED_TO_MAP.name, - "request_for_map_state": State.REQUEST_FOR_MAPPING.name, - }, - ) - - await cur.fetchone() - - return {"project_id": project_id, "task_id": task_id, "comment": comment} - - @staticmethod - async def update_or_create_task_state( - db: Connection, - project_id: uuid.UUID, - task_id: uuid.UUID, - user_id: str, - comment: str, - initial_state: State, - final_state: State, - ): - # Update or insert task event - async with db.cursor(row_factory=dict_row) as cur: - await cur.execute( - """ - WITH last AS ( - SELECT * - FROM task_events - WHERE project_id = %(project_id)s AND task_id = %(task_id)s - ORDER BY created_at DESC - LIMIT 1 - ), - updated AS ( - UPDATE task_events - SET state = %(final_state)s, comment = %(comment)s, created_at = now() - WHERE EXISTS ( - SELECT 1 - FROM last - WHERE user_id = %(user_id)s AND state = %(initial_state)s - ) - RETURNING project_id, task_id, user_id, state - ) - INSERT INTO task_events (event_id, project_id, task_id, user_id, state, comment, created_at) - SELECT gen_random_uuid(), %(project_id)s, %(task_id)s, %(user_id)s, %(final_state)s, %(comment)s, now() - WHERE NOT EXISTS ( - SELECT 1 - FROM updated - ) - RETURNING project_id, task_id, user_id, state; - """, - { - "project_id": str(project_id), - "task_id": str(task_id), - "user_id": str(user_id), - "comment": comment, - "initial_state": initial_state.name, - "final_state": final_state.name, - }, - ) - - result = await cur.fetchone() - return { - "project_id": result["project_id"], - "task_id": result["task_id"], - "comment": comment, - } - - class UserTasksStatsOut(BaseModel): task_id: uuid.UUID task_area: float @@ -259,33 +61,3 @@ async def get_tasks_by_user(db: Connection, user_id: str): status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Retrieval failed", ) from e - - @staticmethod - async def get_all_tasks_with_states(db: Connection, project_id: uuid.UUID): - async with db.cursor(row_factory=dict_row) as cur: - await cur.execute( - """ - WITH all_tasks AS ( - SELECT id AS task_id - FROM tasks - WHERE project_id = %(project_id)s - ), - latest_task_events AS ( - SELECT DISTINCT ON (task_id) task_id, state - FROM task_events - WHERE project_id = %(project_id)s - ORDER BY task_id, created_at DESC - ) - SELECT - %(project_id)s AS project_id, - all_tasks.task_id, - COALESCE(latest_task_events.state, %(default_state)s) AS state - FROM all_tasks - LEFT JOIN latest_task_events - ON all_tasks.task_id = latest_task_events.task_id - """, - {"project_id": project_id, "default_state": State.UNLOCKED_TO_MAP.name}, - ) - - tasks_with_states = await cur.fetchall() - return tasks_with_states From e7c3d8ef4bf49611081086ae08b0d9b4635379b5 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Mon, 12 Aug 2024 17:10:33 +0545 Subject: [PATCH 38/66] fix: changes update to update_task_state --- src/backend/app/tasks/task_crud.py | 2 +- src/backend/app/tasks/task_routes.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/backend/app/tasks/task_crud.py b/src/backend/app/tasks/task_crud.py index 06dbe643..a74dcf9a 100644 --- a/src/backend/app/tasks/task_crud.py +++ b/src/backend/app/tasks/task_crud.py @@ -5,7 +5,7 @@ from app.models.enums import HTTPStatus, State -async def update( +async def update_task_state( db: Connection, project_id: uuid.UUID, task_id: uuid.UUID, diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 394d843a..76948524 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -127,7 +127,7 @@ async def new_event( html_content, ) - return await task_crud.update( + return await task_crud.update_task_state( db, project_id, task_id, @@ -172,7 +172,7 @@ async def new_event( html_content, ) - return await task_crud.update( + return await task_crud.update_task_state( db, project_id, task_id, @@ -182,7 +182,7 @@ async def new_event( State.UNLOCKED_TO_MAP, ) case EventType.FINISH: - return await task_crud.update( + return await task_crud.update_task_state( db, project_id, task_id, @@ -192,7 +192,7 @@ async def new_event( State.UNLOCKED_TO_VALIDATE, ) case EventType.VALIDATE: - return task_crud.update( + return task_crud.update_task_state( db, project_id, task_id, @@ -202,7 +202,7 @@ async def new_event( State.LOCKED_FOR_VALIDATION, ) case EventType.GOOD: - return await task_crud.update( + return await task_crud.update_task_state( db, project_id, task_id, @@ -213,7 +213,7 @@ async def new_event( ) case EventType.BAD: - return await task_crud.update( + return await task_crud.update_task_state( db, project_id, task_id, From 6f68ee836cdace176319b8301dfd6a290486a492 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 13 Aug 2024 10:00:50 +0545 Subject: [PATCH 39/66] refractor: update request mapping functions --- src/backend/app/projects/project_crud.py | 13 --- src/backend/app/projects/project_schemas.py | 2 + src/backend/app/tasks/task_crud.py | 101 ++------------------ src/backend/app/tasks/task_routes.py | 35 +++---- src/backend/app/tasks/task_schemas.py | 54 ++++++++++- src/backend/app/users/user_schemas.py | 3 +- 6 files changed, 83 insertions(+), 125 deletions(-) diff --git a/src/backend/app/projects/project_crud.py b/src/backend/app/projects/project_crud.py index 3e4b7630..751070de 100644 --- a/src/backend/app/projects/project_crud.py +++ b/src/backend/app/projects/project_crud.py @@ -6,23 +6,10 @@ from fastapi.concurrency import run_in_threadpool from psycopg import Connection from geojson_pydantic import FeatureCollection -from psycopg.rows import dict_row - from app.models.enums import HTTPStatus from app.utils import merge_multipolygon -async def get_project_by_id(db: Connection, project_id: uuid.UUID): - "Get a single database project object by project_id" - async with db.cursor(row_factory=dict_row) as cur: - await cur.execute( - """ select * from projects where id=%(project_id)s""", - {"project_id": project_id}, - ) - result = await cur.fetchone() - return result - - async def create_tasks_from_geojson( db: Connection, project_id: uuid.UUID, diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index f010ae76..dc72993a 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -133,6 +133,8 @@ class DbProject(BaseModel): no_fly_zones: Optional[MultiPolygon] task_count: int = 0 tasks: Optional[list[TaskOut]] = [] + requires_approval_from_manager_for_locking: Optional[bool] + author_id: Optional[str] # TODO add all remaining project fields and validators @staticmethod diff --git a/src/backend/app/tasks/task_crud.py b/src/backend/app/tasks/task_crud.py index a74dcf9a..91b0f5c1 100644 --- a/src/backend/app/tasks/task_crud.py +++ b/src/backend/app/tasks/task_crud.py @@ -52,7 +52,7 @@ async def update_task_state( }, ) - result = await db.fetchone() + result = await cur.fetchone() return { "project_id": result["project_id"], @@ -99,6 +99,8 @@ async def request_mapping( task_id: uuid.UUID, user_id: str, comment: str, + initial_state: State, + final_state: State, ): async with db.cursor(row_factory=dict_row) as cur: await cur.execute( @@ -134,97 +136,10 @@ async def request_mapping( "task_id": str(task_id), "user_id": str(user_id), "comment": comment, - "unlocked_to_map_state": State.UNLOCKED_TO_MAP.name, - "request_for_map_state": State.REQUEST_FOR_MAPPING.name, - }, - ) - - await cur.fetchone() + "unlocked_to_map_state": initial_state.name, #State.UNLOCKED_TO_MAP.name, + "request_for_map_state": final_state.name #State.REQUEST_FOR_MAPPING.name, + },) + # result = await cur.fetchone() + # return result return {"project_id": project_id, "task_id": task_id, "comment": comment} - - -async def update_or_create_task_state( - db: Connection, - project_id: uuid.UUID, - task_id: uuid.UUID, - user_id: str, - comment: str, - initial_state: State, - final_state: State, -): - # Update or insert task event - async with db.cursor(row_factory=dict_row) as cur: - await cur.execute( - """ - WITH last AS ( - SELECT * - FROM task_events - WHERE project_id = %(project_id)s AND task_id = %(task_id)s - ORDER BY created_at DESC - LIMIT 1 - ), - updated AS ( - UPDATE task_events - SET state = %(final_state)s, comment = %(comment)s, created_at = now() - WHERE EXISTS ( - SELECT 1 - FROM last - WHERE user_id = %(user_id)s AND state = %(initial_state)s - ) - RETURNING project_id, task_id, user_id, state - ) - INSERT INTO task_events (event_id, project_id, task_id, user_id, state, comment, created_at) - SELECT gen_random_uuid(), %(project_id)s, %(task_id)s, %(user_id)s, %(final_state)s, %(comment)s, now() - WHERE NOT EXISTS ( - SELECT 1 - FROM updated - ) - RETURNING project_id, task_id, user_id, state; - """, - { - "project_id": str(project_id), - "task_id": str(task_id), - "user_id": str(user_id), - "comment": comment, - "initial_state": initial_state.name, - "final_state": final_state.name, - }, - ) - - result = await cur.fetchone() - return { - "project_id": result["project_id"], - "task_id": result["task_id"], - "comment": comment, - } - - -async def get_all_tasks_with_states(db: Connection, project_id: uuid.UUID): - async with db.cursor(row_factory=dict_row) as cur: - await cur.execute( - """ - WITH all_tasks AS ( - SELECT id AS task_id - FROM tasks - WHERE project_id = %(project_id)s - ), - latest_task_events AS ( - SELECT DISTINCT ON (task_id) task_id, state - FROM task_events - WHERE project_id = %(project_id)s - ORDER BY task_id, created_at DESC - ) - SELECT - %(project_id)s AS project_id, - all_tasks.task_id, - COALESCE(latest_task_events.state, %(default_state)s) AS state - FROM all_tasks - LEFT JOIN latest_task_events - ON all_tasks.task_id = latest_task_events.task_id - """, - {"project_id": project_id, "default_state": State.UNLOCKED_TO_MAP.name}, - ) - - tasks_with_states = await cur.fetchall() - return tasks_with_states diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 76948524..efcd5e4c 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -1,5 +1,6 @@ import uuid from typing import Annotated +from app.projects import project_deps, project_schemas from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from app.config import settings from app.models.enums import EventType, State, UserRole @@ -10,7 +11,6 @@ from psycopg import Connection from app.db import database from app.utils import send_notification_email, render_email_template -from app.projects.project_crud import get_project_by_id from psycopg.rows import dict_row router = APIRouter( @@ -36,7 +36,7 @@ async def task_states( db: Annotated[Connection, Depends(database.get_db)], project_id: uuid.UUID ): """Get all tasks states for a project.""" - return await task_crud.get_all_tasks_with_states(db, project_id) + return await task_schemas.Task.all(db, project_id) @router.post("/event/{project_id}/{task_id}") @@ -47,20 +47,23 @@ async def new_event( task_id: uuid.UUID, detail: task_schemas.NewEvent, user_data: Annotated[AuthUser, Depends(login_required)], + project: Annotated[ + project_schemas.DbProject, Depends(project_deps.get_project_by_id) + ], ): user_id = user_data.id - + project = project.model_dump() + # print(project) match detail.event: case EventType.REQUESTS: - project = await get_project_by_id(db, project_id) if project["requires_approval_from_manager_for_locking"] is False: - data = await task_crud.update_or_create_task_state( + data = await task_crud.request_mapping( db, project_id, task_id, user_id, "Request accepted automatically", - State.REQUEST_FOR_MAPPING, + State.UNLOCKED_TO_MAP, State.LOCKED_FOR_MAPPING, ) else: @@ -70,18 +73,19 @@ async def new_event( task_id, user_id, "Request for mapping", + State.UNLOCKED_TO_MAP, + State.REQUEST_FOR_MAPPING, ) # email notification - author = await user_schemas.DbUser.get_user_by_id(db, project.author_id) - + author = await user_schemas.DbUser.get_user_by_id(db, project['author_id']) html_content = render_email_template( template_name="mapping_requests.html", context={ - "name": author.name, + "name": author['name'], "drone_operator_name": user_data.name, "task_id": task_id, - "project_name": project.name, - "description": project.description, + "project_name": project['name'], + "description": project['description'], }, ) background_tasks.add_task( @@ -93,8 +97,8 @@ async def new_event( return data case EventType.MAP: - project = await get_project_by_id(db, project_id) - if user_id != project.author_id: + # project = await get_project_by_id(db, project_id) + if user_id != project['author_id']: raise HTTPException( status_code=403, detail="Only the project creator can approve the mapping.", @@ -138,8 +142,8 @@ async def new_event( ) case EventType.REJECTED: - project = await get_project_by_id(db, project_id) - if user_id != project.author_id: + # project = await get_project_by_id(db, project_id) + if user_id != project['author_id']: raise HTTPException( status_code=403, detail="Only the project creator can approve the mapping.", @@ -240,7 +244,6 @@ async def get_pending_tasks( {"user_id": user_id}, ) records = await cur.fetchall() - print("*" * 100, records) if not records: raise HTTPException(status_code=404, detail="User profile not found") diff --git a/src/backend/app/tasks/task_schemas.py b/src/backend/app/tasks/task_schemas.py index d63cb847..6b1de296 100644 --- a/src/backend/app/tasks/task_schemas.py +++ b/src/backend/app/tasks/task_schemas.py @@ -1,5 +1,6 @@ -from pydantic import BaseModel -from app.models.enums import EventType, HTTPStatus +from typing import Optional +from pydantic import BaseModel, validator +from app.models.enums import EventType, HTTPStatus, State import uuid from datetime import datetime from psycopg import Connection @@ -12,6 +13,55 @@ class NewEvent(BaseModel): event: EventType +class Task(BaseModel): + task_id: uuid.UUID + task_area: float = None + created_at: datetime = None + state: State = None + project_id: uuid.UUID + # final_state: Optional[State] = None + # initial_state: Optional[State] = None + + @validator("state", pre=True, always=True) + def validate_state(cls, v): + if isinstance(v, str): + # Attempt to match the string to an enum value + try: + return State[v] + except KeyError: + raise ValueError(f"Unknown state label: {v}") + return v + + async def all(db: Connection, project_id: uuid.UUID): + async with db.cursor(row_factory=class_row(Task)) as cur: + await cur.execute( + """ + WITH all_tasks AS ( + SELECT id AS task_id + FROM tasks + WHERE project_id = %(project_id)s + ), + latest_task_events AS ( + SELECT DISTINCT ON (task_id) task_id, state + FROM task_events + WHERE project_id = %(project_id)s + ORDER BY task_id, created_at DESC + ) + SELECT + %(project_id)s AS project_id, + all_tasks.task_id, + COALESCE(latest_task_events.state, %(default_state)s) AS state + FROM all_tasks + LEFT JOIN latest_task_events + ON all_tasks.task_id = latest_task_events.task_id + """, + {"project_id": project_id, "default_state": State.UNLOCKED_TO_MAP.name}, + ) + + tasks_with_states = await cur.fetchall() + return tasks_with_states + + class UserTasksStatsOut(BaseModel): task_id: uuid.UUID task_area: float diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index d28e904b..1dd587b3 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -10,6 +10,7 @@ from typing import Any from loguru import logger as log from app.users import user_crud +from psycopg.rows import dict_row class AuthUser(BaseModel): @@ -237,7 +238,7 @@ async def get_or_create_user(db: Connection, user_data: AuthUser): @staticmethod async def get_user_by_id(db: Connection, id: str) -> dict[str, Any] | None: query = "SELECT * FROM users WHERE id = %s LIMIT 1;" - async with db.cursor() as cur: + async with db.cursor(row_factory=dict_row) as cur: await cur.execute(query, (id,)) result = await cur.fetchone() return result if result else None From a6870d13630795aeeb73e8bc339479c7ae7853d9 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 13 Aug 2024 10:21:42 +0545 Subject: [PATCH 40/66] fix: rename functions name in pending tasks --- src/backend/app/tasks/task_crud.py | 9 +++++---- src/backend/app/tasks/task_routes.py | 19 ++++++++++--------- src/backend/app/tasks/task_schemas.py | 1 - 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/backend/app/tasks/task_crud.py b/src/backend/app/tasks/task_crud.py index 91b0f5c1..913be0a1 100644 --- a/src/backend/app/tasks/task_crud.py +++ b/src/backend/app/tasks/task_crud.py @@ -61,7 +61,7 @@ async def update_task_state( } -async def get_project_task_by_id(db: Connection, user_id: str): +async def get_pending_tasks_for_user(db: Connection, user_id: str): """Get a list of pending tasks created by a specific user (project creator).""" async with db.cursor(row_factory=dict_row) as cur: await cur.execute( @@ -136,9 +136,10 @@ async def request_mapping( "task_id": str(task_id), "user_id": str(user_id), "comment": comment, - "unlocked_to_map_state": initial_state.name, #State.UNLOCKED_TO_MAP.name, - "request_for_map_state": final_state.name #State.REQUEST_FOR_MAPPING.name, - },) + "unlocked_to_map_state": initial_state.name, # State.UNLOCKED_TO_MAP.name, + "request_for_map_state": final_state.name, # State.REQUEST_FOR_MAPPING.name, + }, + ) # result = await cur.fetchone() # return result diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index efcd5e4c..72d86ce3 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -52,8 +52,7 @@ async def new_event( ], ): user_id = user_data.id - project = project.model_dump() - # print(project) + project = project.model_dump() match detail.event: case EventType.REQUESTS: if project["requires_approval_from_manager_for_locking"] is False: @@ -77,15 +76,17 @@ async def new_event( State.REQUEST_FOR_MAPPING, ) # email notification - author = await user_schemas.DbUser.get_user_by_id(db, project['author_id']) + author = await user_schemas.DbUser.get_user_by_id( + db, project["author_id"] + ) html_content = render_email_template( template_name="mapping_requests.html", context={ - "name": author['name'], + "name": author["name"], "drone_operator_name": user_data.name, "task_id": task_id, - "project_name": project['name'], - "description": project['description'], + "project_name": project["name"], + "description": project["description"], }, ) background_tasks.add_task( @@ -98,7 +99,7 @@ async def new_event( case EventType.MAP: # project = await get_project_by_id(db, project_id) - if user_id != project['author_id']: + if user_id != project["author_id"]: raise HTTPException( status_code=403, detail="Only the project creator can approve the mapping.", @@ -143,7 +144,7 @@ async def new_event( case EventType.REJECTED: # project = await get_project_by_id(db, project_id) - if user_id != project['author_id']: + if user_id != project["author_id"]: raise HTTPException( status_code=403, detail="Only the project creator can approve the mapping.", @@ -252,7 +253,7 @@ async def get_pending_tasks( raise HTTPException( status_code=403, detail="Access forbidden for non-Project Creator users" ) - pending_tasks = await task_crud.get_project_task_by_id(db, user_id) + pending_tasks = await task_crud.get_pending_tasks_for_user(db, user_id) if pending_tasks is None: raise HTTPException(status_code=404, detail="Project not found") return pending_tasks diff --git a/src/backend/app/tasks/task_schemas.py b/src/backend/app/tasks/task_schemas.py index 6b1de296..a8a08570 100644 --- a/src/backend/app/tasks/task_schemas.py +++ b/src/backend/app/tasks/task_schemas.py @@ -1,4 +1,3 @@ -from typing import Optional from pydantic import BaseModel, validator from app.models.enums import EventType, HTTPStatus, State import uuid From 99f12dc099583ba67ed5a52704296ef05b3ce7d8 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 13 Aug 2024 10:44:01 +0545 Subject: [PATCH 41/66] Fix: Access project details and drone operator. --- src/backend/app/tasks/task_routes.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 72d86ce3..d0dcada5 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -118,16 +118,16 @@ async def new_event( "email_body": "We are pleased to inform you that your mapping request has been approved. Your contribution is invaluable to our efforts in improving humanitarian responses worldwide.", "task_status": "approved", "name": user_data.name, - "drone_operator_name": drone_operator.name, + "drone_operator_name": drone_operator["name"], "task_id": task_id, - "project_name": project.name, - "description": project.description, + "project_name": project["name"], + "description": project["description"], }, ) background_tasks.add_task( send_notification_email, - drone_operator.email_address, + drone_operator["email_address"], "Task is approved", html_content, ) @@ -163,16 +163,16 @@ async def new_event( "email_body": "We are sorry to inform you that your mapping request has been rejected.", "task_status": "rejected", "name": user_data.name, - "drone_operator_name": drone_operator.name, + "drone_operator_name": drone_operator["name"], "task_id": task_id, - "project_name": project.name, - "description": project.description, + "project_name": project["name"], + "description": project["description"], }, ) background_tasks.add_task( send_notification_email, - drone_operator.email_address, + drone_operator["email_address"], "Task is Rejected", html_content, ) From df0df4451a0696d1caef42634951e219a28e7cc5 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 13 Aug 2024 13:31:55 +0545 Subject: [PATCH 42/66] refractor: refractor the statistics & read_task with pyscopg --- src/backend/app/tasks/task_routes.py | 99 +++++++++++++++++----------- 1 file changed, 59 insertions(+), 40 deletions(-) diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 0e87cd7e..4f7fbf49 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -28,31 +28,40 @@ async def read_task( ): "Retrieve details of a specific task by its ID." try: - query = """ - SELECT - ST_Area(ST_Transform(tasks.outline, 4326)) / 1000000 AS task_area, - task_events.created_at, - projects.name AS project_name, - project_task_index, - projects.front_overlap AS front_overlap, - projects.side_overlap AS side_overlap, - projects.gsd_cm_px AS gsd_cm_px, - projects.gimble_angles_degrees AS gimble_angles_degrees - FROM - task_events - JOIN - tasks ON task_events.task_id = tasks.id - JOIN - projects ON task_events.project_id = projects.id - WHERE - task_events.task_id = :task_id - """ - records = await db.fetch_one(query, values={"task_id": task_id}) - return records + async with db.cursor() as cur: + await cur.execute( + """ + SELECT + ST_Area(ST_Transform(tasks.outline, 4326)) / 1000000 AS task_area, + task_events.created_at, + projects.name AS project_name, + project_task_index, + projects.front_overlap AS front_overlap, + projects.side_overlap AS side_overlap, + projects.gsd_cm_px AS gsd_cm_px, + projects.gimble_angles_degrees AS gimble_angles_degrees + FROM + task_events + JOIN + tasks ON task_events.task_id = tasks.id + JOIN + projects ON task_events.project_id = projects.id + WHERE + task_events.task_id = %(task_id)s + """, + {"task_id": task_id}, + ) + task = await cur.fetchone() + if task is None: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail=f"Task with ID {task_id} not found.", + ) + return task except Exception as e: raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=f"Failed to fetch tasks. {e}", + detail=f"Failed to fetch task. {e}", ) @@ -63,30 +72,40 @@ async def get_task_stats( ): "Retrieve statistics related to tasks for the authenticated user." user_id = user_data.id - query = """SELECT role FROM user_profile WHERE user_id = :user_id""" - records = await db.fetch_all(query, {"user_id": user_id}) - - if not records: - raise HTTPException(status_code=404, detail="User profile not found") - raw_sql = """ - SELECT - COUNT(CASE WHEN te.state = 'REQUEST_FOR_MAPPING' THEN 1 END) AS request_logs, - COUNT(CASE WHEN te.state = 'LOCKED_FOR_MAPPING' THEN 1 END) AS ongoing_tasks, - COUNT(CASE WHEN te.state = 'UNLOCKED_DONE' THEN 1 END) AS completed_tasks, - COUNT(CASE WHEN te.state = 'UNFLYABLE_TASK' THEN 1 END) AS unflyable_tasks - FROM tasks t - LEFT JOIN task_events te ON t.id = te.task_id - WHERE t.project_id IN (SELECT id FROM projects WHERE author_id = :user_id); - """ try: - db_counts = await db.fetch_one(query=raw_sql, values={"user_id": user_id}) + async with db.cursor() as cur: + # Check if the user profile exists + await cur.execute( + """SELECT role FROM user_profile WHERE user_id = %(user_id)s""", + {"user_id": user_id}, + ) + records = await cur.fetchone() + + if not records: + raise HTTPException(status_code=404, detail="User profile not found") + + # Query for task statistics + raw_sql = """ + SELECT + COUNT(CASE WHEN te.state = 'REQUEST_FOR_MAPPING' THEN 1 END) AS request_logs, + COUNT(CASE WHEN te.state = 'LOCKED_FOR_MAPPING' THEN 1 END) AS ongoing_tasks, + COUNT(CASE WHEN te.state = 'UNLOCKED_DONE' THEN 1 END) AS completed_tasks, + COUNT(CASE WHEN te.state = 'UNFLYABLE_TASK' THEN 1 END) AS unflyable_tasks + FROM tasks t + LEFT JOIN task_events te ON t.id = te.task_id + WHERE t.project_id IN (SELECT id FROM projects WHERE author_id = %(user_id)s); + """ + await cur.execute(raw_sql, {"user_id": user_id}) + db_counts = await cur.fetchone() + + return db_counts + except Exception as e: raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=f"Failed to fetch task counts. {e}", + detail=f"Failed to fetch task statistics. {e}", ) - return db_counts @router.get("/", response_model=list[task_schemas.UserTasksStatsOut]) From 855539b08780c97aed3e8677e74d696f6721f36c Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 13 Aug 2024 13:46:32 +0545 Subject: [PATCH 43/66] refractor: changes waypoints module with pyscopg --- src/backend/app/tasks/task_crud.py | 26 ++++++++++++++++++++ src/backend/app/tasks/task_routes.py | 2 +- src/backend/app/waypoints/waypoint_routes.py | 10 ++++---- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/backend/app/tasks/task_crud.py b/src/backend/app/tasks/task_crud.py index 1248c4b8..4184c103 100644 --- a/src/backend/app/tasks/task_crud.py +++ b/src/backend/app/tasks/task_crud.py @@ -3,7 +3,33 @@ from app.models.enums import HTTPStatus, State from fastapi import HTTPException from psycopg.rows import dict_row +import json +async def get_task_geojson(db: Connection, task_id: uuid.UUID): + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute(""" + SELECT jsonb_build_object( + 'type', 'FeatureCollection', + 'features', jsonb_agg( + jsonb_build_object( + 'type', 'Feature', + 'geometry', ST_AsGeoJSON(outline)::jsonb, + 'properties', jsonb_build_object( + 'id', id + ) + ) + ) + ) as geom + FROM tasks + WHERE id = :task_id; + """, {"task_id": str(task_id)}) + + data = await db.fetchone() + + if data is None: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Task not found") + + return json.loads(data["geom"]) async def update_task_state( db: Connection, diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 4f7fbf49..86d3b3ab 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -1,5 +1,5 @@ import uuid -from typing import Annotated +from typing import Annotated from app.projects import project_deps, project_schemas from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from app.config import settings diff --git a/src/backend/app/waypoints/waypoint_routes.py b/src/backend/app/waypoints/waypoint_routes.py index 8416e20a..6840d1f1 100644 --- a/src/backend/app/waypoints/waypoint_routes.py +++ b/src/backend/app/waypoints/waypoint_routes.py @@ -7,12 +7,12 @@ from drone_flightplan import flightplan, waypoints from app.models.enums import HTTPStatus from app.tasks.task_crud import get_task_geojson -from app.projects.project_crud import get_project_by_id from app.db import database from app.utils import merge_multipolygon from app.s3 import get_file_from_bucket -from databases import Database - +from typing import Annotated +from psycopg import Connection +from app.projects import project_deps # Constant to convert gsd to Altitude above ground level GSD_to_AGL_CONST = 29.7 # For DJI Mini 4 Pro @@ -26,14 +26,14 @@ @router.get("/task/{task_id}/") async def get_task_waypoint( + db: Annotated[Connection, Depends(database.get_db)], project_id: uuid.UUID, task_id: uuid.UUID, download: bool = True, - db: Database = Depends(database.get_db), ): task_geojson = await get_task_geojson(db, task_id) features = task_geojson["features"][0] - project = await get_project_by_id(db, project_id) + project = await project_deps.get_project_by_id(db, project_id) forward_overlap = project.front_overlap if project.front_overlap else 70 side_overlap = project.side_overlap if project.side_overlap else 70 From b38b58ceb24a130da41a102be0ef2f8ef9de2c22 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 08:03:02 +0000 Subject: [PATCH 44/66] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/backend/app/tasks/task_crud.py | 9 +++++++-- src/backend/app/tasks/task_routes.py | 2 +- src/backend/app/waypoints/waypoint_routes.py | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/backend/app/tasks/task_crud.py b/src/backend/app/tasks/task_crud.py index 4184c103..2bbd53e5 100644 --- a/src/backend/app/tasks/task_crud.py +++ b/src/backend/app/tasks/task_crud.py @@ -5,9 +5,11 @@ from psycopg.rows import dict_row import json + async def get_task_geojson(db: Connection, task_id: uuid.UUID): async with db.cursor(row_factory=dict_row) as cur: - await cur.execute(""" + await cur.execute( + """ SELECT jsonb_build_object( 'type', 'FeatureCollection', 'features', jsonb_agg( @@ -22,7 +24,9 @@ async def get_task_geojson(db: Connection, task_id: uuid.UUID): ) as geom FROM tasks WHERE id = :task_id; - """, {"task_id": str(task_id)}) + """, + {"task_id": str(task_id)}, + ) data = await db.fetchone() @@ -31,6 +35,7 @@ async def get_task_geojson(db: Connection, task_id: uuid.UUID): return json.loads(data["geom"]) + async def update_task_state( db: Connection, project_id: uuid.UUID, diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 86d3b3ab..4f7fbf49 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -1,5 +1,5 @@ import uuid -from typing import Annotated +from typing import Annotated from app.projects import project_deps, project_schemas from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from app.config import settings diff --git a/src/backend/app/waypoints/waypoint_routes.py b/src/backend/app/waypoints/waypoint_routes.py index 6840d1f1..e19d53f0 100644 --- a/src/backend/app/waypoints/waypoint_routes.py +++ b/src/backend/app/waypoints/waypoint_routes.py @@ -10,7 +10,7 @@ from app.db import database from app.utils import merge_multipolygon from app.s3 import get_file_from_bucket -from typing import Annotated +from typing import Annotated from psycopg import Connection from app.projects import project_deps From 1ac7ec8771bbd8c382589cf727839a79429104fb Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 13 Aug 2024 15:12:01 +0545 Subject: [PATCH 45/66] fix: fixes the project creator issues --- src/backend/app/projects/project_crud.py | 10 ++++++++++ src/backend/app/projects/project_schemas.py | 10 +++++----- src/backend/app/tasks/task_crud.py | 9 +++++++-- src/backend/app/tasks/task_routes.py | 2 +- src/backend/app/waypoints/waypoint_routes.py | 2 +- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/backend/app/projects/project_crud.py b/src/backend/app/projects/project_crud.py index 751070de..4bf223b5 100644 --- a/src/backend/app/projects/project_crud.py +++ b/src/backend/app/projects/project_crud.py @@ -9,6 +9,16 @@ from app.models.enums import HTTPStatus from app.utils import merge_multipolygon +async def update_project_dem_url(db: Connection, project_id: uuid.UUID, dem_url: str): + """Update the DEM URL for a project.""" + + async with db.cursor() as cur: + await cur.execute(""" + UPDATE projects + SET dem_url = %(dem_url)s + WHERE id = %(project_id)s""", {"dem_url": dem_url, "project_id": project_id}) + + return True async def create_tasks_from_geojson( db: Connection, diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index 05a765a0..2f0f1c43 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -1,6 +1,6 @@ import json import uuid -from typing import Annotated, Optional, List +from typing import Annotated, Optional, List, Union from datetime import datetime, date import geojson @@ -30,7 +30,8 @@ def validate_geojson( value: FeatureCollection | Feature | Polygon, ) -> geojson.FeatureCollection: """Convert the upload GeoJSON to standardised FeatureCollection.""" - return merge_multipolygon(value.model_dump()) + if value: + return merge_multipolygon(value.model_dump()) def enum_to_str(value: IntEnum) -> str: @@ -140,11 +141,11 @@ class DbProject(BaseModel): organisation_id: Optional[int] outline: Polygon centroid: Optional[Point] - no_fly_zones: Optional[MultiPolygon] + no_fly_zones: Optional[MultiPolygon] = None task_count: int = 0 tasks: Optional[list[TaskOut]] = [] requires_approval_from_manager_for_locking: Optional[bool] - author_id: Optional[str] + author_id: Optional[str] = None # TODO add all remaining project fields and validators @staticmethod @@ -235,7 +236,6 @@ async def create(db: Connection, project: ProjectIn, user_id: str) -> uuid.UUID: msg = f"Project name ({project.name}) already exists!" log.warning(f"User ({user_id}) failed project creation: {msg}") raise HTTPException(status_code=HTTPStatus.CONFLICT, detail=msg) - # NOTE exclude_none is used over exclude_unset, or default value are not included model_dump = project.model_dump( exclude_none=True, exclude=["outline", "centroid"] diff --git a/src/backend/app/tasks/task_crud.py b/src/backend/app/tasks/task_crud.py index 4184c103..2bbd53e5 100644 --- a/src/backend/app/tasks/task_crud.py +++ b/src/backend/app/tasks/task_crud.py @@ -5,9 +5,11 @@ from psycopg.rows import dict_row import json + async def get_task_geojson(db: Connection, task_id: uuid.UUID): async with db.cursor(row_factory=dict_row) as cur: - await cur.execute(""" + await cur.execute( + """ SELECT jsonb_build_object( 'type', 'FeatureCollection', 'features', jsonb_agg( @@ -22,7 +24,9 @@ async def get_task_geojson(db: Connection, task_id: uuid.UUID): ) as geom FROM tasks WHERE id = :task_id; - """, {"task_id": str(task_id)}) + """, + {"task_id": str(task_id)}, + ) data = await db.fetchone() @@ -31,6 +35,7 @@ async def get_task_geojson(db: Connection, task_id: uuid.UUID): return json.loads(data["geom"]) + async def update_task_state( db: Connection, project_id: uuid.UUID, diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 86d3b3ab..4f7fbf49 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -1,5 +1,5 @@ import uuid -from typing import Annotated +from typing import Annotated from app.projects import project_deps, project_schemas from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from app.config import settings diff --git a/src/backend/app/waypoints/waypoint_routes.py b/src/backend/app/waypoints/waypoint_routes.py index 6840d1f1..e19d53f0 100644 --- a/src/backend/app/waypoints/waypoint_routes.py +++ b/src/backend/app/waypoints/waypoint_routes.py @@ -10,7 +10,7 @@ from app.db import database from app.utils import merge_multipolygon from app.s3 import get_file_from_bucket -from typing import Annotated +from typing import Annotated from psycopg import Connection from app.projects import project_deps From f59dfcdfc89f67b66b0b6b4fa27c2884fece51e8 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 13 Aug 2024 15:44:35 +0545 Subject: [PATCH 46/66] fix(frontend): fixes task boundary api fail --- .../src/components/CreateProject/CreateprojectLayout/index.tsx | 2 +- src/frontend/src/services/createproject.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx b/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx index b150e799..4de472f8 100644 --- a/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx +++ b/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx @@ -139,7 +139,7 @@ export default function CreateprojectLayout() { dispatch(setCreateProjectState({ projectId: res.data.project_id })); if (!splitGeojson) return; const geojson = convertGeojsonToFile(splitGeojson); - const formData = prepareFormData({ task_geojson: geojson }); + const formData = prepareFormData({ geojson: geojson }); uploadTaskBoundary({ id: res.data.project_id, data: formData }); reset(); dispatch(resetUploadedAndDrawnAreas()); diff --git a/src/frontend/src/services/createproject.ts b/src/frontend/src/services/createproject.ts index 5de0cbb7..d30e9300 100644 --- a/src/frontend/src/services/createproject.ts +++ b/src/frontend/src/services/createproject.ts @@ -8,7 +8,7 @@ export const getProjectDetail = (id: string) => export const postCreateProject = (data: any) => authenticated(api).post('/projects', data, { - headers: { 'Content-Type': 'application/json' }, + // headers: { 'Content-Type': 'application/json' }, }); export const postPreviewSplitBySquare = (data: any) => From 74f544e7a49185d3e1dbadc62ee188bcd7695b38 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 13 Aug 2024 17:12:14 +0545 Subject: [PATCH 47/66] fix: fixes the task listing issues --- src/backend/app/tasks/task_crud.py | 1 - src/backend/app/tasks/task_routes.py | 6 +- src/backend/app/tasks/task_schemas.py | 91 +++++++++++-------- .../IndividualProject/MapSection/index.tsx | 6 +- 4 files changed, 58 insertions(+), 46 deletions(-) diff --git a/src/backend/app/tasks/task_crud.py b/src/backend/app/tasks/task_crud.py index 2bbd53e5..aaa8e8be 100644 --- a/src/backend/app/tasks/task_crud.py +++ b/src/backend/app/tasks/task_crud.py @@ -101,7 +101,6 @@ async def get_pending_tasks_for_user(db: Connection, user_id: str): ) project_ids_result = await cur.fetchall() - project_ids = [row["id"] for row in project_ids_result] await cur.execute( """ diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 4f7fbf49..cdbd84e3 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -28,7 +28,7 @@ async def read_task( ): "Retrieve details of a specific task by its ID." try: - async with db.cursor() as cur: + async with db.cursor(row_factory=dict_row) as cur: await cur.execute( """ SELECT @@ -124,7 +124,7 @@ async def task_states( db: Annotated[Connection, Depends(database.get_db)], project_id: uuid.UUID ): """Get all tasks states for a project.""" - return await task_schemas.Task.all(db, project_id) + return await task_schemas.Task.all_tasks_states(db, project_id) @router.post("/event/{project_id}/{task_id}") @@ -186,7 +186,6 @@ async def new_event( return data case EventType.MAP: - # project = await get_project_by_id(db, project_id) if user_id != project["author_id"]: raise HTTPException( status_code=403, @@ -231,7 +230,6 @@ async def new_event( ) case EventType.REJECTED: - # project = await get_project_by_id(db, project_id) if user_id != project["author_id"]: raise HTTPException( status_code=403, diff --git a/src/backend/app/tasks/task_schemas.py b/src/backend/app/tasks/task_schemas.py index 28d8a3f4..1d499128 100644 --- a/src/backend/app/tasks/task_schemas.py +++ b/src/backend/app/tasks/task_schemas.py @@ -7,6 +7,7 @@ from fastapi import HTTPException from psycopg.rows import class_row from typing import Optional +from psycopg.rows import dict_row class NewEvent(BaseModel): @@ -20,48 +21,62 @@ class Task(BaseModel): created_at: datetime = None state: State = None project_id: uuid.UUID - # final_state: Optional[State] = None - # initial_state: Optional[State] = None - @validator("state", pre=True, always=True) - def validate_state(cls, v): - if isinstance(v, str): - # Attempt to match the string to an enum value - try: - return State[v] - except KeyError: - raise ValueError(f"Unknown state label: {v}") - return v + # @validator("state", pre=True, always=True) + # def validate_state(cls, v): + # if isinstance(v, str): + # # Attempt to match the string to an enum value + # try: + # return State[v] + # except KeyError: + # raise ValueError(f"Unknown state label: {v}") + # return v + + @staticmethod + async def get_all_tasks(db: Connection, project_id: uuid.UUID): + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute("""SELECT id FROM tasks WHERE project_id = %(project_id)s""",{"project_id": str(project_id)} ) - async def all(db: Connection, project_id: uuid.UUID): - async with db.cursor(row_factory=class_row(Task)) as cur: - await cur.execute( - """ - WITH all_tasks AS ( - SELECT id AS task_id - FROM tasks - WHERE project_id = %(project_id)s - ), - latest_task_events AS ( - SELECT DISTINCT ON (task_id) task_id, state - FROM task_events - WHERE project_id = %(project_id)s - ORDER BY task_id, created_at DESC - ) - SELECT - %(project_id)s AS project_id, - all_tasks.task_id, - COALESCE(latest_task_events.state, %(default_state)s) AS state - FROM all_tasks - LEFT JOIN latest_task_events - ON all_tasks.task_id = latest_task_events.task_id - """, - {"project_id": project_id, "default_state": State.UNLOCKED_TO_MAP.name}, - ) + data = await cur.fetchall() + + # Extracting the list of IDs from the data + task_ids = [task["id"] for task in data] + + return task_ids + + @staticmethod + async def all_tasks_states(db: Connection, project_id: uuid.UUID): + + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute("""SELECT DISTINCT ON (task_id) project_id, task_id, state + FROM task_events + WHERE project_id = %(project_id)s + ORDER BY task_id, created_at DESC + """, {"project_id": project_id}) + + existing_tasks = await cur.fetchall() + + # Get all task_ids from the tasks table + task_ids = await Task.get_all_tasks(db, project_id) + # Create a set of existing task_ids for quick lookup + existing_task_ids = {task["task_id"] for task in existing_tasks} + + # task ids that are not in task_events table + remaining_task_ids = [x for x in task_ids if x not in existing_task_ids] - tasks_with_states = await cur.fetchall() - return tasks_with_states + # Add missing tasks with state as "UNLOCKED_FOR_MAPPING" + remaining_tasks = [ + { + "project_id": str(project_id), + "task_id": task_id, + "state": State.UNLOCKED_TO_MAP.name, + } + for task_id in remaining_task_ids + ] + # Combine both existing tasks and remaining tasks + combined_tasks = existing_tasks + remaining_tasks + return combined_tasks class UserTasksStatsOut(BaseModel): task_id: uuid.UUID diff --git a/src/frontend/src/components/IndividualProject/MapSection/index.tsx b/src/frontend/src/components/IndividualProject/MapSection/index.tsx index 14ee13ac..238a831d 100644 --- a/src/frontend/src/components/IndividualProject/MapSection/index.tsx +++ b/src/frontend/src/components/IndividualProject/MapSection/index.tsx @@ -87,10 +87,10 @@ export default function MapSection() { features: [], }, ); - const bbox = getBbox(tasksCollectiveGeojson as FeatureCollection); - map?.fitBounds(bbox as LngLatBoundsLike, { padding: 25 }); + // const bbox = getBbox(tasksCollectiveGeojson as FeatureCollection); + // map?.fitBounds(bbox as LngLatBoundsLike, { padding: 25 }); }, [map, tasksData]); - + const getPopupUI = useCallback( (properties: Record) => { const status = taskStatusObj?.[properties?.id]; From af7a555ef7c47e5c562ad3097f56268627c14023 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 13 Aug 2024 17:38:46 +0545 Subject: [PATCH 48/66] refractor: changes task module with pyscopg --- src/backend/app/models/enums.py | 3 ++ src/backend/app/projects/project_crud.py | 13 ++++-- src/backend/app/projects/project_schemas.py | 4 +- src/backend/app/tasks/task_routes.py | 2 +- src/backend/app/tasks/task_schemas.py | 45 ++++++++----------- .../IndividualProject/MapSection/index.tsx | 2 +- 6 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/backend/app/models/enums.py b/src/backend/app/models/enums.py index 2a3d3ddd..6e29b1d8 100644 --- a/src/backend/app/models/enums.py +++ b/src/backend/app/models/enums.py @@ -143,6 +143,9 @@ class State(int, Enum): UNLOCKED_DONE = 4 UNFLYABLE_TASK = 5 + def __str__(self): + return self.name + class EventType(str, Enum): """Events that can be used via the API to update a state diff --git a/src/backend/app/projects/project_crud.py b/src/backend/app/projects/project_crud.py index 4bf223b5..0267f5b9 100644 --- a/src/backend/app/projects/project_crud.py +++ b/src/backend/app/projects/project_crud.py @@ -9,17 +9,22 @@ from app.models.enums import HTTPStatus from app.utils import merge_multipolygon + async def update_project_dem_url(db: Connection, project_id: uuid.UUID, dem_url: str): """Update the DEM URL for a project.""" - + async with db.cursor() as cur: - await cur.execute(""" + await cur.execute( + """ UPDATE projects SET dem_url = %(dem_url)s - WHERE id = %(project_id)s""", {"dem_url": dem_url, "project_id": project_id}) - + WHERE id = %(project_id)s""", + {"dem_url": dem_url, "project_id": project_id}, + ) + return True + async def create_tasks_from_geojson( db: Connection, project_id: uuid.UUID, diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index 2f0f1c43..c924bb08 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -1,6 +1,6 @@ import json import uuid -from typing import Annotated, Optional, List, Union +from typing import Annotated, Optional, List from datetime import datetime, date import geojson @@ -30,7 +30,7 @@ def validate_geojson( value: FeatureCollection | Feature | Polygon, ) -> geojson.FeatureCollection: """Convert the upload GeoJSON to standardised FeatureCollection.""" - if value: + if value: return merge_multipolygon(value.model_dump()) diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index cdbd84e3..12ab4f72 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -124,7 +124,7 @@ async def task_states( db: Annotated[Connection, Depends(database.get_db)], project_id: uuid.UUID ): """Get all tasks states for a project.""" - return await task_schemas.Task.all_tasks_states(db, project_id) + return await task_schemas.TaskState.all(db, project_id) @router.post("/event/{project_id}/{task_id}") diff --git a/src/backend/app/tasks/task_schemas.py b/src/backend/app/tasks/task_schemas.py index 1d499128..34b33b9c 100644 --- a/src/backend/app/tasks/task_schemas.py +++ b/src/backend/app/tasks/task_schemas.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, validator +from pydantic import BaseModel from app.models.enums import EventType, HTTPStatus, State import uuid from datetime import datetime @@ -15,27 +15,18 @@ class NewEvent(BaseModel): comment: Optional[str] = None -class Task(BaseModel): +class TaskState(BaseModel): task_id: uuid.UUID - task_area: float = None - created_at: datetime = None - state: State = None + state: str project_id: uuid.UUID - # @validator("state", pre=True, always=True) - # def validate_state(cls, v): - # if isinstance(v, str): - # # Attempt to match the string to an enum value - # try: - # return State[v] - # except KeyError: - # raise ValueError(f"Unknown state label: {v}") - # return v - @staticmethod async def get_all_tasks(db: Connection, project_id: uuid.UUID): async with db.cursor(row_factory=dict_row) as cur: - await cur.execute("""SELECT id FROM tasks WHERE project_id = %(project_id)s""",{"project_id": str(project_id)} ) + await cur.execute( + """SELECT id FROM tasks WHERE project_id = %(project_id)s""", + {"project_id": str(project_id)}, + ) data = await cur.fetchall() @@ -43,23 +34,24 @@ async def get_all_tasks(db: Connection, project_id: uuid.UUID): task_ids = [task["id"] for task in data] return task_ids - - @staticmethod - async def all_tasks_states(db: Connection, project_id: uuid.UUID): - async with db.cursor(row_factory=dict_row) as cur: - await cur.execute("""SELECT DISTINCT ON (task_id) project_id, task_id, state + @staticmethod + async def all(db: Connection, project_id: uuid.UUID): + async with db.cursor(row_factory=class_row(TaskState)) as cur: + await cur.execute( + """SELECT DISTINCT ON (task_id) project_id, task_id, state FROM task_events WHERE project_id = %(project_id)s ORDER BY task_id, created_at DESC - """, {"project_id": project_id}) - - existing_tasks = await cur.fetchall() + """, + {"project_id": project_id}, + ) + existing_tasks = await cur.fetchall() # Get all task_ids from the tasks table - task_ids = await Task.get_all_tasks(db, project_id) + task_ids = await TaskState.get_all_tasks(db, project_id) # Create a set of existing task_ids for quick lookup - existing_task_ids = {task["task_id"] for task in existing_tasks} + existing_task_ids = {task.task_id for task in existing_tasks} # task ids that are not in task_events table remaining_task_ids = [x for x in task_ids if x not in existing_task_ids] @@ -78,6 +70,7 @@ async def all_tasks_states(db: Connection, project_id: uuid.UUID): combined_tasks = existing_tasks + remaining_tasks return combined_tasks + class UserTasksStatsOut(BaseModel): task_id: uuid.UUID task_area: float diff --git a/src/frontend/src/components/IndividualProject/MapSection/index.tsx b/src/frontend/src/components/IndividualProject/MapSection/index.tsx index 238a831d..b3ec69db 100644 --- a/src/frontend/src/components/IndividualProject/MapSection/index.tsx +++ b/src/frontend/src/components/IndividualProject/MapSection/index.tsx @@ -90,7 +90,7 @@ export default function MapSection() { // const bbox = getBbox(tasksCollectiveGeojson as FeatureCollection); // map?.fitBounds(bbox as LngLatBoundsLike, { padding: 25 }); }, [map, tasksData]); - + const getPopupUI = useCallback( (properties: Record) => { const status = taskStatusObj?.[properties?.id]; From 3887f4332ecf4fa9c02217eacc5f9d65bd00e9d8 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Wed, 14 Aug 2024 09:06:54 +0545 Subject: [PATCH 49/66] fix: get task waypoints --- src/backend/app/projects/project_schemas.py | 8 +++++++- src/backend/app/tasks/task_crud.py | 15 +++++++-------- src/backend/app/tasks/task_schemas.py | 3 +-- src/backend/app/waypoints/waypoint_routes.py | 3 +-- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index c924bb08..e0d70ecb 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -146,7 +146,13 @@ class DbProject(BaseModel): tasks: Optional[list[TaskOut]] = [] requires_approval_from_manager_for_locking: Optional[bool] author_id: Optional[str] = None - # TODO add all remaining project fields and validators + front_overlap: Optional[float] = None + side_overlap: Optional[float] = None + gsd_cm_px: Optional[float] = None + altitude_from_ground: Optional[float] = None + is_terrain_follow: bool = False + + @staticmethod async def one(db: Connection, project_id: uuid.UUID): diff --git a/src/backend/app/tasks/task_crud.py b/src/backend/app/tasks/task_crud.py index aaa8e8be..d16125c6 100644 --- a/src/backend/app/tasks/task_crud.py +++ b/src/backend/app/tasks/task_crud.py @@ -7,7 +7,7 @@ async def get_task_geojson(db: Connection, task_id: uuid.UUID): - async with db.cursor(row_factory=dict_row) as cur: + async with db.cursor() as cur: await cur.execute( """ SELECT jsonb_build_object( @@ -23,17 +23,16 @@ async def get_task_geojson(db: Connection, task_id: uuid.UUID): ) ) as geom FROM tasks - WHERE id = :task_id; + WHERE id = %(task_id)s; """, {"task_id": str(task_id)}, ) - data = await db.fetchone() - - if data is None: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Task not found") - - return json.loads(data["geom"]) + data = await cur.fetchone() + if data is None: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Task not found") + return data[0] + # return json.loads(data[0]["geom"]) async def update_task_state( diff --git a/src/backend/app/tasks/task_schemas.py b/src/backend/app/tasks/task_schemas.py index 34b33b9c..3a5315fc 100644 --- a/src/backend/app/tasks/task_schemas.py +++ b/src/backend/app/tasks/task_schemas.py @@ -5,9 +5,8 @@ from psycopg import Connection from loguru import logger as log from fastapi import HTTPException -from psycopg.rows import class_row +from psycopg.rows import class_row, dict_row from typing import Optional -from psycopg.rows import dict_row class NewEvent(BaseModel): diff --git a/src/backend/app/waypoints/waypoint_routes.py b/src/backend/app/waypoints/waypoint_routes.py index e19d53f0..39153f88 100644 --- a/src/backend/app/waypoints/waypoint_routes.py +++ b/src/backend/app/waypoints/waypoint_routes.py @@ -33,8 +33,7 @@ async def get_task_waypoint( ): task_geojson = await get_task_geojson(db, task_id) features = task_geojson["features"][0] - project = await project_deps.get_project_by_id(db, project_id) - + project = await project_deps.get_project_by_id(project_id, db) forward_overlap = project.front_overlap if project.front_overlap else 70 side_overlap = project.side_overlap if project.side_overlap else 70 generate_each_points = False From 6ffebf109b98afee0643e244436c1d00d111c191 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 03:25:59 +0000 Subject: [PATCH 50/66] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/backend/app/projects/project_schemas.py | 2 -- src/backend/app/tasks/task_crud.py | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index e0d70ecb..50b1ac0d 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -151,8 +151,6 @@ class DbProject(BaseModel): gsd_cm_px: Optional[float] = None altitude_from_ground: Optional[float] = None is_terrain_follow: bool = False - - @staticmethod async def one(db: Connection, project_id: uuid.UUID): diff --git a/src/backend/app/tasks/task_crud.py b/src/backend/app/tasks/task_crud.py index d16125c6..d37309be 100644 --- a/src/backend/app/tasks/task_crud.py +++ b/src/backend/app/tasks/task_crud.py @@ -3,7 +3,6 @@ from app.models.enums import HTTPStatus, State from fastapi import HTTPException from psycopg.rows import dict_row -import json async def get_task_geojson(db: Connection, task_id: uuid.UUID): @@ -30,7 +29,9 @@ async def get_task_geojson(db: Connection, task_id: uuid.UUID): data = await cur.fetchone() if data is None: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Task not found") + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Task not found" + ) return data[0] # return json.loads(data[0]["geom"]) From 8481dfe969bb535c1786b71fc5658a4dc9c9eb4f Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Wed, 14 Aug 2024 09:22:53 +0545 Subject: [PATCH 51/66] Refactor: Rename to across modules --- .../drones/{drone_crud.py => drone_logic.py} | 0 .../{project_crud.py => project_logic.py} | 0 src/backend/app/projects/project_routes.py | 10 ++++----- .../app/tasks/{task_crud.py => task_logic.py} | 0 src/backend/app/tasks/task_routes.py | 22 +++++++++---------- src/backend/app/users/user_deps.py | 2 +- .../app/users/{user_crud.py => user_logic.py} | 0 src/backend/app/users/user_routes.py | 8 +++---- src/backend/app/users/user_schemas.py | 4 ++-- .../{waypoint_crud.py => waypoint_logic.py} | 0 src/backend/app/waypoints/waypoint_routes.py | 2 +- 11 files changed, 24 insertions(+), 24 deletions(-) rename src/backend/app/drones/{drone_crud.py => drone_logic.py} (100%) rename src/backend/app/projects/{project_crud.py => project_logic.py} (100%) rename src/backend/app/tasks/{task_crud.py => task_logic.py} (100%) rename src/backend/app/users/{user_crud.py => user_logic.py} (100%) rename src/backend/app/waypoints/{waypoint_crud.py => waypoint_logic.py} (100%) diff --git a/src/backend/app/drones/drone_crud.py b/src/backend/app/drones/drone_logic.py similarity index 100% rename from src/backend/app/drones/drone_crud.py rename to src/backend/app/drones/drone_logic.py diff --git a/src/backend/app/projects/project_crud.py b/src/backend/app/projects/project_logic.py similarity index 100% rename from src/backend/app/projects/project_crud.py rename to src/backend/app/projects/project_logic.py diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index b5d2dec9..2d9a2771 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -9,7 +9,7 @@ from shapely.geometry import shape, mapping from shapely.ops import unary_union -from app.projects import project_schemas, project_crud, project_deps +from app.projects import project_schemas, project_deps, project_logic from app.db import database from app.models.enums import HTTPStatus from app.s3 import s3_client @@ -59,10 +59,10 @@ async def create_project( project_id = await project_schemas.DbProject.create(db, project_info, user_data.id) # Upload DEM to S3 - dem_url = await project_crud.upload_dem_to_s3(project_id, dem) if dem else None + dem_url = await project_logic.upload_dem_to_s3(project_id, dem) if dem else None # Update dem url to database - await project_crud.update_project_dem_url(db, project_id, dem_url) + await project_logic.update_project_dem_url(db, project_id, dem_url) if not project_id: raise HTTPException( @@ -88,7 +88,7 @@ async def upload_project_task_boundaries( dict: JSON containing success message, project ID, and number of tasks. """ log.debug("Creating tasks for each polygon in project") - await project_crud.create_tasks_from_geojson(db, project.id, task_featcol) + await project_logic.create_tasks_from_geojson(db, project.id, task_featcol) return {"message": "Project Boundary Uploaded", "project_id": f"{project.id}"} @@ -128,7 +128,7 @@ async def preview_split_by_square( new_outline = project_shape result_geojson = geojson.Feature(geometry=mapping(new_outline)) - result = await project_crud.preview_split_by_square(result_geojson, dimension) + result = await project_logic.preview_split_by_square(result_geojson, dimension) return result diff --git a/src/backend/app/tasks/task_crud.py b/src/backend/app/tasks/task_logic.py similarity index 100% rename from src/backend/app/tasks/task_crud.py rename to src/backend/app/tasks/task_logic.py diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 12ab4f72..2e6fa975 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from app.config import settings from app.models.enums import EventType, HTTPStatus, State, UserRole -from app.tasks import task_schemas, task_crud +from app.tasks import task_schemas, task_logic from app.users.user_deps import login_required from app.users.user_schemas import AuthUser from app.users import user_schemas @@ -144,7 +144,7 @@ async def new_event( match detail.event: case EventType.REQUESTS: if project["requires_approval_from_manager_for_locking"] is False: - data = await task_crud.request_mapping( + data = await task_logic.request_mapping( db, project_id, task_id, @@ -154,7 +154,7 @@ async def new_event( State.LOCKED_FOR_MAPPING, ) else: - data = await task_crud.request_mapping( + data = await task_logic.request_mapping( db, project_id, task_id, @@ -219,7 +219,7 @@ async def new_event( html_content, ) - return await task_crud.update_task_state( + return await task_logic.update_task_state( db, project_id, task_id, @@ -263,7 +263,7 @@ async def new_event( html_content, ) - return await task_crud.update_task_state( + return await task_logic.update_task_state( db, project_id, task_id, @@ -273,7 +273,7 @@ async def new_event( State.UNLOCKED_TO_MAP, ) case EventType.FINISH: - return await task_crud.update_task_state( + return await task_logic.update_task_state( db, project_id, task_id, @@ -283,7 +283,7 @@ async def new_event( State.UNLOCKED_TO_VALIDATE, ) case EventType.VALIDATE: - return task_crud.update_task_state( + return task_logic.update_task_state( db, project_id, task_id, @@ -293,7 +293,7 @@ async def new_event( State.LOCKED_FOR_VALIDATION, ) case EventType.GOOD: - return await task_crud.update_task_state( + return await task_logic.update_task_state( db, project_id, task_id, @@ -304,7 +304,7 @@ async def new_event( ) case EventType.BAD: - return await task_crud.update_task_state( + return await task_logic.update_task_state( db, project_id, task_id, @@ -314,7 +314,7 @@ async def new_event( State.UNLOCKED_TO_MAP, ) case EventType.COMMENT: - return await task_crud.update_task_state( + return await task_logic.update_task_state( db, project_id, task_id, @@ -349,7 +349,7 @@ async def get_pending_tasks( raise HTTPException( status_code=403, detail="Access forbidden for non-Project Creator users" ) - pending_tasks = await task_crud.get_pending_tasks_for_user(db, user_id) + pending_tasks = await task_logic.get_pending_tasks_for_user(db, user_id) if pending_tasks is None: raise HTTPException(status_code=404, detail="Project not found") return pending_tasks diff --git a/src/backend/app/users/user_deps.py b/src/backend/app/users/user_deps.py index a6ce339e..53bc9b5c 100644 --- a/src/backend/app/users/user_deps.py +++ b/src/backend/app/users/user_deps.py @@ -1,4 +1,4 @@ -from app.users.user_crud import verify_token +from app.users.user_logic import verify_token from fastapi import HTTPException, Request, Header from app.config import settings from app.users.auth import Auth diff --git a/src/backend/app/users/user_crud.py b/src/backend/app/users/user_logic.py similarity index 100% rename from src/backend/app/users/user_crud.py rename to src/backend/app/users/user_logic.py diff --git a/src/backend/app/users/user_routes.py b/src/backend/app/users/user_routes.py index 59c387c4..3ca667a6 100644 --- a/src/backend/app/users/user_routes.py +++ b/src/backend/app/users/user_routes.py @@ -1,7 +1,7 @@ import os from app.users import user_schemas from app.users import user_deps -from app.users import user_crud +from app.users import user_logic from fastapi import APIRouter, Response, HTTPException, Depends, Request from typing import Annotated from fastapi.security import OAuth2PasswordRequestForm @@ -52,7 +52,7 @@ async def login_access_token( "img_url": user.profile_img, } - access_token, refresh_token = await user_crud.create_access_token(user_info) + access_token, refresh_token = await user_logic.create_access_token(user_info) return Token(access_token=access_token, refresh_token=refresh_token) @@ -120,7 +120,7 @@ async def callback(request: Request, google_auth=Depends(init_google_auth)): access_token = google_auth.callback(callback_url).get("access_token") user_data = google_auth.deserialize_access_token(access_token) - access_token, refresh_token = await user_crud.create_access_token(user_data) + access_token, refresh_token = await user_logic.create_access_token(user_data) return Token(access_token=access_token, refresh_token=refresh_token) @@ -129,7 +129,7 @@ async def callback(request: Request, google_auth=Depends(init_google_auth)): async def update_token(user_data: Annotated[AuthUser, Depends(login_required)]): """Refresh access token""" - access_token, refresh_token = await user_crud.create_access_token( + access_token, refresh_token = await user_logic.create_access_token( user_data.model_dump() ) return Token(access_token=access_token, refresh_token=refresh_token) diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index 1dd587b3..622db17c 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -9,7 +9,7 @@ from fastapi import HTTPException from typing import Any from loguru import logger as log -from app.users import user_crud +from app.users import user_logic from psycopg.rows import dict_row @@ -149,7 +149,7 @@ async def update(db: Connection, user_id: int, profile_update: UserProfileIn): await cur.execute( password_update_query, { - "password": user_crud.get_password_hash( + "password": user_logic.get_password_hash( profile_update.password ), "user_id": user_id, diff --git a/src/backend/app/waypoints/waypoint_crud.py b/src/backend/app/waypoints/waypoint_logic.py similarity index 100% rename from src/backend/app/waypoints/waypoint_crud.py rename to src/backend/app/waypoints/waypoint_logic.py diff --git a/src/backend/app/waypoints/waypoint_routes.py b/src/backend/app/waypoints/waypoint_routes.py index 39153f88..e2792fa3 100644 --- a/src/backend/app/waypoints/waypoint_routes.py +++ b/src/backend/app/waypoints/waypoint_routes.py @@ -6,7 +6,7 @@ from app.config import settings from drone_flightplan import flightplan, waypoints from app.models.enums import HTTPStatus -from app.tasks.task_crud import get_task_geojson +from app.tasks.task_logic import get_task_geojson from app.db import database from app.utils import merge_multipolygon from app.s3 import get_file_from_bucket From 09c172cc47bbae73a8a327b614b365c42d0c37bc Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Wed, 14 Aug 2024 15:09:24 +0545 Subject: [PATCH 52/66] fix: no fly zone cliping isuues --- src/backend/app/projects/project_deps.py | 14 ++-- src/backend/app/projects/project_logic.py | 82 ++++++++++++------- src/backend/app/projects/project_routes.py | 25 ++++-- src/backend/app/projects/project_schemas.py | 89 +++++++++++++-------- src/backend/app/utils.py | 67 +++++++++++++++- 5 files changed, 198 insertions(+), 79 deletions(-) diff --git a/src/backend/app/projects/project_deps.py b/src/backend/app/projects/project_deps.py index 2881b0c6..521a9bdc 100644 --- a/src/backend/app/projects/project_deps.py +++ b/src/backend/app/projects/project_deps.py @@ -11,7 +11,7 @@ from app.db import database from app.models.enums import HTTPStatus from app.projects.project_schemas import DbProject -from app.utils import geojson_to_featcol +from app.utils import multipolygon_to_polygon async def get_project_by_id( @@ -47,7 +47,9 @@ async def geojson_upload( bytes_data = await geojson.read() try: - geojson_data = json.loads(bytes_data) # + task_boundaries = json.loads(bytes_data) + geojson_data = multipolygon_to_polygon(task_boundaries) + except json.decoder.JSONDecodeError as e: msg = "Failed to read uploaded GeoJSON file. Is it valid?" log.warning(msg) @@ -55,10 +57,4 @@ async def geojson_upload( status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=msg ) from e - featcol = geojson_to_featcol(geojson_data) - if not isinstance(featcol, FeatureCollection): - msg = "Uploaded GeoJSON could not be parsed to FeatureCollection" - log.warning(msg) - raise HTTPException(status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=msg) - - return featcol + return geojson_data diff --git a/src/backend/app/projects/project_logic.py b/src/backend/app/projects/project_logic.py index 0267f5b9..4652590c 100644 --- a/src/backend/app/projects/project_logic.py +++ b/src/backend/app/projects/project_logic.py @@ -5,9 +5,9 @@ from fmtm_splitter.splitter import split_by_square from fastapi.concurrency import run_in_threadpool from psycopg import Connection -from geojson_pydantic import FeatureCollection -from app.models.enums import HTTPStatus from app.utils import merge_multipolygon +import shapely.wkb as wkblib +from shapely.geometry import shape async def update_project_dem_url(db: Connection, project_id: uuid.UUID, dem_url: str): @@ -28,41 +28,63 @@ async def update_project_dem_url(db: Connection, project_id: uuid.UUID, dem_url: async def create_tasks_from_geojson( db: Connection, project_id: uuid.UUID, - boundaries: FeatureCollection, + boundaries: str, ): """Create tasks for a project, from provided task boundaries.""" - # TODO this should probably drop existing boundaries before creating new? try: - polygons = boundaries["features"] - log.debug(f"Processing {len(polygons)} task geometries") - - # Prepare the data for bulk insert - task_data = [ - (project_id, index + 1, json.dumps(polygon["geometry"])) - for index, polygon in enumerate(polygons) - ] + if isinstance(boundaries, str): + boundaries = json.loads(boundaries) - # Perform bulk insert - async with db.cursor() as cur: - await cur.executemany( - """ - INSERT INTO tasks (id, project_id, project_task_index, outline) - VALUES ( - gen_random_uuid(), - (%s), - (%s), - ST_GeomFromGeoJSON(%s) - ); - """, - task_data, - ) - - log.debug(f"Created database tasks for project ID {project_id}") - return True + # Update the boundary polyon on the database. + if boundaries["type"] == "Feature": + polygons = [boundaries] + else: + polygons = boundaries["features"] + log.debug(f"Processing {len(polygons)} task geometries") + for index, polygon in enumerate(polygons): + try: + # If the polygon is a MultiPolygon, convert it to a Polygon + if polygon["geometry"]["type"] == "MultiPolygon": + log.debug("Converting MultiPolygon to Polygon") + polygon["geometry"]["type"] = "Polygon" + polygon["geometry"]["coordinates"] = polygon["geometry"][ + "coordinates" + ][0] + task_id = str(uuid.uuid4()) + async with db.cursor() as cur: + await cur.execute( + """ + INSERT INTO tasks (id, project_id, outline, project_task_index) + VALUES (%(id)s, %(project_id)s, %(outline)s, %(project_task_index)s) + RETURNING id; + """, + { + "id": task_id, + "project_id": project_id, + "outline": wkblib.dumps( + shape(polygon["geometry"]), hex=True + ), + "project_task_index": index + 1, + }, + ) + result = await cur.fetchone() + if result: + log.debug( + "Created database task | " + f"Project ID {project_id} | " + f"Task index {index}" + ) + log.debug( + "COMPLETE: creating project boundary, based on task boundaries" + ) + # return True + except Exception as e: + log.exception(e) + raise HTTPException(e) from e except Exception as e: log.exception(e) - raise HTTPException(status_code=HTTPStatus.UNPROCESSABLE_ENTITY, detail=str(e)) + raise HTTPException(e) from e async def preview_split_by_square(boundary: str, meters: int): diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index 2d9a2771..e982acf7 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -1,5 +1,6 @@ import os from typing import Annotated +import uuid import geojson from datetime import timedelta from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Form @@ -16,6 +17,7 @@ from app.config import settings from app.users.user_deps import login_required from app.users.user_schemas import AuthUser +from psycopg.rows import dict_row router = APIRouter( prefix=f"{settings.API_PREFIX}/projects", @@ -73,9 +75,10 @@ async def create_project( @router.post("/{project_id}/upload-task-boundaries", tags=["Projects"]) async def upload_project_task_boundaries( - project: Annotated[ - project_schemas.DbProject, Depends(project_deps.get_project_by_id) - ], + # project: Annotated[ + # project_schemas.DbProject, Depends(project_deps.get_project_by_id) + # ], + project_id: uuid.UUID, db: Annotated[Connection, Depends(database.get_db)], user: Annotated[AuthUser, Depends(login_required)], task_featcol: Annotated[FeatureCollection, Depends(project_deps.geojson_upload)], @@ -88,8 +91,19 @@ async def upload_project_task_boundaries( dict: JSON containing success message, project ID, and number of tasks. """ log.debug("Creating tasks for each polygon in project") - await project_logic.create_tasks_from_geojson(db, project.id, task_featcol) - return {"message": "Project Boundary Uploaded", "project_id": f"{project.id}"} + + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute( + """SELECT * FROM projects WHERE id = %(project_id)s LIMIT 1;""", + {"project_id": project_id}, + ) + project = await cur.fetchone() + if not project: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail="Project not found." + ) + await project_logic.create_tasks_from_geojson(db, project["id"], task_featcol) + return {"message": "Project Boundary Uploaded", "project_id": f"{project['id']}"} @router.post("/preview-split-by-square/", tags=["Projects"]) @@ -126,6 +140,7 @@ async def preview_split_by_square( new_outline = project_shape.difference(no_fly_union) else: new_outline = project_shape + result_geojson = geojson.Feature(geometry=mapping(new_outline)) result = await project_logic.preview_split_by_square(result_geojson, dimension) diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index 50b1ac0d..ae5f4c9c 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -1,19 +1,17 @@ import json import uuid -from typing import Annotated, Optional, List +from typing import Annotated, Optional, List, Any from datetime import datetime, date - import geojson from loguru import logger as log from pydantic import BaseModel, computed_field, Field from pydantic.functional_validators import AfterValidator from pydantic.functional_serializers import PlainSerializer -from geojson_pydantic import Feature, FeatureCollection, Polygon, Point, MultiPolygon +from geojson_pydantic import Feature, FeatureCollection, Polygon, Point from fastapi import HTTPException from psycopg import Connection from psycopg.rows import class_row from slugify import slugify - from pydantic import model_validator from app.models.enums import FinalOutput, ProjectVisibility, State from app.models.enums import ( @@ -24,6 +22,7 @@ from app.utils import ( merge_multipolygon, ) +from psycopg.rows import dict_row def validate_geojson( @@ -32,6 +31,8 @@ def validate_geojson( """Convert the upload GeoJSON to standardised FeatureCollection.""" if value: return merge_multipolygon(value.model_dump()) + else: + return None def enum_to_str(value: IntEnum) -> str: @@ -141,7 +142,7 @@ class DbProject(BaseModel): organisation_id: Optional[int] outline: Polygon centroid: Optional[Point] - no_fly_zones: Optional[MultiPolygon] = None + no_fly_zones: Any = Field(exclude=True) task_count: int = 0 tasks: Optional[list[TaskOut]] = [] requires_approval_from_manager_for_locking: Optional[bool] @@ -183,43 +184,62 @@ async def one(db: Connection, project_id: uuid.UUID): {"project_id": project_id}, ) project = await cur.fetchone() - if not project: raise KeyError(f"Project {project_id} not found") return project - @staticmethod - async def all(db: Connection, skip: int = 0, limit: int = 100): - """Get all projects, including tasks and task count.""" - async with db.cursor(row_factory=class_row(DbProject)) as cur: + async def all( + db: Connection, + skip: int = 0, + limit: int = 100, + ): + """Get all projects.""" + async with db.cursor(row_factory=dict_row) as cur: await cur.execute( """ - SELECT - p.*, - ST_AsGeoJSON(p.outline)::jsonb AS outline, - ST_AsGeoJSON(p.centroid)::jsonb AS centroid, - COALESCE(JSON_AGG(t.*) FILTER (WHERE t.id IS NOT NULL), '[]'::json) AS tasks, - COUNT(t.id) AS task_count - FROM - projects p - LEFT JOIN - tasks t ON p.id = t.project_id - GROUP BY - p.id - ORDER BY - created_at DESC - OFFSET %(skip)s - LIMIT %(limit)s; - """, + SELECT id, slug, name, description, per_task_instructions, outline, requires_approval_from_manager_for_locking + FROM projects + ORDER BY created_at DESC + OFFSET %(skip)s + LIMIT %(limit)s + """, {"skip": skip, "limit": limit}, ) - projects = await cur.fetchall() - - if not projects: - raise KeyError("No projects found") - - return projects + db_projects = await cur.fetchall() + return db_projects + + # @staticmethod + # async def all(db: Connection, skip: int = 0, limit: int = 100): + # """Get all projects, including tasks and task count.""" + # async with db.cursor(row_factory=class_row(DbProject)) as cur: + # await cur.execute( + # """ + # SELECT + # p.*, + # ST_AsGeoJSON(p.outline)::jsonb AS outline, + # ST_AsGeoJSON(p.centroid)::jsonb AS centroid, + # COALESCE(JSON_AGG(t.*) FILTER (WHERE t.id IS NOT NULL), '[]'::json) AS tasks, + # COUNT(t.id) AS task_count + # FROM + # projects p + # LEFT JOIN + # tasks t ON p.id = t.project_id + # GROUP BY + # p.id + # ORDER BY + # created_at DESC + # OFFSET %(skip)s + # LIMIT %(limit)s; + # """, + # {"skip": skip, "limit": limit}, + # ) + # projects = await cur.fetchall() + + # if not projects: + # raise KeyError("No projects found") + + # return projects @staticmethod async def create(db: Connection, project: ProjectIn, user_id: str) -> uuid.UUID: @@ -331,7 +351,8 @@ class ProjectOut(BaseModel): name: str description: str per_task_instructions: Optional[str] = None - outline: Polygon + outline: Any = Field(exclude=True) + requires_approval_from_manager_for_locking: bool task_count: int = 0 tasks: Optional[list[TaskOut]] = [] diff --git a/src/backend/app/utils.py b/src/backend/app/utils.py index 52e8fa8b..c71e2cdb 100644 --- a/src/backend/app/utils.py +++ b/src/backend/app/utils.py @@ -179,7 +179,7 @@ def remove_z_dimension(coord): """Remove z dimension from geojson.""" return coord.pop() if len(coord) == 3 else None - features = geojson_to_featcol(features) + features = parse_featcol(features) multi_polygons = [] # handles both collection or single feature @@ -208,6 +208,71 @@ def remove_z_dimension(coord): ) from e +def parse_featcol(features: Union[Feature, FeatCol, MultiPolygon, Polygon]): + """Parse a feature collection or feature into a GeoJSON FeatureCollection. + + Args: + features: Feature, FeatCol, MultiPolygon, Polygon or dict. + + Returns: + dict: Parsed GeoJSON FeatureCollection. + """ + if isinstance(features, dict): + return features + + feat_col = features.model_dump_json() + feat_col = geojson.loads(feat_col) + if isinstance(features, (Polygon, MultiPolygon)): + feat_col = geojson.FeatureCollection([geojson.Feature(geometry=feat_col)]) + elif isinstance(features, Feature): + feat_col = geojson.FeatureCollection([feat_col]) + return feat_col + + +# def merge_multipolygon(features: Union[Feature, FeatCol, MultiPolygon, Polygon]): +# """Merge multiple Polygons or MultiPolygons into a single Polygon. + +# Args: +# features: geojson features to merge. + +# Returns: +# A GeoJSON FeatureCollection containing the merged Polygon. +# """ +# try: + +# def remove_z_dimension(coord): +# """Remove z dimension from geojson.""" +# return coord.pop() if len(coord) == 3 else None + +# features = geojson_to_featcol(features) + +# multi_polygons = [] +# # handles both collection or single feature +# features = features.get("features", [features]) + +# for feature in features: +# list(map(remove_z_dimension, feature["geometry"]["coordinates"][0])) +# polygon = shapely.geometry.shape(feature["geometry"]) +# multi_polygons.append(polygon) + +# merged_polygon = unary_union(multi_polygons) +# if isinstance(merged_polygon, MultiPolygon): +# merged_polygon = merged_polygon.convex_hull + +# merged_geojson = mapping(merged_polygon) +# if merged_geojson["type"] == "MultiPolygon": +# log.error( +# "Resulted GeoJSON contains disjoint Polygons. " +# "Adjacent polygons are preferred." +# ) +# return geojson.FeatureCollection([geojson.Feature(geometry=merged_geojson)]) +# except Exception as e: +# raise HTTPException( +# status_code=400, +# detail=f"Couldn't merge the multipolygon to polygon: {str(e)}", +# ) from e + + def get_address_from_lat_lon(latitude, longitude): """Get address using Nominatim, using lat,lon.""" base_url = "https://nominatim.openstreetmap.org/reverse" From ea75e65021e2f9abc76f5d5cdfa5fabcb507a93f Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Wed, 14 Aug 2024 15:32:41 +0545 Subject: [PATCH 53/66] fix: upload project task boundaries --- src/backend/app/projects/project_logic.py | 2 +- src/backend/app/projects/project_routes.py | 23 ++++------------- src/backend/app/utils.py | 29 +++++++++++----------- 3 files changed, 20 insertions(+), 34 deletions(-) diff --git a/src/backend/app/projects/project_logic.py b/src/backend/app/projects/project_logic.py index 4652590c..335bb54e 100644 --- a/src/backend/app/projects/project_logic.py +++ b/src/backend/app/projects/project_logic.py @@ -78,7 +78,7 @@ async def create_tasks_from_geojson( log.debug( "COMPLETE: creating project boundary, based on task boundaries" ) - # return True + return True except Exception as e: log.exception(e) raise HTTPException(e) from e diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index e982acf7..5909c7b0 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -1,6 +1,5 @@ import os from typing import Annotated -import uuid import geojson from datetime import timedelta from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Form @@ -17,7 +16,6 @@ from app.config import settings from app.users.user_deps import login_required from app.users.user_schemas import AuthUser -from psycopg.rows import dict_row router = APIRouter( prefix=f"{settings.API_PREFIX}/projects", @@ -75,10 +73,9 @@ async def create_project( @router.post("/{project_id}/upload-task-boundaries", tags=["Projects"]) async def upload_project_task_boundaries( - # project: Annotated[ - # project_schemas.DbProject, Depends(project_deps.get_project_by_id) - # ], - project_id: uuid.UUID, + project: Annotated[ + project_schemas.DbProject, Depends(project_deps.get_project_by_id) + ], db: Annotated[Connection, Depends(database.get_db)], user: Annotated[AuthUser, Depends(login_required)], task_featcol: Annotated[FeatureCollection, Depends(project_deps.geojson_upload)], @@ -92,18 +89,8 @@ async def upload_project_task_boundaries( """ log.debug("Creating tasks for each polygon in project") - async with db.cursor(row_factory=dict_row) as cur: - await cur.execute( - """SELECT * FROM projects WHERE id = %(project_id)s LIMIT 1;""", - {"project_id": project_id}, - ) - project = await cur.fetchone() - if not project: - raise HTTPException( - status_code=HTTPStatus.BAD_REQUEST, detail="Project not found." - ) - await project_logic.create_tasks_from_geojson(db, project["id"], task_featcol) - return {"message": "Project Boundary Uploaded", "project_id": f"{project['id']}"} + await project_logic.create_tasks_from_geojson(db, project.id, task_featcol) + return {"message": "Project Boundary Uploaded", "project_id": f"{project.id}"} @router.post("/preview-split-by-square/", tags=["Projects"]) diff --git a/src/backend/app/utils.py b/src/backend/app/utils.py index c71e2cdb..aa3f34f8 100644 --- a/src/backend/app/utils.py +++ b/src/backend/app/utils.py @@ -312,36 +312,35 @@ def get_address_from_lat_lon(latitude, longitude): return address_str -def multipolygon_to_polygon( - featcol: geojson.FeatureCollection, -) -> geojson.FeatureCollection: +def multipolygon_to_polygon(features: Union[Feature, FeatCol, MultiPolygon, Polygon]): """Converts a GeoJSON FeatureCollection of MultiPolygons to Polygons. Args: - featcol : A GeoJSON FeatureCollection containing MultiPolygons/Polygons. + features : A GeoJSON FeatureCollection containing MultiPolygons/Polygons. Returns: geojson.FeatureCollection: A GeoJSON FeatureCollection containing Polygons. """ - final_features = [] + geojson_feature = [] + features = parse_featcol(features) - for feature in featcol.get("features", []): - properties = feature["properties"] - try: - geom = shape(feature["geometry"]) - except ValueError: - log.warning(f"Geometry is not valid, so was skipped: {feature['geometry']}") - continue + # handles both collection or single feature + features = features.get("features", [features]) + for feature in features: + properties = feature["properties"] + geom = shape(feature["geometry"]) if geom.geom_type == "Polygon": - final_features.append(geojson.Feature(geometry=geom, properties=properties)) + geojson_feature.append( + geojson.Feature(geometry=geom, properties=properties) + ) elif geom.geom_type == "MultiPolygon": - final_features.extend( + geojson_feature.extend( geojson.Feature(geometry=polygon_coords, properties=properties) for polygon_coords in geom.geoms ) - return geojson.FeatureCollection(final_features) + return geojson.FeatureCollection(geojson_feature) def normalise_featcol(featcol: geojson.FeatureCollection) -> geojson.FeatureCollection: From 15462e289ba391500643e3b31ce8207e84cb6d7b Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Wed, 14 Aug 2024 15:38:52 +0545 Subject: [PATCH 54/66] remove: remove unnecessary code --- src/backend/app/models/enums.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/backend/app/models/enums.py b/src/backend/app/models/enums.py index 6e29b1d8..2a3d3ddd 100644 --- a/src/backend/app/models/enums.py +++ b/src/backend/app/models/enums.py @@ -143,9 +143,6 @@ class State(int, Enum): UNLOCKED_DONE = 4 UNFLYABLE_TASK = 5 - def __str__(self): - return self.name - class EventType(str, Enum): """Events that can be used via the API to update a state From ceec01ca01e8fc1d12010f80871be2d32b159377 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Wed, 14 Aug 2024 16:02:51 +0545 Subject: [PATCH 55/66] fix: remove pending task api endpoint --- src/backend/app/tasks/task_logic.py | 31 ---------------------------- src/backend/app/tasks/task_routes.py | 30 +-------------------------- 2 files changed, 1 insertion(+), 60 deletions(-) diff --git a/src/backend/app/tasks/task_logic.py b/src/backend/app/tasks/task_logic.py index d37309be..01a745be 100644 --- a/src/backend/app/tasks/task_logic.py +++ b/src/backend/app/tasks/task_logic.py @@ -92,37 +92,6 @@ async def update_task_state( } -async def get_pending_tasks_for_user(db: Connection, user_id: str): - """Get a list of pending tasks created by a specific user (project creator).""" - async with db.cursor(row_factory=dict_row) as cur: - await cur.execute( - """SELECT id FROM projects WHERE author_id = %(user_id)s""", - {"user_id": user_id}, - ) - - project_ids_result = await cur.fetchall() - project_ids = [row["id"] for row in project_ids_result] - await cur.execute( - """ - SELECT t.id AS task_id, te.event_id, te.user_id, te.project_id, te.comment, te.state, te.created_at - FROM tasks t - LEFT JOIN task_events te ON t.id = te.task_id - WHERE t.project_id = ANY(%(project_ids)s) - AND te.state = %(state)s - ORDER BY t.project_task_index;""", - {"project_ids": project_ids, "state": "REQUEST_FOR_MAPPING"}, - ) - - try: - db_tasks = await cur.fetchall() - except Exception as e: - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, - detail=f"Failed to fetch project tasks. {e}", - ) - return db_tasks - - async def request_mapping( db: Connection, project_id: uuid.UUID, diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 2e6fa975..b41f8668 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -3,7 +3,7 @@ from app.projects import project_deps, project_schemas from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from app.config import settings -from app.models.enums import EventType, HTTPStatus, State, UserRole +from app.models.enums import EventType, HTTPStatus, State from app.tasks import task_schemas, task_logic from app.users.user_deps import login_required from app.users.user_schemas import AuthUser @@ -325,31 +325,3 @@ async def new_event( ) return True - - -@router.get("/requested_tasks/pending") -async def get_pending_tasks( - db: Annotated[Connection, Depends(database.get_db)], - user_data: Annotated[AuthUser, Depends(login_required)], -): - """Get a list of pending tasks for a project creator.""" - user_id = user_data.id - - async with db.cursor(row_factory=dict_row) as cur: - await cur.execute( - """SELECT role FROM user_profile WHERE user_id = %(user_id)s""", - {"user_id": user_id}, - ) - records = await cur.fetchall() - if not records: - raise HTTPException(status_code=404, detail="User profile not found") - - roles = [record["role"] for record in records] - if UserRole.PROJECT_CREATOR.name not in roles: - raise HTTPException( - status_code=403, detail="Access forbidden for non-Project Creator users" - ) - pending_tasks = await task_logic.get_pending_tasks_for_user(db, user_id) - if pending_tasks is None: - raise HTTPException(status_code=404, detail="Project not found") - return pending_tasks From 0774cd9bc309e8d9f76f74a965d9ac3430d148a1 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Thu, 15 Aug 2024 11:54:05 +0545 Subject: [PATCH 56/66] fix: task statistics endpoint --- src/backend/app/tasks/task_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index b41f8668..e5dd90f2 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -74,7 +74,7 @@ async def get_task_stats( user_id = user_data.id try: - async with db.cursor() as cur: + async with db.cursor(row_factory=dict_row) as cur: # Check if the user profile exists await cur.execute( """SELECT role FROM user_profile WHERE user_id = %(user_id)s""", From c59b044add1fc2e304bfd5b43142ff68ac1840ac Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 20 Aug 2024 13:34:10 +0545 Subject: [PATCH 57/66] fix: issues slove in list tasks --- src/backend/app/projects/project_schemas.py | 3 +- src/backend/app/tasks/task_routes.py | 114 +++++++++++--------- src/backend/app/tasks/task_schemas.py | 47 ++++---- 3 files changed, 92 insertions(+), 72 deletions(-) diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index 86b1dada..83a1338b 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -13,7 +13,7 @@ from psycopg.rows import class_row from slugify import slugify from pydantic import model_validator -from app.models.enums import FinalOutput, ProjectVisibility, State +from app.models.enums import FinalOutput, ProjectVisibility from app.models.enums import ( IntEnum, ProjectStatus, @@ -131,6 +131,7 @@ class TaskOut(BaseModel): task_area: Optional[float] = None name: Optional[str] = None + class DbProject(BaseModel): """Project model for extracting from database.""" diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 206cd128..2fc23936 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -63,46 +63,54 @@ async def get_task_stats( ): "Retrieve statistics related to tasks for the authenticated user." user_id = user_data.id - query = """SELECT role FROM user_profile WHERE user_id = :user_id""" - records = await db.fetch_all(query, {"user_id": user_id}) - - if not records: - raise HTTPException(status_code=404, detail="User profile not found") - - roles = [record["role"] for record in records] - if UserRole.PROJECT_CREATOR.name in roles: - role = "PROJECT_CREATOR" - else: - role = "DRONE_PILOT" - - raw_sql = """ - SELECT - COUNT(CASE WHEN te.state = 'REQUEST_FOR_MAPPING' THEN 1 END) AS request_logs, - COUNT(CASE WHEN te.state = 'LOCKED_FOR_MAPPING' THEN 1 END) AS ongoing_tasks, - COUNT(CASE WHEN te.state = 'UNLOCKED_DONE' THEN 1 END) AS completed_tasks, - COUNT(CASE WHEN te.state = 'UNFLYABLE_TASK' THEN 1 END) AS unflyable_tasks - FROM ( - SELECT DISTINCT ON (te.task_id) - te.task_id, - te.state, - te.created_at - FROM task_events te - WHERE - (%(role)s = 'DRONE_PILOT' AND te.user_id = %(user_id)s) - OR - (%(role)s != 'DRONE_PILOT' AND te.task_id IN ( - SELECT t.id - FROM tasks t - WHERE t.project_id IN (SELECT id FROM projects WHERE author_id = %(user_id)s) - )) - ORDER BY te.task_id, te.created_at DESC - ) AS te; - """ try: - db_counts = await db.fetch_one( - query=raw_sql, values={"user_id": user_id, "role": role} - ) + async with db.cursor(row_factory=dict_row) as cur: + # Check if the user profile exists + await cur.execute( + """SELECT role FROM user_profile WHERE user_id = %(user_id)s""", + {"user_id": user_id}, + ) + records = await cur.fetchall() + + if not records: + raise HTTPException(status_code=404, detail="User profile not found") + roles = [record["role"] for record in records] + + if UserRole.PROJECT_CREATOR.name in roles: + role = "PROJECT_CREATOR" + else: + role = "DRONE_PILOT" + + # Query for task statistics + raw_sql = """ + SELECT + COUNT(CASE WHEN te.state = 'REQUEST_FOR_MAPPING' THEN 1 END) AS request_logs, + COUNT(CASE WHEN te.state = 'LOCKED_FOR_MAPPING' THEN 1 END) AS ongoing_tasks, + COUNT(CASE WHEN te.state = 'UNLOCKED_DONE' THEN 1 END) AS completed_tasks, + COUNT(CASE WHEN te.state = 'UNFLYABLE_TASK' THEN 1 END) AS unflyable_tasks + FROM ( + SELECT DISTINCT ON (te.task_id) + te.task_id, + te.state, + te.created_at + FROM task_events te + WHERE + (%(role)s = 'DRONE_PILOT' AND te.user_id = %(user_id)s) + OR + (%(role)s != 'DRONE_PILOT' AND te.task_id IN ( + SELECT t.id + FROM tasks t + WHERE t.project_id IN (SELECT id FROM projects WHERE author_id = %(user_id)s) + )) + ORDER BY te.task_id, te.created_at DESC + ) AS te; + """ + await cur.execute(raw_sql, {"user_id": user_id, "role": role}) + db_counts = await cur.fetchone() + + return db_counts + except Exception as e: raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, @@ -118,21 +126,29 @@ async def list_tasks( limit: int = 50, ): """Get all tasks for a all user.""" - user_id = user_data.id - query = """SELECT role FROM user_profile WHERE user_id = %(user_id)s""" - records = await db.fetch_all(query, {"user_id": user_id}) - roles = [record["role"] for record in records] - if UserRole.PROJECT_CREATOR.name in roles: - role = "PROJECT_CREATOR" - else: - role = "DRONE_PILOT" + async with db.cursor(row_factory=dict_row) as cur: + # Check if the user profile exists + await cur.execute( + """SELECT role FROM user_profile WHERE user_id = %(user_id)s""", + {"user_id": user_id}, + ) + records = await cur.fetchall() + + if not records: + raise HTTPException(status_code=404, detail="User profile not found") + + roles = [record["role"] for record in records] - if not records: - raise HTTPException(status_code=404, detail="User profile not found") + if UserRole.PROJECT_CREATOR.name in roles: + role = "PROJECT_CREATOR" + else: + role = "DRONE_PILOT" - return await task_schemas.UserTasksStatsOut.get_tasks_by_user(user_id, db, role, skip, limit) + return await task_schemas.UserTasksStatsOut.get_tasks_by_user( + db, user_id, role, skip, limit + ) @router.get("/states/{project_id}") diff --git a/src/backend/app/tasks/task_schemas.py b/src/backend/app/tasks/task_schemas.py index 3a5315fc..1e1be159 100644 --- a/src/backend/app/tasks/task_schemas.py +++ b/src/backend/app/tasks/task_schemas.py @@ -78,37 +78,40 @@ class UserTasksStatsOut(BaseModel): project_id: uuid.UUID @staticmethod - async def get_tasks_by_user(db: Connection, user_id: str): + async def get_tasks_by_user( + db: Connection, user_id: str, role: str, skip: int = 0, limit: int = 50 + ): async with db.cursor(row_factory=class_row(UserTasksStatsOut)) as cur: await cur.execute( - """WITH task_details AS ( - SELECT + """SELECT DISTINCT ON (tasks.id) tasks.id AS task_id, task_events.project_id AS project_id, - ST_Area(ST_Transform(tasks.outline, 4326)) / 1000000 AS task_area, + ST_Area(ST_Transform(tasks.outline, 3857)) / 1000000 AS task_area, task_events.created_at, - task_events.state + CASE + WHEN task_events.state = 'REQUEST_FOR_MAPPING' THEN 'request logs' + WHEN task_events.state = 'LOCKED_FOR_MAPPING' THEN 'ongoing' + WHEN task_events.state = 'UNLOCKED_DONE' THEN 'completed' + WHEN task_events.state = 'UNFLYABLE_TASK' THEN 'unflyable task' + ELSE 'UNLOCKED_TO_MAP' + END AS state FROM task_events - JOIN + LEFT JOIN tasks ON task_events.task_id = tasks.id WHERE - task_events.user_id = %(user_id)s - ) - SELECT - task_details.task_id, - task_details.project_id, - task_details.task_area, - task_details.created_at, - CASE - WHEN task_details.state = 'REQUEST_FOR_MAPPING' THEN 'request logs' - WHEN task_details.state = 'LOCKED_FOR_MAPPING' THEN 'ongoing' - WHEN task_details.state = 'UNLOCKED_DONE' THEN 'completed' - WHEN task_details.state = 'UNFLYABLE_TASK' THEN 'unflyable task' - ELSE 'UNLOCKED_TO_MAP' -- Default case if the state does not match any expected values - END AS state - FROM task_details;""", - {"user_id": user_id}, + ( + %(role)s = 'DRONE_PILOT' AND task_events.user_id = %(user_id)s + ) + OR + ( + %(role)s!= 'DRONE_PILOT' AND task_events.project_id IN (SELECT id FROM projects WHERE author_id = %(user_id)s) + ) + ORDER BY + tasks.id, task_events.created_at DESC + OFFSET %(skip)s + LIMIT %(limit)s;""", + {"user_id": user_id, "role": role, "skip": skip, "limit": limit}, ) try: return await cur.fetchall() From 4037b3a285f81d8c4f5ff5edef7af18fe6141820 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 20 Aug 2024 14:26:44 +0545 Subject: [PATCH 58/66] fix: issues slove in get project details --- src/backend/app/projects/project_schemas.py | 88 +++++++++++++-------- 1 file changed, 57 insertions(+), 31 deletions(-) diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index 83a1338b..f2b93065 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -125,7 +125,7 @@ class TaskOut(BaseModel): id: uuid.UUID project_task_index: int - outline: Any = Field(exclude=True) + outline: Polygon state: Optional[str] = None user_id: Optional[str] = None task_area: Optional[float] = None @@ -138,16 +138,16 @@ class DbProject(BaseModel): id: uuid.UUID name: str slug: Optional[str] = None - short_description: Optional[str] - description: str + short_description: Optional[str] = None + description: str = None per_task_instructions: Optional[str] = None - organisation_id: Optional[int] + organisation_id: Optional[int] = None outline: Polygon centroid: Optional[Point] no_fly_zones: Any = Field(exclude=True) task_count: int = 0 tasks: Optional[list[TaskOut]] = [] - requires_approval_from_manager_for_locking: Optional[bool] + requires_approval_from_manager_for_locking: Optional[bool] = None author_id: Optional[str] = None front_overlap: Optional[float] = None side_overlap: Optional[float] = None @@ -155,41 +155,67 @@ class DbProject(BaseModel): altitude_from_ground: Optional[float] = None is_terrain_follow: bool = False - @staticmethod async def one(db: Connection, project_id: uuid.UUID): - """Get a single project by it's ID, including tasks and task count.""" + """Get a single project & all associated tasks by ID.""" async with db.cursor(row_factory=class_row(DbProject)) as cur: - # NOTE to wrap Polygon geometry in Feature - # jsonb_build_object( - # 'type', 'Feature', - # 'geometry', ST_AsGeoJSON(p.outline)::jsonb, - # 'id', p.id::varchar, - # 'properties', jsonb_build_object() - # ) AS outline, await cur.execute( """ SELECT - p.*, - ST_AsGeoJSON(p.outline)::jsonb AS outline, - ST_AsGeoJSON(p.centroid)::jsonb AS centroid, - COALESCE(JSON_AGG(t.*) FILTER (WHERE t.id IS NOT NULL), '[]'::json) AS tasks, - COUNT(t.id) AS task_count - FROM - projects p - LEFT JOIN - tasks t ON p.id = t.project_id - WHERE - p.id = %(project_id)s - GROUP BY - p.id; + projects.*, + ST_AsGeoJSON(projects.outline)::jsonb AS outline, + ST_AsGeoJSON(projects.centroid)::jsonb AS centroid + + FROM projects + WHERE projects.id = %(project_id)s + LIMIT 1; """, {"project_id": project_id}, ) - project = await cur.fetchone() - if not project: - raise KeyError(f"Project {project_id} not found") - return project + project_record = await cur.fetchone() + if not project_record: + return None + + async with db.cursor(row_factory=class_row(TaskOut)) as cur: + await cur.execute( + """WITH TaskStateCalculation AS ( + SELECT DISTINCT ON (te.task_id) + te.task_id, + te.user_id, + CASE + WHEN te.state = 'REQUEST_FOR_MAPPING' THEN 'request logs' + WHEN te.state = 'LOCKED_FOR_MAPPING' THEN 'ongoing' + WHEN te.state = 'UNLOCKED_DONE' THEN 'completed' + WHEN te.state = 'UNFLYABLE_TASK' THEN 'unflyable task' + ELSE 'UNLOCKED_TO_MAP' + END AS calculated_state + FROM + task_events te + ORDER BY + te.task_id, te.created_at DESC + ) + SELECT + t.id, + t.project_task_index, + ST_AsGeoJSON(t.outline)::jsonb AS outline, + tsc.user_id, + u.name, + ST_Area(ST_Transform(t.outline, 3857)) / 1000000 AS task_area, + COALESCE(tsc.calculated_state, 'UNLOCKED_TO_MAP') AS state + FROM + tasks t + LEFT JOIN + TaskStateCalculation tsc ON t.id = tsc.task_id + LEFT JOIN + users u ON tsc.user_id = u.id + WHERE + t.project_id = %(project_id)s;""", + {"project_id": project_id}, + ) + task_records = await cur.fetchall() + project_record.tasks = task_records if task_records is not None else [] + project_record.task_count = len(task_records) + return project_record async def all( db: Connection, From cb1ced4dcdd1be4454c505fb9cabbcf5ed7121f7 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 20 Aug 2024 14:29:51 +0545 Subject: [PATCH 59/66] fix: refine the project get all --- src/backend/app/projects/project_schemas.py | 32 --------------------- 1 file changed, 32 deletions(-) diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index f2b93065..39917d6a 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -237,38 +237,6 @@ async def all( db_projects = await cur.fetchall() return db_projects - # @staticmethod - # async def all(db: Connection, skip: int = 0, limit: int = 100): - # """Get all projects, including tasks and task count.""" - # async with db.cursor(row_factory=class_row(DbProject)) as cur: - # await cur.execute( - # """ - # SELECT - # p.*, - # ST_AsGeoJSON(p.outline)::jsonb AS outline, - # ST_AsGeoJSON(p.centroid)::jsonb AS centroid, - # COALESCE(JSON_AGG(t.*) FILTER (WHERE t.id IS NOT NULL), '[]'::json) AS tasks, - # COUNT(t.id) AS task_count - # FROM - # projects p - # LEFT JOIN - # tasks t ON p.id = t.project_id - # GROUP BY - # p.id - # ORDER BY - # created_at DESC - # OFFSET %(skip)s - # LIMIT %(limit)s; - # """, - # {"skip": skip, "limit": limit}, - # ) - # projects = await cur.fetchall() - - # if not projects: - # raise KeyError("No projects found") - - # return projects - @staticmethod async def create(db: Connection, project: ProjectIn, user_id: str) -> uuid.UUID: """Create a single project.""" From 1f4b3ae427d1add79d8ff6ef9a61fe0a67364a22 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 20 Aug 2024 14:32:31 +0545 Subject: [PATCH 60/66] fix: remove xxx_crud.py from all module --- src/backend/app/projects/project_crud.py | 283 ----------------------- src/backend/app/tasks/task_crud.py | 246 -------------------- src/backend/app/tasks/task_logic.py | 6 +- 3 files changed, 3 insertions(+), 532 deletions(-) delete mode 100644 src/backend/app/projects/project_crud.py delete mode 100644 src/backend/app/tasks/task_crud.py diff --git a/src/backend/app/projects/project_crud.py b/src/backend/app/projects/project_crud.py deleted file mode 100644 index ae00f1fc..00000000 --- a/src/backend/app/projects/project_crud.py +++ /dev/null @@ -1,283 +0,0 @@ -import json -import uuid -from app.projects import project_schemas -from loguru import logger as log -import shapely.wkb as wkblib -from shapely.geometry import shape -from fastapi import HTTPException, UploadFile -from app.utils import merge_multipolygon -from fmtm_splitter.splitter import split_by_square -from fastapi.concurrency import run_in_threadpool -from databases import Database -from app.models.enums import ProjectStatus -from app.utils import generate_slug -from io import BytesIO -from app.s3 import add_obj_to_bucket -from app.config import settings - - -async def update_project_dem_url(db: Database, project_id: uuid.UUID, dem_url: str): - """Update the DEM URL for a project.""" - query = """ - UPDATE projects - SET dem_url = :dem_url - WHERE id = :project_id - """ - await db.execute(query, {"dem_url": dem_url, "project_id": project_id}) - return True - - -async def upload_dem_to_s3(project_id: uuid.UUID, dem_file: UploadFile) -> str: - """Upload dem into S3. - - Args: - project_id (int): The organisation id in the database. - dem_file (UploadFile): The logo image uploaded to FastAPI. - - Returns: - dem_url(str): The S3 URL for the dem file. - """ - dem_path = f"/dem/{project_id}/dem.tif" - - file_bytes = await dem_file.read() - file_obj = BytesIO(file_bytes) - - add_obj_to_bucket( - settings.S3_BUCKET_NAME, - file_obj, - dem_path, - content_type=dem_file.content_type, - ) - - dem_url = f"{settings.S3_DOWNLOAD_ROOT}/{settings.S3_BUCKET_NAME}{dem_path}" - - return dem_url - - -async def create_project_with_project_info( - db: Database, author_id: uuid.UUID, project_metadata: project_schemas.ProjectIn -): - """Create a project in database.""" - _id = uuid.uuid4() - query = """ - INSERT INTO projects ( - id, slug, author_id, name, description, per_task_instructions, status, visibility, outline, no_fly_zones, - gsd_cm_px, front_overlap, side_overlap, final_output ,altitude_from_ground,is_terrain_follow, task_split_dimension, deadline_at, - requires_approval_from_manager_for_locking, created_at) - VALUES ( - :id, - :slug, - :author_id, - :name, - :description, - :per_task_instructions, - :status, - :visibility, - :outline, - :no_fly_zones, - :gsd_cm_px, - :front_overlap, - :side_overlap, - :final_output, - :altitude_from_ground, - :is_terrain_follow, - :task_split_dimension, - :deadline_at, - :requires_approval_from_manager_for_locking, - CURRENT_TIMESTAMP - ) - RETURNING id - """ - try: - project_id = await db.execute( - query, - values={ - "id": _id, - "slug": generate_slug(project_metadata.name), - "author_id": author_id, - "name": project_metadata.name, - "description": project_metadata.description, - "per_task_instructions": project_metadata.per_task_instructions, - "status": ProjectStatus.DRAFT.name, - "visibility": project_metadata.visibility.name, - "outline": str(project_metadata.outline), - "gsd_cm_px": project_metadata.gsd_cm_px, - "altitude_from_ground": project_metadata.altitude_from_ground, - "is_terrain_follow": project_metadata.is_terrain_follow, - "no_fly_zones": str(project_metadata.no_fly_zones) - if project_metadata.no_fly_zones is not None - else None, - "task_split_dimension": project_metadata.task_split_dimension, - "deadline_at": project_metadata.deadline_at, - "final_output": [item.value for item in project_metadata.final_output], - "requires_approval_from_manager_for_locking": project_metadata.requires_approval_from_manager_for_locking, - "front_overlap": project_metadata.front_overlap, - "side_overlap": project_metadata.side_overlap, - }, - ) - return project_id - - except Exception as e: - log.exception(e) - raise HTTPException(e) from e - - -async def get_project_by_id(db: Database, project_id: uuid.UUID): - "Get a single database project object by project_id" - - query = """ select * from projects where id=:project_id""" - result = await db.fetch_one(query, {"project_id": project_id}) - return result - - -async def get_project_info_by_id(db: Database, project_id: uuid.UUID): - """Get a single project & all associated tasks by ID.""" - query = """ - SELECT - projects.id, - projects.slug, - projects.name, - projects.description, - projects.per_task_instructions, - projects.outline, - projects.requires_approval_from_manager_for_locking - FROM projects - WHERE projects.id = :project_id - LIMIT 1; - """ - - project_record = await db.fetch_one(query, {"project_id": project_id}) - if not project_record: - return None - query = """ - WITH TaskStateCalculation AS ( - SELECT DISTINCT ON (te.task_id) - te.task_id, - te.user_id, - CASE - WHEN te.state = 'REQUEST_FOR_MAPPING' THEN 'request logs' - WHEN te.state = 'LOCKED_FOR_MAPPING' THEN 'ongoing' - WHEN te.state = 'UNLOCKED_DONE' THEN 'completed' - WHEN te.state = 'UNFLYABLE_TASK' THEN 'unflyable task' - ELSE 'UNLOCKED_TO_MAP' - END AS calculated_state - FROM - task_events te - ORDER BY - te.task_id, te.created_at DESC - ) - SELECT - t.id, - t.project_task_index, - t.outline, - tsc.user_id, - u.name, - ST_Area(ST_Transform(t.outline, 3857)) / 1000000 AS task_area, - COALESCE(tsc.calculated_state, 'UNLOCKED_TO_MAP') AS state - FROM - tasks t - LEFT JOIN - TaskStateCalculation tsc ON t.id = tsc.task_id - LEFT JOIN - users u ON tsc.user_id = u.id - WHERE - t.project_id = :project_id; - """ - - task_records = await db.fetch_all(query, {"project_id": project_id}) - project_record.tasks = task_records if task_records is not None else [] - project_record.task_count = len(task_records) - return project_record - - -async def get_projects( - db: Database, - skip: int = 0, - limit: int = 100, -): - """Get all projects.""" - raw_sql = """ - SELECT id, slug, name, description, per_task_instructions, outline, requires_approval_from_manager_for_locking - FROM projects - ORDER BY created_at DESC - OFFSET :skip - LIMIT :limit; - """ - db_projects = await db.fetch_all(raw_sql, {"skip": skip, "limit": limit}) - - return db_projects - - -async def create_tasks_from_geojson( - db: Database, - project_id: uuid.UUID, - boundaries: str, -): - """Create tasks for a project, from provided task boundaries.""" - try: - if isinstance(boundaries, str): - boundaries = json.loads(boundaries) - - # Update the boundary polyon on the database. - if boundaries["type"] == "Feature": - polygons = [boundaries] - else: - polygons = boundaries["features"] - log.debug(f"Processing {len(polygons)} task geometries") - for index, polygon in enumerate(polygons): - try: - # If the polygon is a MultiPolygon, convert it to a Polygon - if polygon["geometry"]["type"] == "MultiPolygon": - log.debug("Converting MultiPolygon to Polygon") - polygon["geometry"]["type"] = "Polygon" - polygon["geometry"]["coordinates"] = polygon["geometry"][ - "coordinates" - ][0] - - task_id = str(uuid.uuid4()) - query = """ - INSERT INTO tasks (id, project_id, outline, project_task_index) - VALUES (:id, :project_id, :outline, :project_task_index);""" - - result = await db.execute( - query, - values={ - "id": task_id, - "project_id": project_id, - "outline": wkblib.dumps(shape(polygon["geometry"]), hex=True), - "project_task_index": index + 1, - }, - ) - - if result: - log.debug( - "Created database task | " - f"Project ID {project_id} | " - f"Task index {index}" - ) - log.debug( - "COMPLETE: creating project boundary, based on task boundaries" - ) - return True - except Exception as e: - log.exception(e) - raise HTTPException(e) from e - except Exception as e: - log.exception(e) - raise HTTPException(e) from e - - -async def preview_split_by_square(boundary: str, meters: int): - """Preview split by square for a project boundary. - - Use a lambda function to remove the "z" dimension from each - coordinate in the feature's geometry. - """ - boundary = merge_multipolygon(boundary) - - return await run_in_threadpool( - lambda: split_by_square( - boundary, - meters=meters, - ) - ) diff --git a/src/backend/app/tasks/task_crud.py b/src/backend/app/tasks/task_crud.py deleted file mode 100644 index e4d60b27..00000000 --- a/src/backend/app/tasks/task_crud.py +++ /dev/null @@ -1,246 +0,0 @@ -import uuid -import json -from databases import Database -from app.models.enums import HTTPStatus, State -from fastapi import HTTPException -from loguru import logger as log - - -async def get_task_geojson(db: Database, task_id: uuid.UUID): - query = """ - SELECT jsonb_build_object( - 'type', 'FeatureCollection', - 'features', jsonb_agg( - jsonb_build_object( - 'type', 'Feature', - 'geometry', ST_AsGeoJSON(outline)::jsonb, - 'properties', jsonb_build_object( - 'id', id - ) - ) - ) - ) as geom - FROM tasks - WHERE id = :task_id; - """ - - values = {"task_id": str(task_id)} - - data = await db.fetch_one(query, values) - - if data is None: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Task not found") - - return json.loads(data["geom"]) - - -async def get_tasks_by_user( - user_id: str, db: Database, role: str, skip: int = 0, limit: int = 50 -): - try: - query = """SELECT DISTINCT ON (tasks.id) - tasks.id AS task_id, - task_events.project_id AS project_id, - ST_Area(ST_Transform(tasks.outline, 3857)) / 1000000 AS task_area, - task_events.created_at, - CASE - WHEN task_events.state = 'REQUEST_FOR_MAPPING' THEN 'request logs' - WHEN task_events.state = 'LOCKED_FOR_MAPPING' THEN 'ongoing' - WHEN task_events.state = 'UNLOCKED_DONE' THEN 'completed' - WHEN task_events.state = 'UNFLYABLE_TASK' THEN 'unflyable task' - ELSE 'UNLOCKED_TO_MAP' - END AS state - FROM - task_events - LEFT JOIN - tasks ON task_events.task_id = tasks.id - WHERE - ( - :role = 'DRONE_PILOT' AND task_events.user_id = :user_id - ) - OR - ( - :role != 'DRONE_PILOT' AND task_events.project_id IN (SELECT id FROM projects WHERE author_id = :user_id) - ) - ORDER BY - tasks.id, task_events.created_at DESC - OFFSET :skip - LIMIT :limit; - """ - records = await db.fetch_all( - query, - values={"user_id": user_id, "role": role, "skip": skip, "limit": limit}, - ) - return records - - except Exception as e: - log.exception(e) - raise HTTPException( - status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Retrieval failed" - ) from e - - -async def get_all_tasks(db: Database, project_id: uuid.UUID): - query = """ - SELECT id FROM tasks WHERE project_id = :project_id - """ - values = {"project_id": str(project_id)} - - data = await db.fetch_all(query, values) - - # Extracting the list of IDs from the data - task_ids = [task["id"] for task in data] - - return task_ids - - -async def all_tasks_states(db: Database, project_id: uuid.UUID): - query = """ - SELECT DISTINCT ON (task_id) project_id, task_id, state - FROM task_events - WHERE project_id = :project_id - ORDER BY task_id, created_at DESC - """ - - r = await db.fetch_all(query, {"project_id": str(project_id)}) - - # Extract task_ids and corresponding states from the query result - existing_tasks = [dict(r) for r in r] - - # Get all task_ids from the tasks table - task_ids = await get_all_tasks(db, project_id) - - # Create a set of existing task_ids for quick lookup - existing_task_ids = {task["task_id"] for task in existing_tasks} - - # task ids that are not in task_events table - remaining_task_ids = [x for x in task_ids if x not in existing_task_ids] - - # Add missing tasks with state as "UNLOCKED_FOR_MAPPING" - remaining_tasks = [ - { - "project_id": str(project_id), - "task_id": task_id, - "state": State.UNLOCKED_TO_MAP.name, - } - for task_id in remaining_task_ids - ] - - # Combine both existing tasks and remaining tasks - combined_tasks = existing_tasks + remaining_tasks - - return combined_tasks - - -async def request_mapping( - db: Database, - project_id: uuid.UUID, - task_id: uuid.UUID, - user_id: str, - comment: str, - initial_state: State, - final_state: State, -): - query = """ - WITH last AS ( - SELECT * - FROM task_events - WHERE project_id= :project_id AND task_id= :task_id - ORDER BY created_at DESC - LIMIT 1 - ), - released AS ( - SELECT COUNT(*) = 0 AS no_record - FROM task_events - WHERE project_id= :project_id AND task_id= :task_id AND state = :unlocked_to_map_state - ) - INSERT INTO task_events (event_id, project_id, task_id, user_id, comment, state, created_at) - - SELECT - gen_random_uuid(), - :project_id, - :task_id, - :user_id, - :comment, - :request_for_map_state, - now() - FROM last - RIGHT JOIN released ON true - WHERE (last.state = :unlocked_to_map_state OR released.no_record = true); - """ - - values = { - "project_id": str(project_id), - "task_id": str(task_id), - "user_id": str(user_id), - "comment": comment, - "unlocked_to_map_state": initial_state.name, # State.UNLOCKED_TO_MAP.name, - "request_for_map_state": final_state.name, # State.REQUEST_FOR_MAPPING.name, - } - - await db.fetch_one(query, values) - - return {"project_id": project_id, "task_id": task_id, "comment": comment} - - -async def update_task_state( - db: Database, - project_id: uuid.UUID, - task_id: uuid.UUID, - user_id: str, - comment: str, - initial_state: State, - final_state: State, -): - query = """ - WITH last AS ( - SELECT * - FROM task_events - WHERE project_id = :project_id AND task_id = :task_id - ORDER BY created_at DESC - LIMIT 1 - ), - locked AS ( - SELECT * - FROM last - WHERE user_id = :user_id AND state = :initial_state - ) - INSERT INTO task_events(event_id, project_id, task_id, user_id, state, comment, created_at) - SELECT gen_random_uuid(), project_id, task_id, user_id, :final_state, :comment, now() - FROM last - WHERE user_id = :user_id - RETURNING project_id, task_id, user_id, state; - """ - values = { - "project_id": str(project_id), - "task_id": str(task_id), - "user_id": str(user_id), - "comment": comment, - "initial_state": initial_state.name, - "final_state": final_state.name, - } - - await db.fetch_one(query, values) - return {"project_id": project_id, "task_id": task_id, "comment": comment} - - -async def get_requested_user_id( - db: Database, project_id: uuid.UUID, task_id: uuid.UUID -): - query = """ - SELECT user_id - FROM task_events - WHERE project_id = :project_id AND task_id = :task_id and state = :request_for_map_state - ORDER BY created_at DESC - LIMIT 1 - """ - values = { - "project_id": str(project_id), - "task_id": str(task_id), - "request_for_map_state": State.REQUEST_FOR_MAPPING.name, - } - - result = await db.fetch_one(query, values) - if result is None: - raise ValueError("No user requested for mapping") - return result["user_id"] diff --git a/src/backend/app/tasks/task_logic.py b/src/backend/app/tasks/task_logic.py index e8318909..e31d1bad 100644 --- a/src/backend/app/tasks/task_logic.py +++ b/src/backend/app/tasks/task_logic.py @@ -133,7 +133,7 @@ async def request_mapping( "request_for_map_state": final_state.name, # State.REQUEST_FOR_MAPPING.name, }, ) - # result = await cur.fetchone() - # return result + result = await cur.fetchone() + return result - return {"project_id": project_id, "task_id": task_id, "comment": comment} + # return {"project_id": project_id, "task_id": task_id, "comment": comment} From 1967805c38c5aa9d8fff342a5cf2cf36653c32a0 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 20 Aug 2024 16:41:06 +0545 Subject: [PATCH 61/66] fix: issues slove in get project endpoint --- src/backend/app/projects/project_schemas.py | 132 ++++++++++++------ .../src/views/IndividualProject/index.tsx | 8 +- 2 files changed, 93 insertions(+), 47 deletions(-) diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index 39917d6a..be9ae766 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -125,7 +125,7 @@ class TaskOut(BaseModel): id: uuid.UUID project_task_index: int - outline: Polygon + outline: Optional[Polygon | Feature | FeatureCollection] state: Optional[str] = None user_id: Optional[str] = None task_area: Optional[float] = None @@ -142,8 +142,8 @@ class DbProject(BaseModel): description: str = None per_task_instructions: Optional[str] = None organisation_id: Optional[int] = None - outline: Polygon - centroid: Optional[Point] + outline: Optional[Polygon | Feature | FeatureCollection] + centroid: Optional[Point | Feature | Polygon] = None no_fly_zones: Any = Field(exclude=True) task_count: int = 0 tasks: Optional[list[TaskOut]] = [] @@ -162,56 +162,102 @@ async def one(db: Connection, project_id: uuid.UUID): """ SELECT projects.*, - ST_AsGeoJSON(projects.outline)::jsonb AS outline, + jsonb_build_object( + 'type', 'Feature', + 'geometry', jsonb_build_object( + 'type', 'Polygon', + 'coordinates', (ST_AsGeoJSON(projects.outline)::jsonb -> 'coordinates')::jsonb + ), + 'properties', jsonb_build_object( + 'id', projects.id, + 'bbox', jsonb_build_array( + ST_XMin(ST_Envelope(projects.outline)), + ST_YMin(ST_Envelope(projects.outline)), + ST_XMax(ST_Envelope(projects.outline)), + ST_YMax(ST_Envelope(projects.outline)) + ) + ), + 'id', projects.id + ) AS outline, ST_AsGeoJSON(projects.centroid)::jsonb AS centroid - FROM projects - WHERE projects.id = %(project_id)s + FROM + projects + WHERE + projects.id = %(project_id)s LIMIT 1; - """, + """, {"project_id": project_id}, ) - project_record = await cur.fetchone() - if not project_record: - return None async with db.cursor(row_factory=class_row(TaskOut)) as cur: await cur.execute( - """WITH TaskStateCalculation AS ( - SELECT DISTINCT ON (te.task_id) - te.task_id, - te.user_id, - CASE - WHEN te.state = 'REQUEST_FOR_MAPPING' THEN 'request logs' - WHEN te.state = 'LOCKED_FOR_MAPPING' THEN 'ongoing' - WHEN te.state = 'UNLOCKED_DONE' THEN 'completed' - WHEN te.state = 'UNFLYABLE_TASK' THEN 'unflyable task' - ELSE 'UNLOCKED_TO_MAP' - END AS calculated_state + """ + WITH TaskStateCalculation AS ( + SELECT DISTINCT ON (te.task_id) + te.task_id, + te.user_id, + CASE + WHEN te.state = 'REQUEST_FOR_MAPPING' THEN 'request logs' + WHEN te.state = 'LOCKED_FOR_MAPPING' THEN 'ongoing' + WHEN te.state = 'UNLOCKED_DONE' THEN 'completed' + WHEN te.state = 'UNFLYABLE_TASK' THEN 'unflyable task' + ELSE 'UNLOCKED_TO_MAP' + END AS calculated_state + FROM + task_events te + ORDER BY + te.task_id, te.created_at DESC + ), + TaskGeoJSON AS ( + SELECT + t.id, + t.project_task_index, + ST_AsGeoJSON(t.outline)::jsonb -> 'coordinates' AS coordinates, + ST_AsGeoJSON(t.outline)::jsonb -> 'type' AS type, + ST_XMin(ST_Envelope(t.outline)) AS xmin, + ST_YMin(ST_Envelope(t.outline)) AS ymin, + ST_XMax(ST_Envelope(t.outline)) AS xmax, + ST_YMax(ST_Envelope(t.outline)) AS ymax, + COALESCE(tsc.calculated_state, 'UNLOCKED_TO_MAP') AS state, + tsc.user_id, + u.name, + ST_Area(ST_Transform(t.outline, 3857)) / 1000000 AS task_area + FROM + tasks t + LEFT JOIN + TaskStateCalculation tsc ON t.id = tsc.task_id + LEFT JOIN + users u ON tsc.user_id = u.id + WHERE + t.project_id = %(project_id)s + ) + SELECT + id, + project_task_index, + state, + user_id, + name, + task_area, + jsonb_build_object( + 'type', 'Feature', + 'geometry', jsonb_build_object( + 'type', type, + 'coordinates', coordinates + ), + 'properties', jsonb_build_object( + 'id', id, + 'bbox', jsonb_build_array(xmin, ymin, xmax, ymax) + ), + 'id', id + ) AS outline FROM - task_events te - ORDER BY - te.task_id, te.created_at DESC - ) - SELECT - t.id, - t.project_task_index, - ST_AsGeoJSON(t.outline)::jsonb AS outline, - tsc.user_id, - u.name, - ST_Area(ST_Transform(t.outline, 3857)) / 1000000 AS task_area, - COALESCE(tsc.calculated_state, 'UNLOCKED_TO_MAP') AS state - FROM - tasks t - LEFT JOIN - TaskStateCalculation tsc ON t.id = tsc.task_id - LEFT JOIN - users u ON tsc.user_id = u.id - WHERE - t.project_id = %(project_id)s;""", + TaskGeoJSON; + """, {"project_id": project_id}, ) + task_records = await cur.fetchall() project_record.tasks = task_records if task_records is not None else [] project_record.task_count = len(task_records) @@ -226,7 +272,7 @@ async def all( async with db.cursor(row_factory=dict_row) as cur: await cur.execute( """ - SELECT id, slug, name, description, per_task_instructions, outline, requires_approval_from_manager_for_locking + SELECT id, slug, name, description, per_task_instructions, ST_AsGeoJSON(outline)::jsonb AS outline, requires_approval_from_manager_for_locking FROM projects ORDER BY created_at DESC OFFSET %(skip)s @@ -348,7 +394,7 @@ class ProjectOut(BaseModel): description: str per_task_instructions: Optional[str] = None requires_approval_from_manager_for_locking: Optional[bool] = None - outline: Any = Field(exclude=True) + outline: Optional[Polygon | Feature | FeatureCollection] requires_approval_from_manager_for_locking: bool task_count: int = 0 tasks: Optional[list[TaskOut]] = [] diff --git a/src/frontend/src/views/IndividualProject/index.tsx b/src/frontend/src/views/IndividualProject/index.tsx index 00e46f15..7d2a53e5 100644 --- a/src/frontend/src/views/IndividualProject/index.tsx +++ b/src/frontend/src/views/IndividualProject/index.tsx @@ -50,16 +50,16 @@ const IndividualProject = () => { // modify each task geojson and set locked user id and name to properties and save to redux state called taskData tasksData: res.tasks?.map((task: Record) => ({ ...task, - outline_geojson: { - ...task.outline_geojson, + outline: { + ...task.outline, properties: { - ...task.outline_geojson.properties, + ...task.outline.properties, locked_user_id: task?.user_id, locked_user_name: task?.name, }, }, })), - projectArea: res.outline_geojson, + projectArea: res.outline, }), ); }, From 8b47c6de84fb885461eec6e7776108c5333b5758 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 20 Aug 2024 16:53:09 +0545 Subject: [PATCH 62/66] fix: update user profile dashboard --- src/backend/app/users/user_schemas.py | 2 +- .../src/components/Dashboard/DashboardSidebar/index.tsx | 6 +++--- src/frontend/src/components/GoogleAuth/types/index.ts | 4 ++-- .../UserProfile/FormContents/BasicDetails/index.tsx | 2 +- src/frontend/src/components/common/UserProfile/index.tsx | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index 622db17c..f7d36967 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -210,7 +210,7 @@ async def create(db: Connection, user_data: AuthUser): "user_id": str(user_data.id), "name": user_data.name, "email_address": user_data.email, - "profile_img": user_data.img_url, + "profile_img": user_data.profile_img, }, ) return await cur.fetchone() diff --git a/src/frontend/src/components/Dashboard/DashboardSidebar/index.tsx b/src/frontend/src/components/Dashboard/DashboardSidebar/index.tsx index 0c9996bc..6a5a89e0 100644 --- a/src/frontend/src/components/Dashboard/DashboardSidebar/index.tsx +++ b/src/frontend/src/components/Dashboard/DashboardSidebar/index.tsx @@ -11,10 +11,10 @@ const DashboardSidebar = () => { return ( - profile + profile
{userDetails?.name}
-

{userDetails?.email}

+

{userDetails?.email_address}