From 90091e8a2573c26e9b6d3ce23bb66c68f39736d5 Mon Sep 17 00:00:00 2001 From: phil Date: Fri, 17 Nov 2023 11:35:09 +0530 Subject: [PATCH] Use experimental pydantic, sqlmodel 2 and sqlalchemy 2 JWT based user auth pydantic_settings conf --- .vscode/settings.json | 3 +- pdm.lock | 417 ++++++++++++++++++++++++++++------- pyproject.toml | 16 +- src/_version.py | 1 + src/api.py | 45 ++-- src/application.py | 15 +- src/config.py | 324 +++++++++++++++++++++++---- src/database.py | 18 +- src/models/authentication.py | 9 +- src/models/bootstrap.py | 17 ++ src/models/category.py | 34 +-- src/models/metadata.py | 5 + src/models/tags.py | 9 + src/security.py | 164 ++++++++------ 14 files changed, 840 insertions(+), 237 deletions(-) create mode 100644 src/_version.py create mode 100644 src/models/bootstrap.py create mode 100644 src/models/metadata.py create mode 100644 src/models/tags.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 0fa4149..d7314f5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,6 @@ ], "python.autoComplete.extraPaths": [ "${workspaceFolder}/__pypackages__/3.11/lib" - ] + ], + "editor.defaultFormatter": "charliermarsh.ruff" } \ No newline at end of file diff --git a/pdm.lock b/pdm.lock index b42b2c0..16c807c 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,17 @@ groups = ["default", "dev"] strategy = ["cross_platform"] lock_version = "4.4" -content_hash = "sha256:3a80d01a4a37b3b1440a29f4cd06ba1d9fc3078191feaf3d3401511c9fcdf8c8" +content_hash = "sha256:5f2270d2e84e1fc30449bbcb324864ff25807347a639eb8d6dd070c133fdbe13" + +[[package]] +name = "annotated-types" +version = "0.6.0" +requires_python = ">=3.8" +summary = "Reusable constraint types to use with typing.Annotated" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] [[package]] name = "anyio" @@ -43,20 +53,41 @@ files = [ ] [[package]] -name = "asyncpg" -version = "0.28.0" -requires_python = ">=3.7.0" -summary = "An asyncio PostgreSQL driver" +name = "async-timeout" +version = "4.0.3" +requires_python = ">=3.7" +summary = "Timeout context manager for asyncio programs" files = [ - {file = "asyncpg-0.28.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a0e08fe2c9b3618459caaef35979d45f4e4f8d4f79490c9fa3367251366af207"}, - {file = "asyncpg-0.28.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b24e521f6060ff5d35f761a623b0042c84b9c9b9fb82786aadca95a9cb4a893b"}, - {file = "asyncpg-0.28.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99417210461a41891c4ff301490a8713d1ca99b694fef05dabd7139f9d64bd6c"}, - {file = "asyncpg-0.28.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f029c5adf08c47b10bcdc857001bbef551ae51c57b3110964844a9d79ca0f267"}, - {file = "asyncpg-0.28.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ad1d6abf6c2f5152f46fff06b0e74f25800ce8ec6c80967f0bc789974de3c652"}, - {file = "asyncpg-0.28.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d7fa81ada2807bc50fea1dc741b26a4e99258825ba55913b0ddbf199a10d69d8"}, - {file = "asyncpg-0.28.0-cp311-cp311-win32.whl", hash = "sha256:f33c5685e97821533df3ada9384e7784bd1e7865d2b22f153f2e4bd4a083e102"}, - {file = "asyncpg-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:5e7337c98fb493079d686a4a6965e8bcb059b8e1b8ec42106322fc6c1c889bb0"}, - {file = "asyncpg-0.28.0.tar.gz", hash = "sha256:7252cdc3acb2f52feaa3664280d3bcd78a46bd6c10bfd681acfffefa1120e278"}, + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[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\"", +] +files = [ + {file = "asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b"}, + {file = "asyncpg-0.29.0-cp311-cp311-win32.whl", hash = "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675"}, + {file = "asyncpg-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175"}, + {file = "asyncpg-0.29.0-cp312-cp312-win32.whl", hash = "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02"}, + {file = "asyncpg-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe"}, + {file = "asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e"}, ] [[package]] @@ -78,6 +109,35 @@ files = [ {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, ] +[[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 = "2023.7.22" @@ -213,6 +273,16 @@ files = [ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] +[[package]] +name = "dnspython" +version = "2.4.2" +requires_python = ">=3.8,<4.0" +summary = "DNS toolkit" +files = [ + {file = "dnspython-2.4.2-py3-none-any.whl", hash = "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8"}, + {file = "dnspython-2.4.2.tar.gz", hash = "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984"}, +] + [[package]] name = "ecdsa" version = "0.18.0" @@ -226,6 +296,20 @@ files = [ {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, ] +[[package]] +name = "email-validator" +version = "2.1.0.post1" +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.1.0.post1-py3-none-any.whl", hash = "sha256:c973053efbeddfef924dc0bd93f6e77a1ea7ee0fce935aea7103c7a3d6d2d637"}, + {file = "email_validator-2.1.0.post1.tar.gz", hash = "sha256:a4b0bd1cf55f073b924258d19321b1f3aa74b4b5a71a42c305575dba920e1a44"}, +] + [[package]] name = "executing" version = "2.0.0" @@ -293,7 +377,7 @@ files = [ [[package]] name = "geopandas" -version = "0.14.0" +version = "0.14.1" requires_python = ">=3.9" summary = "Geographic pandas extensions" dependencies = [ @@ -304,8 +388,8 @@ dependencies = [ "shapely>=1.8.0", ] files = [ - {file = "geopandas-0.14.0-py3-none-any.whl", hash = "sha256:a402a565e727642cb44a500c911f226eea26c1b1247c6586827031e3d7a9403a"}, - {file = "geopandas-0.14.0.tar.gz", hash = "sha256:ea6c031889e1e1888aecaa6e182ca620d78f63551c49b3002a998bcbb280531f"}, + {file = "geopandas-0.14.1-py3-none-any.whl", hash = "sha256:ed5a7cae7874bfc3238fb05e0501cc1760e1b7b11e5b76ecad29da644ca305da"}, + {file = "geopandas-0.14.1.tar.gz", hash = "sha256:4853ff89ecb6d1cfc43e7b3671092c8160e8a46a3dd7368f25906283314e42bb"}, ] [[package]] @@ -394,6 +478,16 @@ files = [ {file = "ipython-8.16.1.tar.gz", hash = "sha256:ad52f58fca8f9f848e256c629eff888efc0528c12fe0f8ec14f33205f23ef938"}, ] +[[package]] +name = "itsdangerous" +version = "2.1.2" +requires_python = ">=3.7" +summary = "Safely pass data to untrusted environments and back." +files = [ + {file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"}, + {file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"}, +] + [[package]] name = "jedi" version = "0.19.1" @@ -458,7 +552,7 @@ files = [ [[package]] name = "pandas" -version = "2.1.2" +version = "2.1.3" requires_python = ">=3.9" summary = "Powerful data structures for data analysis, time series, and statistics" dependencies = [ @@ -469,19 +563,19 @@ dependencies = [ "tzdata>=2022.1", ] files = [ - {file = "pandas-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:08d287b68fd28906a94564f15118a7ca8c242e50ae7f8bd91130c362b2108a81"}, - {file = "pandas-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bbd98dcdcd32f408947afdb3f7434fade6edd408c3077bbce7bd840d654d92c6"}, - {file = "pandas-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e90c95abb3285d06f6e4feedafc134306a8eced93cb78e08cf50e224d5ce22e2"}, - {file = "pandas-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52867d69a54e71666cd184b04e839cff7dfc8ed0cd6b936995117fdae8790b69"}, - {file = "pandas-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8d0382645ede2fde352da2a885aac28ec37d38587864c0689b4b2361d17b1d4c"}, - {file = "pandas-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:65177d1c519b55e5b7f094c660ed357bb7d86e799686bb71653b8a4803d8ff0d"}, - {file = "pandas-2.1.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5aa6b86802e8cf7716bf4b4b5a3c99b12d34e9c6a9d06dad254447a620437931"}, - {file = "pandas-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d594e2ce51b8e0b4074e6644758865dc2bb13fd654450c1eae51201260a539f1"}, - {file = "pandas-2.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3223f997b6d2ebf9c010260cf3d889848a93f5d22bb4d14cd32638b3d8bba7ad"}, - {file = "pandas-2.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4944dc004ca6cc701dfa19afb8bdb26ad36b9bed5bcec617d2a11e9cae6902"}, - {file = "pandas-2.1.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3f76280ce8ec216dde336e55b2b82e883401cf466da0fe3be317c03fb8ee7c7d"}, - {file = "pandas-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:7ad20d24acf3a0042512b7e8d8fdc2e827126ed519d6bd1ed8e6c14ec8a2c813"}, - {file = "pandas-2.1.2.tar.gz", hash = "sha256:52897edc2774d2779fbeb6880d2cfb305daa0b1a29c16b91f531a18918a6e0f3"}, + {file = "pandas-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04d4c58e1f112a74689da707be31cf689db086949c71828ef5da86727cfe3f82"}, + {file = "pandas-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fa2ad4ff196768ae63a33f8062e6838efed3a319cf938fdf8b95e956c813042"}, + {file = "pandas-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4441ac94a2a2613e3982e502ccec3bdedefe871e8cea54b8775992485c5660ef"}, + {file = "pandas-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5ded6ff28abbf0ea7689f251754d3789e1edb0c4d0d91028f0b980598418a58"}, + {file = "pandas-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca5680368a5139d4920ae3dc993eb5106d49f814ff24018b64d8850a52c6ed2"}, + {file = "pandas-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:de21e12bf1511190fc1e9ebc067f14ca09fccfb189a813b38d63211d54832f5f"}, + {file = "pandas-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a5d53c725832e5f1645e7674989f4c106e4b7249c1d57549023ed5462d73b140"}, + {file = "pandas-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7cf4cf26042476e39394f1f86868d25b265ff787c9b2f0d367280f11afbdee6d"}, + {file = "pandas-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72c84ec1b1d8e5efcbff5312abe92bfb9d5b558f11e0cf077f5496c4f4a3c99e"}, + {file = "pandas-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f539e113739a3e0cc15176bf1231a553db0239bfa47a2c870283fd93ba4f683"}, + {file = "pandas-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc77309da3b55732059e484a1efc0897f6149183c522390772d3561f9bf96c00"}, + {file = "pandas-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:08637041279b8981a062899da0ef47828df52a1838204d2b3761fbd3e9fcb549"}, + {file = "pandas-2.1.3.tar.gz", hash = "sha256:22929f84bca106921917eb73c1521317ddd0a4c71b395bcf767a106e3494209f"}, ] [[package]] @@ -494,6 +588,29 @@ files = [ {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, ] +[[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" +version = "1.7.4" +extras = ["bcrypt"] +summary = "comprehensive password hashing framework supporting over 30 schemes" +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.8.0" @@ -548,6 +665,7 @@ files = [ {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, @@ -600,22 +718,115 @@ files = [ [[package]] name = "pydantic" -version = "1.10.13" +version = "2.4.0" requires_python = ">=3.7" -summary = "Data validation and settings management using python type hints" +summary = "Data validation using Python type hints" dependencies = [ - "typing-extensions>=4.2.0", + "annotated-types>=0.4.0", + "pydantic-core==2.10.0", + "typing-extensions>=4.6.1", ] files = [ - {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"}, - {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"}, - {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"}, - {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"}, - {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"}, - {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"}, - {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"}, - {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"}, - {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"}, + {file = "pydantic-2.4.0-py3-none-any.whl", hash = "sha256:909b2b7d7be775a890631218e8c4b6b5418c9b6c57074ae153e5c09b73bf06a3"}, + {file = "pydantic-2.4.0.tar.gz", hash = "sha256:54216ccb537a606579f53d7f6ed912e98fffce35aff93b25cd80b1c2ca806fc3"}, +] + +[[package]] +name = "pydantic-core" +version = "2.10.0" +requires_python = ">=3.7" +summary = "" +dependencies = [ + "typing-extensions!=4.7.0,>=4.6.0", +] +files = [ + {file = "pydantic_core-2.10.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:ab2d56dfa13244164f0ba8125d8315c799fa0150459b88fc42ed5c1e3c04d47a"}, + {file = "pydantic_core-2.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1e79893a20207ff671f13f5562c1f0aaece030e6e30252683f536286ba89864"}, + {file = "pydantic_core-2.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:030ba2f59e78c8732445d8c9f093579674f2b5b93b3960945face14ec2e82682"}, + {file = "pydantic_core-2.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:705fad71297dfedc5c9e3c935702864aa0cc7812be11ac544f152677ba6ea430"}, + {file = "pydantic_core-2.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:394a8ce4a7495af8dbf33038daf57a6170be15f8d1d92a7b63c6f2211527d950"}, + {file = "pydantic_core-2.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19c7aa3c0ff08ddc91597d8af08f8c4de59b27fe752b3bd1db9a67f6f08c4020"}, + {file = "pydantic_core-2.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb204346d3eda4e0c63cbeeec6398a52682ac51f9cf7379a13505863e47d3186"}, + {file = "pydantic_core-2.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1fefe63baa04f1d9dd5b4564b1e73d133e1c745589933d7ef9718235915cc81"}, + {file = "pydantic_core-2.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fa4bd88165d860111e860e8b43efd97afd137a9165cf24eb3cfb2371f57452bf"}, + {file = "pydantic_core-2.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e21ab9c49cc58282c228ff89fb4a5e4b447233ccd53acb7f333d1cde58df37b"}, + {file = "pydantic_core-2.10.0-cp311-none-win32.whl", hash = "sha256:2a6f28e2b2a5cef3b52b5ac6c6d64fe810ca51ec57081554f447c818778eea09"}, + {file = "pydantic_core-2.10.0-cp311-none-win_amd64.whl", hash = "sha256:f94539aa4265ab5528d8c3dc4505a19369083c29d0713b8ed536f93b9bc1e94f"}, + {file = "pydantic_core-2.10.0-cp311-none-win_arm64.whl", hash = "sha256:2352f7cb8ef0cd21fbc582abe2a14105d7e8400f97a551ca2e3b05dee77525d2"}, + {file = "pydantic_core-2.10.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:c2a126c7271a9421005a0f57cf71294ad49c375e4d0a9198b93665796f49e7f7"}, + {file = "pydantic_core-2.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7440933341f655a64456065211cf7657c3cf3524d5b0b02f5d9b63ef5a7e0d49"}, + {file = "pydantic_core-2.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85d8225cd08aacb8a2843cf0a0a72f1c403c6ac6f18d4cfeecabe050f80c9ea3"}, + {file = "pydantic_core-2.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:573e89b3da5908f564ae54b6284e20b490158681e91e1776a59dfda17ec0a6a8"}, + {file = "pydantic_core-2.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0061965942489e6da23f0399b1136fd10eff0a4f0cefae13369eba1776e22a6"}, + {file = "pydantic_core-2.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:725f0276402773a6b61b6f67bf9562f37ba08a8bfebdfb9990eea786ed5711b2"}, + {file = "pydantic_core-2.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25cacd12689b1a357ae6212c7f5980ebf487720db5bbf1bb5d91085226b6a962"}, + {file = "pydantic_core-2.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e70c6c882ab101a72010c8f91e87db211fa2aaf6aa51acc7160fe5649630ed75"}, + {file = "pydantic_core-2.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e079540fd4c45c23de4465cafb20cddcd8befe3b5f46505a2eb28e49b9d13ee2"}, + {file = "pydantic_core-2.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:98474284adb71c8738e5efb71ccb1382d8d66f042ad0875018c78bcb38ac0f47"}, + {file = "pydantic_core-2.10.0-cp312-none-win32.whl", hash = "sha256:ab1fa046ef9058ceef941b576c5e7711bab3d99be00a304fb4726cf4b94e05ff"}, + {file = "pydantic_core-2.10.0-cp312-none-win_amd64.whl", hash = "sha256:b4df023610af081d6da85328411fed7aacf19e939fe955bb31f29212f8dcf306"}, + {file = "pydantic_core-2.10.0-cp312-none-win_arm64.whl", hash = "sha256:f1a70f99d1a7270d4f321a8824e87d5b88acd64c2af6049915b7fd8215437e04"}, + {file = "pydantic_core-2.10.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:b40221d1490f2c6e488d2576773a574d42436b5aba1faed91f59a9feb82c384b"}, + {file = "pydantic_core-2.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f3b25201efe20d182f3bd6fe8d99685f4ed01cac67b79c017c9cf688b747263"}, + {file = "pydantic_core-2.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34a45943bb14275e9681fd4abafbe3acae1e7dac7248bebf38ac5bde492e00f7"}, + {file = "pydantic_core-2.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc5be7a29a6b25a186941e9e2b5f9281c05723628e1fdb244f429f4c1682ff49"}, + {file = "pydantic_core-2.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17460ffd8f8e49ca52711b4926fefe2b336d01b63dc27aee432a576c2147c8ce"}, + {file = "pydantic_core-2.10.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c1ab3701d660bd136a22e1ca95292bfed50245eb869adaee2e08f29d4dd5e360"}, + {file = "pydantic_core-2.10.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:09ac18617199704327d99c85893d697b8442c18b8c2db1ea636ba83313223541"}, + {file = "pydantic_core-2.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e3f69d48191103587950981cf47c936064c808b6c18f57e745ed130a305c73a6"}, + {file = "pydantic_core-2.10.0-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:792af9e4f78d6f1d0aabfb95162c5ed56b5369b25350eaa68b1495e8f675d4d9"}, + {file = "pydantic_core-2.10.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ecd28fb4c98c97836046d092029017bcc35e060ea547484aa1234b8a592de17"}, + {file = "pydantic_core-2.10.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a622a8abf656cc51960766fa4d194504e8a9f85ae48032f87fb42c79462c7b8"}, + {file = "pydantic_core-2.10.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:52eb5c61de017bfee422f6aa9a3e76de5aa5a9189ba808bba63b9de67e55c4ca"}, + {file = "pydantic_core-2.10.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:69772dcdcf90b677d0d2ecedafe4c6a610572f1fad15912cde28a6f8eb5654fd"}, + {file = "pydantic_core-2.10.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:12470a4de172aaa1bbadb45744de4a9b0298fa8f974eb508314c3b5da0cb4aed"}, + {file = "pydantic_core-2.10.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f9f2c70257f03db712658d4138e2b892bdd7c71472783eaebc2813a47fd29ef3"}, + {file = "pydantic_core-2.10.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:8a5323d6778931ab1b3b22bac05fb7c961786d3b04a6c84f7c0ffcc331b4b998"}, + {file = "pydantic_core-2.10.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:5f00e83aa9aebbfd4382695a5ed94e6282ac01455fbb1a37d99d2effa29df30f"}, + {file = "pydantic_core-2.10.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c871820c60fc863c7b3f660612af6ce5bb8f5f69d6364f208e29d2ca7992d154"}, + {file = "pydantic_core-2.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1bcb1b9b33573eeef218ffb3a2910c57fedc8831caf3c942e68a2222481d2cc"}, + {file = "pydantic_core-2.10.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d122a46c360c8069f7ac39c6f2c29cf99436baa48ba1e28ea5443336e9bbb838"}, + {file = "pydantic_core-2.10.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ffb2a3462bb7905c4d849b95f536ac1f3948e92f5e0fc7e65bd3f3b0d132cf4"}, + {file = "pydantic_core-2.10.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b5d4eec8aba25b163a4d9dcc6be8354bc8f939040bc15a6400cbd62ba0511a5f"}, + {file = "pydantic_core-2.10.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5cbfe4cd608cf6d032374961e4e07d0506acfaec7b1a69beade1d5f98dce00fd"}, + {file = "pydantic_core-2.10.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:02b3d546342e7f583bf58f4a4618c7e97f44426db2358789393537dd4e9a921d"}, + {file = "pydantic_core-2.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7820faf076216654ae54ad8a8443a296faaac9057a49ff404ce92ab85c9518a3"}, + {file = "pydantic_core-2.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f114130c44ae52b3bd2450dac8e1d3e1e92a92baecb24dbcdb6de2d2fc15bdb5"}, + {file = "pydantic_core-2.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f6f70680c15876c583a24bd476e49004327e87392be0282aedbc65773519ea8"}, + {file = "pydantic_core-2.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3f230d70be54447e12fcd0f1c2319dac74341244fafd2350d5675aa194f6c3f4"}, + {file = "pydantic_core-2.10.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:96b3007451863b46e8138f8096ef31aea6f7721a9910843b0554ce4ae17024a2"}, + {file = "pydantic_core-2.10.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b196c4ace34be6c2953c6ec3906d1af88c418b93325d612d7f900ed30bf1e0ac"}, + {file = "pydantic_core-2.10.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5958b1af7acd7b4a629e9758ce54a31c1910695e85e0ef847ba3daa4f25a0a08"}, + {file = "pydantic_core-2.10.0.tar.gz", hash = "sha256:8fe66506700efdfc699c613ccc4974ac7d8fceed8c74983e55ec380504db2e05"}, +] + +[[package]] +name = "pydantic-settings" +version = "2.0.3" +requires_python = ">=3.7" +summary = "Settings management using Pydantic" +dependencies = [ + "pydantic>=2.0.1", + "python-dotenv>=0.21.0", +] +files = [ + {file = "pydantic_settings-2.0.3-py3-none-any.whl", hash = "sha256:ddd907b066622bd67603b75e2ff791875540dc485b7307c4fffc015719da8625"}, + {file = "pydantic_settings-2.0.3.tar.gz", hash = "sha256:962dc3672495aad6ae96a4390fac7e593591e144625e5112d359f8f67fb75945"}, +] + +[[package]] +name = "pydantic" +version = "2.4.0" +extras = ["email"] +requires_python = ">=3.7" +summary = "Data validation using Python type hints" +dependencies = [ + "email-validator>=2.0.0", + "pydantic==2.4.0", +] +files = [ + {file = "pydantic-2.4.0-py3-none-any.whl", hash = "sha256:909b2b7d7be775a890631218e8c4b6b5418c9b6c57074ae153e5c09b73bf06a3"}, + {file = "pydantic-2.4.0.tar.gz", hash = "sha256:54216ccb537a606579f53d7f6ed912e98fffce35aff93b25cd80b1c2ca806fc3"}, ] [[package]] @@ -667,6 +878,16 @@ files = [ {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] +[[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-jose" version = "3.3.0" @@ -695,6 +916,16 @@ files = [ {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, ] +[[package]] +name = "python-multipart" +version = "0.0.6" +requires_python = ">=3.7" +summary = "A streaming multipart parser for Python" +files = [ + {file = "python_multipart-0.0.6-py3-none-any.whl", hash = "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18"}, + {file = "python_multipart-0.0.6.tar.gz", hash = "sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132"}, +] + [[package]] name = "pytz" version = "2023.3.post1" @@ -704,6 +935,29 @@ files = [ {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, ] +[[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_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 = "rsa" version = "4.9" @@ -775,64 +1029,59 @@ files = [ [[package]] name = "sqlalchemy" -version = "1.4.50" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +version = "2.0.11" +requires_python = ">=3.7" summary = "Database Abstraction Library" dependencies = [ - "greenlet!=0.4.17; python_version >= \"3\" and (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 == \"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\")))))", + "typing-extensions>=4.2.0", ] files = [ - {file = "SQLAlchemy-1.4.50-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14b0cacdc8a4759a1e1bd47dc3ee3f5db997129eb091330beda1da5a0e9e5bd7"}, - {file = "SQLAlchemy-1.4.50-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fb9cb60e0f33040e4f4681e6658a7eb03b5cb4643284172f91410d8c493dace"}, - {file = "SQLAlchemy-1.4.50-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cb501d585aa74a0f86d0ea6263b9c5e1d1463f8f9071392477fd401bd3c7cc"}, - {file = "SQLAlchemy-1.4.50-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a7a66297e46f85a04d68981917c75723e377d2e0599d15fbe7a56abed5e2d75"}, - {file = "SQLAlchemy-1.4.50.tar.gz", hash = "sha256:3b97ddf509fc21e10b09403b5219b06c5b558b27fc2453150274fa4e70707dbf"}, -] - -[[package]] -name = "sqlalchemy2-stubs" -version = "0.0.2a36" -requires_python = ">=3.6" -summary = "Typing Stubs for SQLAlchemy 1.4" -dependencies = [ - "typing-extensions>=3.7.4", -] -files = [ - {file = "sqlalchemy2-stubs-0.0.2a36.tar.gz", hash = "sha256:1c820c176a50401b7b3fc1e25019703b2c0753fe99a79d7e19305146baf1f60f"}, - {file = "sqlalchemy2_stubs-0.0.2a36-py3-none-any.whl", hash = "sha256:9b5b3eb263cdc649b6a5619d2c089b98290406027a01e1de171eeb98c38ce678"}, + {file = "SQLAlchemy-2.0.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa81761ff674d2e2d591fc88d31835d3ecf65bddb021a522f4eaaae831c584cf"}, + {file = "SQLAlchemy-2.0.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21f447403a1bfeb832a7384c4ac742b7baab04460632c0335e020e8e2c741d4b"}, + {file = "SQLAlchemy-2.0.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4d8d96c0a7265de8496250a2c2d02593da5e5e85ea24b5c54c2db028d74cf8c"}, + {file = "SQLAlchemy-2.0.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c4c5834789f718315cb25d1b95d18fde91b72a1a158cdc515d7f6380c1f02a3"}, + {file = "SQLAlchemy-2.0.11-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f57965a9d5882efdea0a2c87ae2f6c7dbc14591dcd0639209b50eec2b3ec947e"}, + {file = "SQLAlchemy-2.0.11-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0dd98b0be54503afc4c74e947720c3196f96fb2546bfa54d911d5de313c5463c"}, + {file = "SQLAlchemy-2.0.11-cp311-cp311-win32.whl", hash = "sha256:eec40c522781a58839df6a2a7a2d9fbaa473419a3ab94633d61e00a8c0c768b7"}, + {file = "SQLAlchemy-2.0.11-cp311-cp311-win_amd64.whl", hash = "sha256:62835d8cd6713458c032466c38a43e56503e19ea6e54b0e73295c6ab281fc0b1"}, + {file = "SQLAlchemy-2.0.11-py3-none-any.whl", hash = "sha256:1d28e8278d943d9111d44720f92cc338282e956ed68849bfcee053c06bde4f39"}, + {file = "SQLAlchemy-2.0.11.tar.gz", hash = "sha256:c3cbff7cced3c42dbe71448ce6bf4202b4a2d305e78dd77e3f280ba6cd245138"}, ] [[package]] name = "sqlalchemy" -version = "1.4.50" +version = "2.0.11" extras = ["asyncio"] -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +requires_python = ">=3.7" summary = "Database Abstraction Library" dependencies = [ - "greenlet!=0.4.17; python_version >= \"3\"", - "sqlalchemy==1.4.50", + "greenlet!=0.4.17", + "sqlalchemy==2.0.11", ] files = [ - {file = "SQLAlchemy-1.4.50-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14b0cacdc8a4759a1e1bd47dc3ee3f5db997129eb091330beda1da5a0e9e5bd7"}, - {file = "SQLAlchemy-1.4.50-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fb9cb60e0f33040e4f4681e6658a7eb03b5cb4643284172f91410d8c493dace"}, - {file = "SQLAlchemy-1.4.50-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cb501d585aa74a0f86d0ea6263b9c5e1d1463f8f9071392477fd401bd3c7cc"}, - {file = "SQLAlchemy-1.4.50-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a7a66297e46f85a04d68981917c75723e377d2e0599d15fbe7a56abed5e2d75"}, - {file = "SQLAlchemy-1.4.50.tar.gz", hash = "sha256:3b97ddf509fc21e10b09403b5219b06c5b558b27fc2453150274fa4e70707dbf"}, + {file = "SQLAlchemy-2.0.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa81761ff674d2e2d591fc88d31835d3ecf65bddb021a522f4eaaae831c584cf"}, + {file = "SQLAlchemy-2.0.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21f447403a1bfeb832a7384c4ac742b7baab04460632c0335e020e8e2c741d4b"}, + {file = "SQLAlchemy-2.0.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4d8d96c0a7265de8496250a2c2d02593da5e5e85ea24b5c54c2db028d74cf8c"}, + {file = "SQLAlchemy-2.0.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c4c5834789f718315cb25d1b95d18fde91b72a1a158cdc515d7f6380c1f02a3"}, + {file = "SQLAlchemy-2.0.11-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f57965a9d5882efdea0a2c87ae2f6c7dbc14591dcd0639209b50eec2b3ec947e"}, + {file = "SQLAlchemy-2.0.11-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0dd98b0be54503afc4c74e947720c3196f96fb2546bfa54d911d5de313c5463c"}, + {file = "SQLAlchemy-2.0.11-cp311-cp311-win32.whl", hash = "sha256:eec40c522781a58839df6a2a7a2d9fbaa473419a3ab94633d61e00a8c0c768b7"}, + {file = "SQLAlchemy-2.0.11-cp311-cp311-win_amd64.whl", hash = "sha256:62835d8cd6713458c032466c38a43e56503e19ea6e54b0e73295c6ab281fc0b1"}, + {file = "SQLAlchemy-2.0.11-py3-none-any.whl", hash = "sha256:1d28e8278d943d9111d44720f92cc338282e956ed68849bfcee053c06bde4f39"}, + {file = "SQLAlchemy-2.0.11.tar.gz", hash = "sha256:c3cbff7cced3c42dbe71448ce6bf4202b4a2d305e78dd77e3f280ba6cd245138"}, ] [[package]] name = "sqlmodel" -version = "0.0.11" +version = "0" requires_python = ">=3.7,<4.0" +git = "https://github.com/honglei/sqlmodel.git" +revision = "3005495a3ec6c8216b31cbd623f91c7bc8ba174f" summary = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness." dependencies = [ - "SQLAlchemy<2.0.0,>=1.4.36", - "pydantic<2.0.0,>=1.9.0", - "sqlalchemy2-stubs", -] -files = [ - {file = "sqlmodel-0.0.11-py3-none-any.whl", hash = "sha256:bc0d64c4b901d919d2f16bbd79aefb07cb268c29f7c1dd83a84758772ccc95c6"}, - {file = "sqlmodel-0.0.11.tar.gz", hash = "sha256:fc33abbf7ec29caafabe3d0e1db61e33597857a289e5fd1ecdb91be702b26084"}, + "SQLAlchemy<=2.0.11,>=2.0.0", + "pydantic[email]<=2.4,>=2.1.1", ] [[package]] @@ -894,7 +1143,7 @@ files = [ [[package]] name = "uvicorn" -version = "0.23.2" +version = "0.24.0.post1" requires_python = ">=3.8" summary = "The lightning-fast ASGI server." dependencies = [ @@ -902,8 +1151,8 @@ dependencies = [ "h11>=0.8", ] files = [ - {file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"}, - {file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"}, + {file = "uvicorn-0.24.0.post1-py3-none-any.whl", hash = "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e"}, + {file = "uvicorn-0.24.0.post1.tar.gz", hash = "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 23d976b..3f59377 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,20 +13,30 @@ dependencies = [ "psycopg2-binary>=2.9.9", "sqlalchemy[asyncio]", "asyncpg>=0.28.0", - "sqlmodel>=0.0.11", + #"sqlmodel>=0.0.11", "python-jose[cryptography]>=3.3.0", "geoalchemy2>=0.14.2", + "pyyaml>=6.0.1", + "python-multipart>=0.0.6", + "pydantic-settings>=2.0.3", + "itsdangerous>=2.1.2", + "passlib[bcrypt]>=1.7.4", ] requires-python = ">=3.11" readme = "README.md" license = {text = "MIT"} +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" + [project.optional-dependencies] dev = [ "ipdb>=0.13.13", + "sqlmodel @ git+https://github.com/honglei/sqlmodel.git#egg=sqlmodel", ] [tool.pdm.version] source = "scm" -write_to = "src/_version.py" -write_template = "__version__ = '{}'" \ No newline at end of file +write_to = "_version.py" +write_template = "__version__ = '{}'" diff --git a/src/_version.py b/src/_version.py new file mode 100644 index 0000000..a96fc95 --- /dev/null +++ b/src/_version.py @@ -0,0 +1 @@ +__version__ = '2023.3+d20231113' \ No newline at end of file diff --git a/src/api.py b/src/api.py index a46994c..a172d91 100644 --- a/src/api.py +++ b/src/api.py @@ -1,7 +1,12 @@ +import logging from datetime import timedelta +from time import time +from uuid import uuid1 +from typing import Annotated -from fastapi import Depends, FastAPI, HTTPException, status +from fastapi import Depends, FastAPI, HTTPException, status, Request from fastapi.security import OAuth2PasswordRequestForm +from starlette.middleware.sessions import SessionMiddleware from sqlmodel import select from sqlmodel.ext.asyncio.session import AsyncSession @@ -17,22 +22,38 @@ from .models.category import ( CategoryGroup, CategoryModelType, Category, CategoryRead ) +from .models.bootstrap import BootstrapData from .database import get_db_session, pandas_query from .security import ( - User, Token, - authenticate_user, get_current_active_user, create_access_token, + User as UserAuth, + Token, + authenticate_user, get_current_user, create_access_token, ) from .config import conf -api = FastAPI() +logger = logging.getLogger(__name__) +api = FastAPI() +api.add_middleware(SessionMiddleware, secret_key=conf.crypto.secret) + +db_session = Annotated[AsyncSession, Depends(get_db_session)] @api.get("/nothing") async def get_nothing() -> str: return '' -@api.post("/token", response_model=Token) -async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): + +@api.get('/bootstrap') +async def bootstrap( + user: Annotated[UserRead, Depends(get_current_user)]) -> BootstrapData: + return BootstrapData(user=user) + + +@api.post("/token") +async def login_for_access_token( + db_session: db_session, + form_data: OAuth2PasswordRequestForm = Depends() + ) -> Token: user = await authenticate_user(form_data.username, form_data.password) if not user: raise HTTPException( @@ -40,16 +61,14 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) - access_token_expires = timedelta( - minutes=conf.security['access_token_expire_minutes']) access_token = create_access_token( data={"sub": user.username}, - expires_delta=access_token_expires) + expires_delta=timedelta(seconds=conf.crypto.expire)) return {"access_token": access_token, "token_type": "bearer"} @api.get("/users") async def get_users( - *, db_session: AsyncSession = Depends(get_db_session) + db_session: db_session, ) -> list[UserRead]: query = select(User).options(selectinload(User.roles)) data = await db_session.exec(query) @@ -57,7 +76,7 @@ async def get_users( @api.get("/roles") async def get_roles( - *, db_session: AsyncSession = Depends(get_db_session) + db_session: db_session, ) -> list[RoleRead]: query = select(Role).options(selectinload(Role.users)) data = await db_session.exec(query) @@ -65,7 +84,7 @@ async def get_roles( @api.get("/categories") async def get_categories( - *, db_session: AsyncSession = Depends(get_db_session) + db_session: db_session, ) -> list[CategoryRead]: query = select(Category) data = await db_session.exec(query) @@ -74,7 +93,7 @@ async def get_categories( @api.get("/categories_p") async def get_categories_p( - *, db_session: AsyncSession = Depends(get_db_session) + db_session: db_session, ) -> list[CategoryRead]: query = select(Category) df = await db_session.run_sync(pandas_query, query) diff --git a/src/application.py b/src/application.py index 759ac3d..75e9b30 100644 --- a/src/application.py +++ b/src/application.py @@ -1,5 +1,14 @@ -from fastapi import Depends, FastAPI -from .api import api +from fastapi import FastAPI +import logging -app = FastAPI() +from .api import api +from .config import conf + +logging.basicConfig(level=conf.gisaf.debugLevel) + +app = FastAPI( + debug=True, + title=conf.gisaf.title, + version=conf.version, +) app.mount('/v2', api) \ No newline at end of file diff --git a/src/config.py b/src/config.py index 5be0631..f486a9b 100644 --- a/src/config.py +++ b/src/config.py @@ -1,55 +1,303 @@ from os import environ import logging from pathlib import Path -import yaml +from typing import Any, Type, Tuple -from sqlalchemy.ext.asyncio.engine import AsyncEngine -from sqlalchemy.orm.session import sessionmaker +from pydantic_settings import BaseSettings, PydanticBaseSettingsSource +from pydantic.v1.utils import deep_update +from yaml import safe_load + +from ._version import __version__ +#from sqlalchemy.ext.asyncio.engine import AsyncEngine +#from sqlalchemy.orm.session import sessionmaker logger = logging.getLogger(__name__) ENV = environ.get('env', 'prod') +config_files = [ + Path(Path.cwd().root) / 'etc' / 'gisaf' / ENV, + Path.home() / '.local' / 'gisaf' / ENV +] -class Config: - app: dict - postgres: dict - storage: dict - map: dict - security: dict +class DashboardHome(BaseSettings): + title: str + content_file: str + footer_file: str + +class GisafConfig(BaseSettings): + title: str + windowTitle: str + debugLevel: str + dashboard_home: DashboardHome + redirect: str = '' + +class SpatialSysRef(BaseSettings): + author: str + ellps: str + k: int + lat_0: float + lon_0: float + no_defs: bool + proj: str + towgs84: str + units: str + x_0: float + y_0: float + +class RawSurvey(BaseSettings): + spatial_sys_ref: SpatialSysRef + srid: int + +class Geo(BaseSettings): + raw_survey: RawSurvey + simplify_geom_factor: int + srid: int + srid_for_proj: int + +class Flask(BaseSettings): + secret_key: str + debug: int + +class MQTT(BaseSettings): + broker: str + +class GisafLive(BaseSettings): + hostname: str + port: int + scheme: str + redis: str + mqtt: MQTT + +class DefaultSurvey(BaseSettings): + surveyor_id: int + equipment_id: int + +class Survey(BaseSettings): + schema_raw: str + schema: str + default: DefaultSurvey + +class Crypto(BaseSettings): + secret: str + algorithm: str + expire: float + +class DB(BaseSettings): + uri: str + host: str + user: str + db: str + password: str + debug: bool + info: bool + pool_size: int = 10 + max_overflow: int = 10 + +class Log(BaseSettings): + level: str + +class OGCAPILicense(BaseSettings): + name: str + url: str + +class OGCAPIProvider(BaseSettings): + name: str + url: str + +class OGCAPIServerContact(BaseSettings): + name: str + address: str + city: str + stateorprovince: str + postalcode: int + country: str + email: str + +class OGCAPIIdentification(BaseSettings): + title: str + description: str + keywords: list[str] + keywords_type: str + terms_of_service: str + url: str + +class OGCAPIMetadata(BaseSettings): + identification: OGCAPIIdentification + license: OGCAPILicense + provider: OGCAPIProvider + contact: OGCAPIServerContact + +class ServerBind(BaseSettings): + host: str + port: int + +class OGCAPIServerMap(BaseSettings): + url: str + attribution: str + +class OGCAPIServer(BaseSettings): + bind: ServerBind + url: str + mimetype: str + encoding: str + language: str + pretty_print: bool + limit: int + map: OGCAPIServerMap + +class OGCAPI(BaseSettings): + base_url: str + bbox: list[float] + log: Log + metadata: OGCAPIMetadata + server: OGCAPIServer + +class Map(BaseSettings): + tilesBaseDir: str + tilesUseRequestUrl: bool + tilesSpriteBaseDir: str + tilesSpriteUrl: str + tilesSpriteBaseUrl: str + openMapTilesKey: str + zoom: int + pitch: int + lat: float + lng: float + bearing: float + style: str + opacity: float + attribution: str + status: list[str] + defaultStatus: list[str] # FIXME: should be str + tagKeys: list[str] + +class Measures(BaseSettings): + defaultStore: str + +class BasketDefault(BaseSettings): + surveyor: str + equipment: str + project: str | None + status: str + store: str | None + +class BasketOldDef(BaseSettings): + base_dir: str + +class Basket(BaseSettings): + base_dir: str + default: BasketDefault + +class Plot(BaseSettings): + maxDataSize: int + +class Dashboard(BaseSettings): + base_source_url: str + base_storage_dir: str + base_storage_url: str + +class Widgets(BaseSettings): + footer: str + +class Admin(BaseSettings): + basket: Basket + +class Attachments(BaseSettings): + base_dir: str + +class Job(BaseSettings): + id: str + func: str + trigger: str + minutes: int | None = 0 + seconds: int | None = 0 + +class Config(BaseSettings): + @classmethod + def settings_customise_sources( + cls, + settings_cls: Type[BaseSettings], + init_settings: PydanticBaseSettingsSource, + env_settings: PydanticBaseSettingsSource, + dotenv_settings: PydanticBaseSettingsSource, + file_secret_settings: PydanticBaseSettingsSource, + ) -> Tuple[PydanticBaseSettingsSource, ...]: + return env_settings, init_settings, file_secret_settings, config_file_settings + + admin: Admin + attachments: Attachments + basket: BasketOldDef + crypto: Crypto + dashboard: Dashboard + db: DB + flask: Flask + geo: Geo + gisaf: GisafConfig + gisaf_live: GisafLive + jobs: list[Job] + map: Map + measures: Measures + ogcapi: OGCAPI + plot: Plot + plugins: dict[str, dict[str, Any]] + survey: Survey version: str - engine: AsyncEngine - session_maker: sessionmaker - - def __init__(self) -> None: - from ._version import __version__ - self.version = __version__ + weather_station: dict[str, dict[str, Any]] + widgets: Widgets + #engine: AsyncEngine + #session_maker: sessionmaker -conf = Config() +def config_file_settings() -> dict[str, Any]: + config: dict[str, Any] = {} + for p in config_files: + for suffix in {".yaml", ".yml"}: + path = p.with_suffix(suffix) + if not path.is_file(): + logger.info(f"No file found at `{path.resolve()}`") + continue + logger.debug(f"Reading config file `{path.resolve()}`") + if path.suffix in {".yaml", ".yml"}: + config = deep_update(config, load_yaml(path)) + else: + logger.info(f"Unknown config file extension `{path.suffix}`") + return config -def set_app_config(app) -> None: - raw_configs = [] - with open(Path(__file__).parent / 'defaults.yml') as cf: - raw_configs.append(cf.read()) - for cf_path in ( - Path(Path.cwd().root) / 'etc' / 'gisaf' / ENV, - Path.home() / '.local' / 'gisaf' / ENV - ): - try: - with open(cf_path.with_suffix('.yml')) as cf: - raw_configs.append(cf.read()) - except FileNotFoundError: - pass +def load_yaml(path: Path) -> dict[str, Any]: + with Path(path).open("r") as f: + config = safe_load(f) + if not isinstance(config, dict): + raise TypeError( + f"Config file has no top-level mapping: {path}" + ) + return config - yaml_config = yaml.safe_load('\n'.join(raw_configs)) - conf.app = yaml_config['app'] - conf.postgres = yaml_config['postgres'] - conf.storage = yaml_config['storage'] - conf.map = yaml_config['map'] - conf.security = yaml_config['security'] - # create_dirs() +conf = Config(version=__version__) + +# def set_app_config(app) -> None: +# raw_configs = [] +# with open(Path(__file__).parent / 'defaults.yml') as cf: +# raw_configs.append(cf.read()) +# for cf_path in ( +# Path(Path.cwd().root) / 'etc' / 'gisaf' / ENV, +# Path.home() / '.local' / 'gisaf' / ENV +# ): +# try: +# with open(cf_path.with_suffix('.yml')) as cf: +# raw_configs.append(cf.read()) +# except FileNotFoundError: +# pass + +# yaml_config = safe_load('\n'.join(raw_configs)) + +# conf.app = yaml_config['app'] +# conf.postgres = yaml_config['postgres'] +# conf.storage = yaml_config['storage'] +# conf.map = yaml_config['map'] +# conf.security = yaml_config['security'] +# # create_dirs() # def create_dirs(): @@ -65,5 +313,5 @@ def set_app_config(app) -> None: # get_cache_dir().mkdir(parents=True, exist_ok=True) -def get_cache_dir() -> Path: - return Path(conf.storage['root_cache_path']) \ No newline at end of file +# def get_cache_dir() -> Path: +# return Path(conf.storage['root_cache_path']) \ No newline at end of file diff --git a/src/database.py b/src/database.py index cb5bf0d..9433ea2 100644 --- a/src/database.py +++ b/src/database.py @@ -1,14 +1,28 @@ +from contextlib import asynccontextmanager + from sqlalchemy.ext.asyncio import create_async_engine from sqlmodel.ext.asyncio.session import AsyncSession import pandas as pd +from .config import conf + echo = False pg_url = "postgresql+asyncpg://avgis@localhost/avgis" -engine = create_async_engine(pg_url, echo=echo) +engine = create_async_engine( + pg_url, + echo=echo, + pool_size=conf.db.pool_size, + max_overflow=conf.db.max_overflow, +) -async def get_db_session(): +async def get_db_session() -> AsyncSession: + async with AsyncSession(engine) as session: + yield session + +@asynccontextmanager +async def db_session() -> AsyncSession: async with AsyncSession(engine) as session: yield session diff --git a/src/models/authentication.py b/src/models/authentication.py index 1eef017..39c7047 100644 --- a/src/models/authentication.py +++ b/src/models/authentication.py @@ -1,10 +1,9 @@ from sqlmodel import Field, SQLModel, MetaData, Relationship -schema = 'gisaf_admin' -metadata = MetaData(schema=schema) +from .metadata import gisaf_admin class UserRoleLink(SQLModel, table=True): - metadata = metadata + metadata = gisaf_admin __tablename__: str = 'roles_users' user_id: int | None = Field( default=None, foreign_key="user.id", primary_key=True @@ -20,7 +19,7 @@ class UserBase(SQLModel): class User(UserBase, table=True): - metadata = metadata + metadata = gisaf_admin id: int | None = Field(default=None, primary_key=True) roles: list["Role"] = Relationship(back_populates="users", link_model=UserRoleLink) @@ -34,7 +33,7 @@ class RoleWithDescription(RoleBase): description: str | None class Role(RoleWithDescription, table=True): - metadata = metadata + metadata = gisaf_admin id: int | None = Field(default=None, primary_key=True) users: list[User] = Relationship(back_populates="roles", link_model=UserRoleLink) diff --git a/src/models/bootstrap.py b/src/models/bootstrap.py new file mode 100644 index 0000000..d2dff89 --- /dev/null +++ b/src/models/bootstrap.py @@ -0,0 +1,17 @@ +from sqlmodel import Field, SQLModel, MetaData, JSON, TEXT, Relationship, Column +from ..config import conf, Map, Measures, Geo +from .authentication import UserRead + +class Proj(SQLModel): + srid: str + srid_for_proj: str + +class BootstrapData(SQLModel): + version: str = conf.version + title: str = conf.gisaf.title + windowTitle: str = conf.gisaf.windowTitle + map: Map = conf.map + geo: Geo = conf.geo + measures: Measures = conf.measures + redirect: str = conf.gisaf.redirect + user: UserRead | None = None \ No newline at end of file diff --git a/src/models/category.py b/src/models/category.py index d3511f7..74e24fe 100644 --- a/src/models/category.py +++ b/src/models/category.py @@ -1,8 +1,8 @@ from typing import Any from sqlmodel import Field, SQLModel, MetaData, JSON, TEXT, Relationship, Column +from pydantic import computed_field -schema = 'gisaf_survey' -metadata = MetaData(schema=schema) +from .metadata import gisaf_survey mapbox_type_mapping = { 'Point': 'symbol', @@ -11,7 +11,7 @@ mapbox_type_mapping = { } class CategoryGroup(SQLModel, table=True): - metadata = metadata + metadata = gisaf_survey name: str = Field(min_length=4, max_length=4, default=None, primary_key=True) major: str @@ -23,7 +23,7 @@ class CategoryGroup(SQLModel, table=True): class CategoryModelType(SQLModel, table=True): - metadata = metadata + metadata = gisaf_survey name: str = Field(default=None, primary_key=True) class Admin: @@ -32,8 +32,6 @@ class CategoryModelType(SQLModel, table=True): class CategoryBase(SQLModel): - metadata = metadata - class Admin: menu = 'Other' flask_admin_model_view = 'CategoryModelView' @@ -49,35 +47,39 @@ class CategoryBase(SQLModel): custom: bool | None auto_import: bool = True model_type: str = Field(max_length=50, - foreign_key='CategoryModelType.name', default='Point') + foreign_key='CategoryModelType.name', + default='Point') long_name: str | None = Field(max_length=50) style: str | None = Field(sa_column=Column(TEXT)) symbol: str | None = Field(max_length=1) mapbox_type_custom: str | None = Field(max_length=32) - mapbox_paint: dict[str, Any] | None = Field(sa_column=Column(JSON, none_as_null=True)) - mapbox_layout: dict[str, Any] | None = Field(sa_column=Column(JSON, none_as_null=True)) + mapbox_paint: dict[str, Any] | None = Field(sa_column=Column(JSON(none_as_null=True))) + mapbox_layout: dict[str, Any] | None = Field(sa_column=Column(JSON(none_as_null=True))) viewable_role: str | None - extra: dict[str, Any] | None = Field(sa_column=Column(JSON, none_as_null=True)) + extra: dict[str, Any] | None = Field(sa_column=Column(JSON(none_as_null=True))) class Category(CategoryBase, table=True): + metadata = gisaf_survey name: str = Field(default=None, primary_key=True) class CategoryRead(CategoryBase): name: str - domain = 'V' # Survey + domain: str = 'V' # Survey + @computed_field @property - def layer_name(self): + def layer_name(self) -> str: """ ISO compliant layer name (see ISO 13567) :return: str """ return '{self.domain}-{self.group:4s}-{self.minor_group_1:4s}-{self.minor_group_2:4s}-{self.status:1s}'.format(self=self) + @computed_field @property - def table_name(self): + def table_name(self) -> str: """ Table name :return: @@ -87,8 +89,9 @@ class CategoryRead(CategoryBase): else: return '{self.domain}_{self.group:4s}_{self.minor_group_1:4s}_{self.minor_group_2:4s}'.format(self=self) + @computed_field @property - def raw_survey_table_name(self): + def raw_survey_table_name(self) -> str: """ Table name :return: @@ -98,6 +101,7 @@ class CategoryRead(CategoryBase): else: return 'RAW_{self.domain}_{self.group:4s}_{self.minor_group_1:4s}_{self.minor_group_2:4s}'.format(self=self) + @computed_field @property - def mapbox_type(self): + def mapbox_type(self) -> str: return self.mapbox_type_custom or mapbox_type_mapping[self.model_type] diff --git a/src/models/metadata.py b/src/models/metadata.py new file mode 100644 index 0000000..38474ab --- /dev/null +++ b/src/models/metadata.py @@ -0,0 +1,5 @@ +from sqlmodel import MetaData + +gisaf = MetaData(schema='gisaf') +gisaf_survey = MetaData(schema='gisaf_survey') +gisaf_admin= MetaData(schema='gisaf_admin') \ No newline at end of file diff --git a/src/models/tags.py b/src/models/tags.py new file mode 100644 index 0000000..f051f0b --- /dev/null +++ b/src/models/tags.py @@ -0,0 +1,9 @@ +from typing import Any +from sqlmodel import Field, SQLModel, MetaData, JSON, TEXT, Relationship, Column +from pydantic import computed_field + +from .metadata import gisaf +from .models_base import GeoPointModel + +class Tags(GeoPointModel, table=True): + metadata = gisaf \ No newline at end of file diff --git a/src/security.py b/src/security.py index 4ecf08a..95cd929 100644 --- a/src/security.py +++ b/src/security.py @@ -1,21 +1,26 @@ from datetime import datetime, timedelta -from passlib.context import CryptContext +import logging +from typing import Annotated +#from passlib.context import CryptContext from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer +from passlib.context import CryptContext +from passlib.exc import UnknownHashError from pydantic import BaseModel -from jose import JWTError, jwt +from sqlmodel.ext.asyncio.session import AsyncSession +from jose import JWTError, jwt, ExpiredSignatureError -from sqlalchemy.future import select +from sqlalchemy import select +from sqlalchemy.orm import selectinload from .config import conf -from .models.authentication import User as UserInDB +from .database import db_session +from .models.authentication import User, UserRead +logger = logging.getLogger(__name__) -# openssl rand -hex 32 -# import secrets -# SECRET_KEY = secrets.token_hex(32) -ALGORITHM: str = "HS256" +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") class Token(BaseModel): @@ -27,105 +32,118 @@ class TokenData(BaseModel): username: str | None = None -class User(BaseModel): - username: str - email: str | None = None - full_name: str | None = None - disabled: bool | None = None +# class User(BaseModel): +# username: str +# email: str | None = None +# full_name: str | None = None +# disabled: bool | None = None -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") - -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False) def get_password_hash(password: str): return pwd_context.hash(password) -async def delete_user(username): - async with conf.session_maker.begin() as session: - user_in_db: UserInDB | None = await get_user(username) - if user_in_db is None: - raise SystemExit(f'User {username} does not exist in the database') - await session.delete(user_in_db) +async def delete_user(session: AsyncSession, username: str) -> None: + user_in_db: User | None = await get_user(session, username) + if user_in_db is None: + raise SystemExit(f'User {username} does not exist in the database') + await session.delete(user_in_db) -async def enable_user(username, enable=True): - async with conf.session_maker.begin() as session: - user_in_db: UserInDB | None = await get_user(username) - if user_in_db is None: - raise SystemExit(f'User {username} does not exist in the database') - user_in_db.disabled = not enable # type: ignore - session.add(user_in_db) - await session.commit() +async def enable_user(session: AsyncSession, username: str, enable=True): + user_in_db: UserRead | None = await get_user(session, username) + if user_in_db is None: + raise SystemExit(f'User {username} does not exist in the database') + user_in_db.disabled = not enable # type: ignore + session.add(user_in_db) + await session.commit() -async def create_user(username: str, password: str, full_name: str, +async def create_user(session: AsyncSession, username: str, password: str, full_name: str, email: str, **kwargs): - async with conf.session_maker.begin() as session: - user_in_db: UserInDB | None = await get_user(username) - if user_in_db is None: - user = UserInDB( - username=username, - password=get_password_hash(password), - full_name=full_name, - email=email, - disabled=False - ) - session.add(user) - else: - user_in_db.full_name = full_name # type: ignore - user_in_db.email = email # type: ignore - user_in_db.password = get_password_hash(password) # type: ignore - await session.commit() + user_in_db: User | None = await get_user(session, username) + if user_in_db is None: + user = User( + username=username, + password=get_password_hash(password), + full_name=full_name, + email=email, + disabled=False + ) + session.add(user) + else: + user_in_db.full_name = full_name # type: ignore + user_in_db.email = email # type: ignore + user_in_db.password = get_password_hash(password) # type: ignore + await session.commit() -async def get_user(username: str) -> (UserInDB | None): - async with conf.session_maker.begin() as session: - req = await session.execute(select(UserInDB).where(UserInDB.username==username)) - return req.scalar() +async def get_user( + session: AsyncSession, + username: str) -> (User | None): + query = select(User).where(User.username==username).options(selectinload(User.roles)) + data = await session.exec(query) + return data.scalar() -def verify_password(plain_password, hashed_password): - return pwd_context.verify(plain_password, hashed_password) +def verify_password(user: User, plain_password): + try: + return pwd_context.verify(plain_password, user.password) + except UnknownHashError: + logger.warning(f'Password not encrypted in DB for {user.username}, assuming it is stored in plain text') + return plain_password == user.password -async def get_current_user(token: str = Depends(oauth2_scheme)) -> User: +async def get_current_user( + token: str = Depends(oauth2_scheme)) -> UserRead | None: credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) + expired_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token has expired", + headers={"WWW-Authenticate": "Bearer"}, + ) + if token is None: + return None try: - payload = jwt.decode(token, conf.security['secret_key'], algorithms=[ALGORITHM]) + payload = jwt.decode(token, conf.crypto.secret, + algorithms=[conf.crypto.algorithm]) username: str = payload.get("sub", '') if username == '': raise credentials_exception token_data = TokenData(username=username) + except ExpiredSignatureError: + raise expired_exception except JWTError: raise credentials_exception - user = await get_user(username=token_data.username) # type: ignore - if user is None: - raise credentials_exception - return User(username=user.username, # type: ignore - email=user.email, # type: ignore - full_name=user.full_name) # type: ignore + async with db_session() as session: + user = await get_user(session, username=token_data.username) + if user is None: + raise credentials_exception + return user async def authenticate_user(username: str, password: str): - user = await get_user(username) - if not user: - return False - if not verify_password(password, user.password): - return False - return user + async with db_session() as session: + user = await get_user(session, username) + if not user: + return False + if not verify_password(user, password): + return False + return user -async def get_current_active_user(current_user: User = Depends(get_current_user)): - if current_user.disabled: - raise HTTPException(status_code=400, detail="Inactive user") - return current_user +# async def get_current_active_user( +# current_user: Annotated[UserRead, Depends(get_current_user)]): +# if current_user.disabled: +# raise HTTPException(status_code=400, detail="Inactive user") +# return current_user def create_access_token(data: dict, expires_delta: timedelta): @@ -133,6 +151,6 @@ def create_access_token(data: dict, expires_delta: timedelta): expire = datetime.utcnow() + expires_delta to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, - conf.security['secret_key'], - algorithm=ALGORITHM) + conf.crypto.secret, + algorithm=conf.crypto.algorithm) return encoded_jwt