diff --git a/.gitignore b/.gitignore index e4f765a..195c87a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -*/**/__pycache__ *.pyc +*.sqlite3 +__pycache__ diff --git a/Dockerfile b/Dockerfile index d3a5c62..57e1414 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ RUN pip install poetry COPY ./pyproject.toml /app/pyproject.toml COPY ./poetry.lock /app/poetry.lock -RUN poetry install --only main +RUN poetry install --only main --only ui --only backend --all-extras COPY ./soul_diary /app/soul_diary diff --git a/poetry.lock b/poetry.lock index 6e629ae..b3788b7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,39 @@ # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +[[package]] +name = "aiosqlite" +version = "0.19.0" +description = "asyncio bridge to the standard sqlite3 module" +optional = true +python-versions = ">=3.7" +files = [ + {file = "aiosqlite-0.19.0-py3-none-any.whl", hash = "sha256:edba222e03453e094a3ce605db1b970c4b3376264e56f32e2a4959f948d66a96"}, + {file = "aiosqlite-0.19.0.tar.gz", hash = "sha256:95ee77b91c8d2808bd08a59fbebf66270e9090c3d92ffbf260dc0db0b979577d"}, +] + +[package.extras] +dev = ["aiounittest (==1.4.1)", "attribution (==1.6.2)", "black (==23.3.0)", "coverage[toml] (==7.2.3)", "flake8 (==5.0.4)", "flake8-bugbear (==23.3.12)", "flit (==3.7.1)", "mypy (==1.2.0)", "ufmt (==2.1.0)", "usort (==1.0.6)"] +docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"] + +[[package]] +name = "alembic" +version = "1.13.0" +description = "A database migration tool for SQLAlchemy." +optional = false +python-versions = ">=3.8" +files = [ + {file = "alembic-1.13.0-py3-none-any.whl", hash = "sha256:a23974ea301c3ee52705db809c7413cecd165290c6679b9998dd6c74342ca23a"}, + {file = "alembic-1.13.0.tar.gz", hash = "sha256:ab4b3b94d2e1e5f81e34be8a9b7b7575fc9dd5398fccb0bef351ec9b14872623"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" +typing-extensions = ">=4" + +[package.extras] +tz = ["backports.zoneinfo"] + [[package]] name = "annotated-types" version = "0.6.0" @@ -50,6 +84,89 @@ types-python-dateutil = ">=2.8.10" doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] +[[package]] +name = "asyncpg" +version = "0.28.0" +description = "An asyncio PostgreSQL driver" +optional = true +python-versions = ">=3.7.0" +files = [ + {file = "asyncpg-0.28.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a6d1b954d2b296292ddff4e0060f494bb4270d87fb3655dd23c5c6096d16d83"}, + {file = "asyncpg-0.28.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0740f836985fd2bd73dca42c50c6074d1d61376e134d7ad3ad7566c4f79f8184"}, + {file = "asyncpg-0.28.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e907cf620a819fab1737f2dd90c0f185e2a796f139ac7de6aa3212a8af96c050"}, + {file = "asyncpg-0.28.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86b339984d55e8202e0c4b252e9573e26e5afa05617ed02252544f7b3e6de3e9"}, + {file = "asyncpg-0.28.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c402745185414e4c204a02daca3d22d732b37359db4d2e705172324e2d94e85"}, + {file = "asyncpg-0.28.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c88eef5e096296626e9688f00ab627231f709d0e7e3fb84bb4413dff81d996d7"}, + {file = "asyncpg-0.28.0-cp310-cp310-win32.whl", hash = "sha256:90a7bae882a9e65a9e448fdad3e090c2609bb4637d2a9c90bfdcebbfc334bf89"}, + {file = "asyncpg-0.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:76aacdcd5e2e9999e83c8fbcb748208b60925cc714a578925adcb446d709016c"}, + {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-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1c56092465e718a9fdcc726cc3d9dcf3a692e4834031c9a9f871d92a75d20d48"}, + {file = "asyncpg-0.28.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4acd6830a7da0eb4426249d71353e8895b350daae2380cb26d11e0d4a01c5472"}, + {file = "asyncpg-0.28.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63861bb4a540fa033a56db3bb58b0c128c56fad5d24e6d0a8c37cb29b17c1c7d"}, + {file = "asyncpg-0.28.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a93a94ae777c70772073d0512f21c74ac82a8a49be3a1d982e3f259ab5f27307"}, + {file = "asyncpg-0.28.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d14681110e51a9bc9c065c4e7944e8139076a778e56d6f6a306a26e740ed86d2"}, + {file = "asyncpg-0.28.0-cp37-cp37m-win32.whl", hash = "sha256:8aec08e7310f9ab322925ae5c768532e1d78cfb6440f63c078b8392a38aa636a"}, + {file = "asyncpg-0.28.0-cp37-cp37m-win_amd64.whl", hash = "sha256:319f5fa1ab0432bc91fb39b3960b0d591e6b5c7844dafc92c79e3f1bff96abef"}, + {file = "asyncpg-0.28.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b337ededaabc91c26bf577bfcd19b5508d879c0ad009722be5bb0a9dd30b85a0"}, + {file = "asyncpg-0.28.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d32b680a9b16d2957a0a3cc6b7fa39068baba8e6b728f2e0a148a67644578f4"}, + {file = "asyncpg-0.28.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f62f04cdf38441a70f279505ef3b4eadf64479b17e707c950515846a2df197"}, + {file = "asyncpg-0.28.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f20cac332c2576c79c2e8e6464791c1f1628416d1115935a34ddd7121bfc6a4"}, + {file = "asyncpg-0.28.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:59f9712ce01e146ff71d95d561fb68bd2d588a35a187116ef05028675462d5ed"}, + {file = "asyncpg-0.28.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fc9e9f9ff1aa0eddcc3247a180ac9e9b51a62311e988809ac6152e8fb8097756"}, + {file = "asyncpg-0.28.0-cp38-cp38-win32.whl", hash = "sha256:9e721dccd3838fcff66da98709ed884df1e30a95f6ba19f595a3706b4bc757e3"}, + {file = "asyncpg-0.28.0-cp38-cp38-win_amd64.whl", hash = "sha256:8ba7d06a0bea539e0487234511d4adf81dc8762249858ed2a580534e1720db00"}, + {file = "asyncpg-0.28.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d009b08602b8b18edef3a731f2ce6d3f57d8dac2a0a4140367e194eabd3de457"}, + {file = "asyncpg-0.28.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ec46a58d81446d580fb21b376ec6baecab7288ce5a578943e2fc7ab73bf7eb39"}, + {file = "asyncpg-0.28.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b48ceed606cce9e64fd5480a9b0b9a95cea2b798bb95129687abd8599c8b019"}, + {file = "asyncpg-0.28.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8858f713810f4fe67876728680f42e93b7e7d5c7b61cf2118ef9153ec16b9423"}, + {file = "asyncpg-0.28.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5e18438a0730d1c0c1715016eacda6e9a505fc5aa931b37c97d928d44941b4bf"}, + {file = "asyncpg-0.28.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e9c433f6fcdd61c21a715ee9128a3ca48be8ac16fa07be69262f016bb0f4dbd2"}, + {file = "asyncpg-0.28.0-cp39-cp39-win32.whl", hash = "sha256:41e97248d9076bc8e4849da9e33e051be7ba37cd507cbd51dfe4b2d99c70e3dc"}, + {file = "asyncpg-0.28.0-cp39-cp39-win_amd64.whl", hash = "sha256:3ed77f00c6aacfe9d79e9eff9e21729ce92a4b38e80ea99a58ed382f42ebd55b"}, + {file = "asyncpg-0.28.0.tar.gz", hash = "sha256:7252cdc3acb2f52feaa3664280d3bcd78a46bd6c10bfd681acfffefa1120e278"}, +] + +[package.extras] +docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["flake8 (>=5.0,<6.0)", "uvloop (>=0.15.3)"] + +[[package]] +name = "bcrypt" +version = "4.1.1" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "bcrypt-4.1.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:196008d91201bbb1aa4e666fee5e610face25d532e433a560cabb33bfdff958b"}, + {file = "bcrypt-4.1.1-cp37-abi3-macosx_13_0_universal2.whl", hash = "sha256:2e197534c884336f9020c1f3a8efbaab0aa96fc798068cb2da9c671818b7fbb0"}, + {file = "bcrypt-4.1.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d573885b637815a7f3a3cd5f87724d7d0822da64b0ab0aa7f7c78bae534e86dc"}, + {file = "bcrypt-4.1.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab33473f973e8058d1b2df8d6e095d237c49fbf7a02b527541a86a5d1dc4444"}, + {file = "bcrypt-4.1.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fb931cd004a7ad36a89789caf18a54c20287ec1cd62161265344b9c4554fdb2e"}, + {file = "bcrypt-4.1.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:12f40f78dcba4aa7d1354d35acf45fae9488862a4fb695c7eeda5ace6aae273f"}, + {file = "bcrypt-4.1.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2ade10e8613a3b8446214846d3ddbd56cfe9205a7d64742f0b75458c868f7492"}, + {file = "bcrypt-4.1.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f33b385c3e80b5a26b3a5e148e6165f873c1c202423570fdf45fe34e00e5f3e5"}, + {file = "bcrypt-4.1.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:755b9d27abcab678e0b8fb4d0abdebeea1f68dd1183b3f518bad8d31fa77d8be"}, + {file = "bcrypt-4.1.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a7a7b8a87e51e5e8ca85b9fdaf3a5dc7aaf123365a09be7a27883d54b9a0c403"}, + {file = "bcrypt-4.1.1-cp37-abi3-win32.whl", hash = "sha256:3d6c4e0d6963c52f8142cdea428e875042e7ce8c84812d8e5507bd1e42534e07"}, + {file = "bcrypt-4.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:14d41933510717f98aac63378b7956bbe548986e435df173c841d7f2bd0b2de7"}, + {file = "bcrypt-4.1.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:24c2ebd287b5b11016f31d506ca1052d068c3f9dc817160628504690376ff050"}, + {file = "bcrypt-4.1.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:476aa8e8aca554260159d4c7a97d6be529c8e177dbc1d443cb6b471e24e82c74"}, + {file = "bcrypt-4.1.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:12611c4b0a8b1c461646228344784a1089bc0c49975680a2f54f516e71e9b79e"}, + {file = "bcrypt-4.1.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6450538a0fc32fb7ce4c6d511448c54c4ff7640b2ed81badf9898dcb9e5b737"}, + {file = "bcrypt-4.1.1.tar.gz", hash = "sha256:df37f5418d4f1cdcff845f60e747a015389fa4e63703c918330865e06ad80007"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + [[package]] name = "binaryornot" version = "0.4.4" @@ -353,6 +470,77 @@ flet-core = "0.14.0" httpx = ">=0.24.1,<0.25.0" oauthlib = ">=3.2.2,<4.0.0" +[[package]] +name = "greenlet" +version = "3.0.2" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.0.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9acd8fd67c248b8537953cb3af8787c18a87c33d4dcf6830e410ee1f95a63fd4"}, + {file = "greenlet-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:339c0272a62fac7e602e4e6ec32a64ff9abadc638b72f17f6713556ed011d493"}, + {file = "greenlet-3.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38878744926cec29b5cc3654ef47f3003f14bfbba7230e3c8492393fe29cc28b"}, + {file = "greenlet-3.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b3f0497db77cfd034f829678b28267eeeeaf2fc21b3f5041600f7617139e6773"}, + {file = "greenlet-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1a8a08de7f68506a38f9a2ddb26bbd1480689e66d788fcd4b5f77e2d9ecfcc"}, + {file = "greenlet-3.0.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89a6f6ddcbef4000cda7e205c4c20d319488ff03db961d72d4e73519d2465309"}, + {file = "greenlet-3.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c1f647fe5b94b51488b314c82fdda10a8756d650cee8d3cd29f657c6031bdf73"}, + {file = "greenlet-3.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9560c580c896030ff9c311c603aaf2282234643c90d1dec738a1d93e3e53cd51"}, + {file = "greenlet-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2e9c5423046eec21f6651268cb674dfba97280701e04ef23d312776377313206"}, + {file = "greenlet-3.0.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1fd25dfc5879a82103b3d9e43fa952e3026c221996ff4d32a9c72052544835d"}, + {file = "greenlet-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cecfdc950dd25f25d6582952e58521bca749cf3eeb7a9bad69237024308c8196"}, + {file = "greenlet-3.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edf7a1daba1f7c54326291a8cde58da86ab115b78c91d502be8744f0aa8e3ffa"}, + {file = "greenlet-3.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4cf532bf3c58a862196b06947b1b5cc55503884f9b63bf18582a75228d9950e"}, + {file = "greenlet-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e79fb5a9fb2d0bd3b6573784f5e5adabc0b0566ad3180a028af99523ce8f6138"}, + {file = "greenlet-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:006c1028ac0cfcc4e772980cfe73f5476041c8c91d15d64f52482fc571149d46"}, + {file = "greenlet-3.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fefd5eb2c0b1adffdf2802ff7df45bfe65988b15f6b972706a0e55d451bffaea"}, + {file = "greenlet-3.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c0fdb8142742ee68e97c106eb81e7d3e883cc739d9c5f2b28bc38a7bafeb6d1"}, + {file = "greenlet-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:8f8d14a0a4e8c670fbce633d8b9a1ee175673a695475acd838e372966845f764"}, + {file = "greenlet-3.0.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:654b84c9527182036747938b81938f1d03fb8321377510bc1854a9370418ab66"}, + {file = "greenlet-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd5bc4fde0842ff2b9cf33382ad0b4db91c2582db836793d58d174c569637144"}, + {file = "greenlet-3.0.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c27b142a9080bdd5869a2fa7ebf407b3c0b24bd812db925de90e9afe3c417fd6"}, + {file = "greenlet-3.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0df7eed98ea23b20e9db64d46eb05671ba33147df9405330695bcd81a73bb0c9"}, + {file = "greenlet-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb5d60805057d8948065338be6320d35e26b0a72f45db392eb32b70dd6dc9227"}, + {file = "greenlet-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0e28f5233d64c693382f66d47c362b72089ebf8ac77df7e12ac705c9fa1163d"}, + {file = "greenlet-3.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e4bfa752b3688d74ab1186e2159779ff4867644d2b1ebf16db14281f0445377"}, + {file = "greenlet-3.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c42bb589e6e9f9d8bdd79f02f044dff020d30c1afa6e84c0b56d1ce8a324553c"}, + {file = "greenlet-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:b2cedf279ca38ef3f4ed0d013a6a84a7fc3d9495a716b84a5fc5ff448965f251"}, + {file = "greenlet-3.0.2-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:6d65bec56a7bc352bcf11b275b838df618651109074d455a772d3afe25390b7d"}, + {file = "greenlet-3.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0acadbc3f72cb0ee85070e8d36bd2a4673d2abd10731ee73c10222cf2dd4713c"}, + {file = "greenlet-3.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14b5d999aefe9ffd2049ad19079f733c3aaa426190ffecadb1d5feacef8fe397"}, + {file = "greenlet-3.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f27aa32466993c92d326df982c4acccd9530fe354e938d9e9deada563e71ce76"}, + {file = "greenlet-3.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f34a765c5170c0673eb747213a0275ecc749ab3652bdbec324621ed5b2edaef"}, + {file = "greenlet-3.0.2-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:520fcb53a39ef90f5021c77606952dbbc1da75d77114d69b8d7bded4a8e1a813"}, + {file = "greenlet-3.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d1fceb5351ab1601903e714c3028b37f6ea722be6873f46e349a960156c05650"}, + {file = "greenlet-3.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7363756cc439a503505b67983237d1cc19139b66488263eb19f5719a32597836"}, + {file = "greenlet-3.0.2-cp37-cp37m-win32.whl", hash = "sha256:d5547b462b8099b84746461e882a3eb8a6e3f80be46cb6afb8524eeb191d1a30"}, + {file = "greenlet-3.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:950e21562818f9c771989b5b65f990e76f4ac27af66e1bb34634ae67886ede2a"}, + {file = "greenlet-3.0.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d64643317e76b4b41fdba659e7eca29634e5739b8bc394eda3a9127f697ed4b0"}, + {file = "greenlet-3.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f9ea7c2c9795549653b6f7569f6bc75d2c7d1f6b2854eb8ce0bc6ec3cb2dd88"}, + {file = "greenlet-3.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db4233358d3438369051a2f290f1311a360d25c49f255a6c5d10b5bcb3aa2b49"}, + {file = "greenlet-3.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bf77b41798e8417657245b9f3649314218a4a17aefb02bb3992862df32495"}, + {file = "greenlet-3.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d0df07a38e41a10dfb62c6fc75ede196572b580f48ee49b9282c65639f3965"}, + {file = "greenlet-3.0.2-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10d247260db20887ae8857c0cbc750b9170f0b067dd7d38fb68a3f2334393bd3"}, + {file = "greenlet-3.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a37ae53cca36823597fd5f65341b6f7bac2dd69ecd6ca01334bb795460ab150b"}, + {file = "greenlet-3.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:80d068e4b6e2499847d916ef64176811ead6bf210a610859220d537d935ec6fd"}, + {file = "greenlet-3.0.2-cp38-cp38-win32.whl", hash = "sha256:b1405614692ac986490d10d3e1a05e9734f473750d4bee3cf7d1286ef7af7da6"}, + {file = "greenlet-3.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8756a94ed8f293450b0e91119eca2a36332deba69feb2f9ca410d35e74eae1e4"}, + {file = "greenlet-3.0.2-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:2c93cd03acb1499ee4de675e1a4ed8eaaa7227f7949dc55b37182047b006a7aa"}, + {file = "greenlet-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1dac09e3c0b78265d2e6d3cbac2d7c48bd1aa4b04a8ffeda3adde9f1688df2c3"}, + {file = "greenlet-3.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ee59c4627c8c4bb3e15949fbcd499abd6b7f4ad9e0bfcb62c65c5e2cabe0ec4"}, + {file = "greenlet-3.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18fe39d70d482b22f0014e84947c5aaa7211fb8e13dc4cc1c43ed2aa1db06d9a"}, + {file = "greenlet-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84bef3cfb6b6bfe258c98c519811c240dbc5b33a523a14933a252e486797c90"}, + {file = "greenlet-3.0.2-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aecea0442975741e7d69daff9b13c83caff8c13eeb17485afa65f6360a045765"}, + {file = "greenlet-3.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f260e6c2337871a52161824058923df2bbddb38bc11a5cbe71f3474d877c5bd9"}, + {file = "greenlet-3.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fc14dd9554f88c9c1fe04771589ae24db76cd56c8f1104e4381b383d6b71aff8"}, + {file = "greenlet-3.0.2-cp39-cp39-win32.whl", hash = "sha256:bfcecc984d60b20ffe30173b03bfe9ba6cb671b0be1e95c3e2056d4fe7006590"}, + {file = "greenlet-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:c235131bf59d2546bb3ebaa8d436126267392f2e51b85ff45ac60f3a26549af0"}, + {file = "greenlet-3.0.2.tar.gz", hash = "sha256:1c1129bc47266d83444c85a8e990ae22688cf05fb20d7951fd2866007c2ba9bc"}, +] + +[package.extras] +docs = ["Sphinx"] +test = ["objgraph", "psutil"] + [[package]] name = "h11" version = "0.14.0" @@ -436,6 +624,25 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "mako" +version = "1.3.0" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Mako-1.3.0-py3-none-any.whl", hash = "sha256:57d4e997349f1a92035aa25c17ace371a4213f2ca42f99bee9a602500cfd54d9"}, + {file = "Mako-1.3.0.tar.gz", hash = "sha256:e3a9d388fd00e87043edbe8792f45880ac0114e9c4adc69f6e9bfb2c55e3b11b"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -540,6 +747,89 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "multidict" +version = "6.0.4" +description = "multidict implementation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, + {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, + {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, + {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, + {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, + {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, + {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, + {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, + {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, + {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, + {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, + {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, + {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, +] + [[package]] name = "oauthlib" version = "3.2.2" @@ -987,6 +1277,93 @@ files = [ {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, ] +[[package]] +name = "sqlalchemy" +version = "2.0.23" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-win32.whl", hash = "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8"}, + {file = "SQLAlchemy-2.0.23-cp310-cp310-win_amd64.whl", hash = "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6"}, + {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-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:14aebfe28b99f24f8a4c1346c48bc3d63705b1f919a24c27471136d2f219f02d"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e983fa42164577d073778d06d2cc5d020322425a509a08119bdcee70ad856bf"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e0dc9031baa46ad0dd5a269cb7a92a73284d1309228be1d5935dac8fb3cae24"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5f94aeb99f43729960638e7468d4688f6efccb837a858b34574e01143cf11f89"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:63bfc3acc970776036f6d1d0e65faa7473be9f3135d37a463c5eba5efcdb24c8"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-win32.whl", hash = "sha256:f48ed89dd11c3c586f45e9eec1e437b355b3b6f6884ea4a4c3111a3358fd0c18"}, + {file = "SQLAlchemy-2.0.23-cp37-cp37m-win_amd64.whl", hash = "sha256:1e018aba8363adb0599e745af245306cb8c46b9ad0a6fc0a86745b6ff7d940fc"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-win32.whl", hash = "sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306"}, + {file = "SQLAlchemy-2.0.23-cp38-cp38-win_amd64.whl", hash = "sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-win32.whl", hash = "sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884"}, + {file = "SQLAlchemy-2.0.23-cp39-cp39-win_amd64.whl", hash = "sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b"}, + {file = "SQLAlchemy-2.0.23-py3-none-any.whl", hash = "sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d"}, + {file = "SQLAlchemy-2.0.23.tar.gz", hash = "sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "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" + +[package.extras] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx-oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3-binary"] + [[package]] name = "starlette" version = "0.27.0" @@ -1226,7 +1603,114 @@ files = [ {file = "websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016"}, ] +[[package]] +name = "yarl" +version = "1.9.4" +description = "Yet another URL library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, + {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, + {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, + {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, + {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, + {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, + {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, + {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, + {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, + {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, + {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, + {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, + {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, + {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, + {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, + {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[extras] +postgres = ["asyncpg"] +sqlite = ["aiosqlite"] + [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "10427d6e72deac4ef97a9541f0be6c56cf626d1547ba12258fc3af5e4087e728" +content-hash = "2edc77f8a1e7fc20a3330be55ee660506982acfa0ace143d3b5fc41c3b813e5d" diff --git a/pyproject.toml b/pyproject.toml index 2e97f18..974c0dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,17 +9,35 @@ include = [ ] [tool.poetry.dependencies] +# extras +aiosqlite = { version = "^0.19.0", optional = true } +asyncpg = { version = "^0.28.0", optional = true } + +# main python = "^3.11" -flet = "^0.14.0" -flet-fastapi = "^0.14.0" uvicorn = "^0.24.0.post1" facet = "^0.9.1" -flet-route = "^0.3.2" pydantic = "^2.5.2" -fastapi = "0.101.1" typer = "^0.9.0" pydantic-settings = "^2.1.0" + +[tool.poetry.extras] +sqlite = ["aiosqlite"] +postgres = ["asyncpg"] + +[tool.poetry.group.backend.dependencies] +fastapi = "0.101.1" +sqlalchemy = "^2.0.23" +alembic = "^1.13.0" +bcrypt = "^4.1.1" + +[tool.poetry.group.ui.dependencies] +flet = "^0.14.0" +flet-route = "^0.3.2" +flet-fastapi = "^0.14.0" pycryptodomex = "^3.19.0" +yarl = "^1.9.4" +httpx = "0.24.1" [build-system] requires = ["poetry-core"] diff --git a/soul_diary/api/__init__.py b/soul_diary/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/soul_diary/backend/__init__.py b/soul_diary/backend/__init__.py new file mode 100644 index 0000000..e482d45 --- /dev/null +++ b/soul_diary/backend/__init__.py @@ -0,0 +1,2 @@ +from .cli import get_cli +from .service import BackendService, get_service diff --git a/soul_diary/backend/__main__.py b/soul_diary/backend/__main__.py new file mode 100644 index 0000000..c148b10 --- /dev/null +++ b/soul_diary/backend/__main__.py @@ -0,0 +1,7 @@ +from .cli import get_cli + + +if __name__ == "__main__": + cli = get_cli() + + cli() diff --git a/soul_diary/backend/api/__init__.py b/soul_diary/backend/api/__init__.py new file mode 100644 index 0000000..f065755 --- /dev/null +++ b/soul_diary/backend/api/__init__.py @@ -0,0 +1,2 @@ +from .cli import get_cli +from .service import APIService, get_service diff --git a/soul_diary/backend/api/cli.py b/soul_diary/backend/api/cli.py new file mode 100644 index 0000000..90a3955 --- /dev/null +++ b/soul_diary/backend/api/cli.py @@ -0,0 +1,13 @@ +import typer + + +def run(): + pass + + +def get_cli() -> typer.Typer: + cli = typer.Typer() + + cli.command(name="run")(run) + + return cli diff --git a/soul_diary/backend/api/dependencies.py b/soul_diary/backend/api/dependencies.py new file mode 100644 index 0000000..a8a2c59 --- /dev/null +++ b/soul_diary/backend/api/dependencies.py @@ -0,0 +1,31 @@ +import fastapi +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer + +from soul_diary.backend.api.exceptions import HTTPNotAuthenticated +from soul_diary.backend.api.settings import APISettings +from soul_diary.backend.database import DatabaseService +from soul_diary.backend.database.models import Session + + +async def database(request: fastapi.Request) -> DatabaseService: + return request.app.service.database + + +async def settings(request: fastapi.Request) -> APISettings: + return request.app.service.settings + + +async def is_auth( + database: DatabaseService = fastapi.Depends(database), + credentials: HTTPAuthorizationCredentials = fastapi.Depends(HTTPBearer()), +) -> Session: + async with database.transaction() as session: + user_session = await database.get_user_session( + session=session, + token=credentials.credentials, + ) + + if user_session is None: + raise HTTPNotAuthenticated() + + return user_session diff --git a/soul_diary/backend/api/exceptions.py b/soul_diary/backend/api/exceptions.py new file mode 100644 index 0000000..d179c8b --- /dev/null +++ b/soul_diary/backend/api/exceptions.py @@ -0,0 +1,41 @@ +import fastapi + + +class HTTPRegistrationNotSupported(fastapi.HTTPException): + def __init__(self): + super().__init__( + status_code=fastapi.status.HTTP_400_BAD_REQUEST, + detail="Registration not supported.", + ) + + +class HTTPUserAlreadyExists(fastapi.HTTPException): + def __init__(self): + super().__init__( + status_code=fastapi.status.HTTP_400_BAD_REQUEST, + detail="User already exists.", + ) + + +class HTTPNotAuthenticated(fastapi.HTTPException): + def __init__(self): + super().__init__( + status_code=fastapi.status.HTTP_401_UNAUTHORIZED, + detail="Not authenticated.", + ) + + +class HTTPForbidden(fastapi.HTTPException): + def __init__(self): + super().__init__( + status_code=fastapi.status.HTTP_403_FORBIDDEN, + detail="Forbidden.", + ) + + +class HTTPNotFound(fastapi.HTTPException): + def __init__(self): + super().__init__( + status_code=fastapi.status.HTTP_404_NOT_FOUND, + detail="Not found.", + ) diff --git a/soul_diary/backend/api/handlers.py b/soul_diary/backend/api/handlers.py new file mode 100644 index 0000000..c873a80 --- /dev/null +++ b/soul_diary/backend/api/handlers.py @@ -0,0 +1,63 @@ +import fastapi +from sqlalchemy.exc import IntegrityError + +from soul_diary.backend.api.settings import APISettings +from soul_diary.backend.api.dependencies import database, is_auth, settings +from soul_diary.backend.database import DatabaseService +from soul_diary.backend.database.models import Session +from .exceptions import ( + HTTPNotAuthenticated, + HTTPRegistrationNotSupported, + HTTPUserAlreadyExists, +) +from .schemas import CredentialsRequest, OptionsResponse, TokenResponse + + +async def options(settings: APISettings = fastapi.Depends(settings)) -> OptionsResponse: + return OptionsResponse(registration_enabled=settings.registration_enabled) + + +async def sign_up( + data: CredentialsRequest = fastapi.Body(...), + settings: APISettings = fastapi.Depends(settings), + database: DatabaseService = fastapi.Depends(database), +) -> TokenResponse: + if not settings.registration_enabled: + raise HTTPRegistrationNotSupported() + + try: + async with database.transaction() as session: + user = await database.create_user( + session=session, + username=data.username, + password=data.password, + ) + except IntegrityError: + raise HTTPUserAlreadyExists() + user_session = user.sessions[0] + + return TokenResponse(token=user_session.token) + + +async def sign_in( + data: CredentialsRequest = fastapi.Body(...), + database: DatabaseService = fastapi.Depends(database), +) -> TokenResponse: + async with database.transaction() as session: + user_session = await database.auth_user( + session=session, + username=data.username, + password=data.password, + ) + if user_session is None: + raise HTTPNotAuthenticated() + + return TokenResponse(token=user_session.token) + + +async def logout( + database: DatabaseService = fastapi.Depends(database), + user_session: Session = fastapi.Depends(is_auth), +): + async with database.transaction() as session: + await database.logout_user(session=session, user_session=user_session) diff --git a/soul_diary/backend/api/router.py b/soul_diary/backend/api/router.py new file mode 100644 index 0000000..3276ff2 --- /dev/null +++ b/soul_diary/backend/api/router.py @@ -0,0 +1,15 @@ +import fastapi + +from . import handlers, senses +from .dependencies import database + + +router = fastapi.APIRouter( + dependencies=[fastapi.Depends(database)], +) + +router.add_api_route(path="/signup", methods=["POST"], endpoint=handlers.sign_up) +router.add_api_route(path="/signin", methods=["POST"], endpoint=handlers.sign_in) +router.add_api_route(path="/logout", methods=["POST"], endpoint=handlers.logout) +router.add_api_route(path="/options", methods=["GET"], endpoint=handlers.options) +router.include_router(senses.router, prefix="/senses", tags=["Senses"]) diff --git a/soul_diary/backend/api/schemas.py b/soul_diary/backend/api/schemas.py new file mode 100644 index 0000000..d4ca143 --- /dev/null +++ b/soul_diary/backend/api/schemas.py @@ -0,0 +1,14 @@ +from pydantic import BaseModel, constr + + +class CredentialsRequest(BaseModel): + username: constr(min_length=4, max_length=64, strip_whitespace=True) + password: constr(min_length=8, max_length=64) + + +class TokenResponse(BaseModel): + token: constr(min_length=32, max_length=32) + + +class OptionsResponse(BaseModel): + registration_enabled: bool diff --git a/soul_diary/backend/api/senses/__init__.py b/soul_diary/backend/api/senses/__init__.py new file mode 100644 index 0000000..2378043 --- /dev/null +++ b/soul_diary/backend/api/senses/__init__.py @@ -0,0 +1 @@ +from .router import router diff --git a/soul_diary/backend/api/senses/dependencies.py b/soul_diary/backend/api/senses/dependencies.py new file mode 100644 index 0000000..e4e140f --- /dev/null +++ b/soul_diary/backend/api/senses/dependencies.py @@ -0,0 +1,24 @@ +import uuid + +import fastapi + +from soul_diary.backend.api.dependencies import database, is_auth +from soul_diary.backend.api.exceptions import HTTPForbidden, HTTPNotFound +from soul_diary.backend.database import DatabaseService +from soul_diary.backend.database.models import Sense, Session + + +async def sense( + database: DatabaseService = fastapi.Depends(database), + user_session: Session = fastapi.Depends(is_auth), + sense_id: uuid.UUID = fastapi.Path(), +) -> Sense: + async with database.transaction() as session: + sense = await database.get_sense(session=session, sense_id=sense_id) + + if sense is None: + raise HTTPNotFound() + if sense.user_id != user_session.user_id: + raise HTTPForbidden() + + return sense diff --git a/soul_diary/backend/api/senses/handlers.py b/soul_diary/backend/api/senses/handlers.py new file mode 100644 index 0000000..42916a7 --- /dev/null +++ b/soul_diary/backend/api/senses/handlers.py @@ -0,0 +1,71 @@ +import fastapi + +from soul_diary.backend.api.dependencies import database +from soul_diary.backend.database import DatabaseService +from soul_diary.backend.database.models import Sense, Session +from .dependencies import is_auth, sense +from .schemas import ( + CreateSenseRequest, + Pagination, + SenseListResponse, + SenseResponse, + UpdateSenseRequest, +) + + +async def get_sense_list( + database: DatabaseService = fastapi.Depends(database), + user_session: Session = fastapi.Depends(is_auth), + pagination: Pagination = fastapi.Depends(Pagination), +) -> SenseListResponse: + async with database.transaction() as session: + senses = await database.get_sense_list( + session=session, + user=user_session.user, + page=pagination.page, + limit=pagination.limit, + ) + + return SenseListResponse(data=senses) + + +async def create_sense( + database: DatabaseService = fastapi.Depends(database), + user_session: Session = fastapi.Depends(is_auth), + data: CreateSenseRequest = fastapi.Body(), +) -> SenseResponse: + async with database.transaction() as session: + sense = await database.create_sense( + session=session, + user=user_session.user, + data=data.data, + ) + + return SenseResponse.model_validate(sense) + + +async def get_sense(sense: Sense = fastapi.Depends(sense)) -> SenseResponse: + return SenseResponse.model_validate(sense) + + +async def update_sense( + database: DatabaseService = fastapi.Depends(database), + sense: Sense = fastapi.Depends(sense), + data: UpdateSenseRequest = fastapi.Body(), +) -> SenseResponse: + async with database.transaction() as session: + sense = await database.update_sense( + session=session, + sense=sense, + data=data.data, + ) + + return SenseResponse.model_validate(sense) + + +async def delete_sense( + database: DatabaseService = fastapi.Depends(database), + sense: Sense = fastapi.Depends(sense), +): + async with database.transaction() as session: + await database.delete_sense(session=session, sense=sense) diff --git a/soul_diary/backend/api/senses/router.py b/soul_diary/backend/api/senses/router.py new file mode 100644 index 0000000..105397f --- /dev/null +++ b/soul_diary/backend/api/senses/router.py @@ -0,0 +1,11 @@ +import fastapi + +from . import handlers + +router = fastapi.APIRouter() + +router.add_api_route(path="/", methods=["GET"], endpoint=handlers.get_sense_list) +router.add_api_route(path="/", methods=["POST"], endpoint=handlers.create_sense) +router.add_api_route(path="/{sense_id}", methods=["GET"], endpoint=handlers.get_sense) +router.add_api_route(path="/{sense_id}", methods=["POST"], endpoint=handlers.update_sense) +router.add_api_route(path="/{sense_id}", methods=["DELETE"], endpoint=handlers.delete_sense) diff --git a/soul_diary/backend/api/senses/schemas.py b/soul_diary/backend/api/senses/schemas.py new file mode 100644 index 0000000..d818612 --- /dev/null +++ b/soul_diary/backend/api/senses/schemas.py @@ -0,0 +1,29 @@ +import uuid +from datetime import datetime + +from pydantic import AwareDatetime, BaseModel, ConfigDict, PositiveInt + + +class CreateSenseRequest(BaseModel): + data: str + + +class UpdateSenseRequest(BaseModel): + data: str + + +class SenseResponse(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: uuid.UUID + data: str + created_at: datetime + + +class Pagination(BaseModel): + page: PositiveInt = 1 + limit: PositiveInt = 10 + + +class SenseListResponse(BaseModel): + data: list[SenseResponse] diff --git a/soul_diary/backend/api/service.py b/soul_diary/backend/api/service.py new file mode 100644 index 0000000..6162a49 --- /dev/null +++ b/soul_diary/backend/api/service.py @@ -0,0 +1,62 @@ +import fastapi +import uvicorn +from facet import ServiceMixin +from fastapi.middleware.cors import CORSMiddleware + +from soul_diary.backend.database import DatabaseService, get_service as get_database_service +from . import router +from .settings import APISettings + + +class UvicornServer(uvicorn.Server): + def install_signal_handlers(self): + pass + + +class APIService(ServiceMixin): + def __init__(self, database: DatabaseService, settings: APISettings, port: int = 8001): + self._database = database + self._settings = settings + + self._port = port + + @property + def database(self) -> DatabaseService: + return self._database + + @property + def settings(self) -> APISettings: + return self._settings + + def get_app(self) -> fastapi.FastAPI: + app = fastapi.FastAPI() + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + app.service = self + self.setup_app(app=app) + + return app + + def setup_app(self, app: fastapi.FastAPI): + app.include_router(router.router) + + async def start(self): + config = uvicorn.Config(app=self.get_app(), host="0.0.0.0", port=self._port) + server = UvicornServer(config) + + self.add_task(server.serve()) + + +def get_service() -> APIService: + database_service = get_database_service() + settings = APISettings() + return APIService( + database=database_service, + settings=settings, + port=settings.port, + ) diff --git a/soul_diary/backend/api/settings.py b/soul_diary/backend/api/settings.py new file mode 100644 index 0000000..bdc0ce8 --- /dev/null +++ b/soul_diary/backend/api/settings.py @@ -0,0 +1,10 @@ +from pydantic import conint +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class APISettings(BaseSettings): + model_config = SettingsConfigDict(prefix="backend_api_") + + port: conint(ge=1, le=65535) = 8001 + + registration_enabled: bool = True diff --git a/soul_diary/backend/cli.py b/soul_diary/backend/cli.py new file mode 100644 index 0000000..17daf60 --- /dev/null +++ b/soul_diary/backend/cli.py @@ -0,0 +1,22 @@ +import asyncio + +import typer + +from . import api, database +from .service import get_service + + +def run(): + backend_service = get_service() + + asyncio.run(backend_service.run()) + + +def get_cli() -> typer.Typer: + cli = typer.Typer() + + cli.add_typer(api.get_cli(), name="api") + cli.add_typer(database.get_cli(), name="database") + cli.command(name="run")(run) + + return cli diff --git a/soul_diary/backend/database/__init__.py b/soul_diary/backend/database/__init__.py new file mode 100644 index 0000000..f0e05d0 --- /dev/null +++ b/soul_diary/backend/database/__init__.py @@ -0,0 +1,2 @@ +from .cli import get_cli +from .service import DatabaseService, get_service diff --git a/soul_diary/backend/database/cli.py b/soul_diary/backend/database/cli.py new file mode 100644 index 0000000..5b9322a --- /dev/null +++ b/soul_diary/backend/database/cli.py @@ -0,0 +1,67 @@ +from typing import Optional + +import typer + +from .service import DatabaseService, get_service + + +def migrations_list(ctx: typer.Context): + database_service: DatabaseService = ctx.obj["database"] + + database_service.show_migrations() + + +def migrations_apply(ctx: typer.Context): + database_service: DatabaseService = ctx.obj["database"] + + database_service.migrate() + + +def migrations_rollback( + ctx: typer.Context, + revision: Optional[str] = typer.Argument( + None, + help="Revision id or relative revision (`-1`, `-2`)", + ), +): + database_service: DatabaseService = ctx.obj["database"] + + database_service.rollback(revision=revision) + + +def migrations_create( + ctx: typer.Context, + message: Optional[str] = typer.Option( + None, + "-m", "--message", + help="Migration short message", + ), +): + database_service: DatabaseService = ctx.obj["database"] + + database_service.create_migration(message=message) + + +def get_migrations_cli() -> typer.Typer: + cli = typer.Typer(name="Migration") + + cli.command(name="apply")(migrations_apply) + cli.command(name="rollback")(migrations_rollback) + cli.command(name="create")(migrations_create) + cli.command(name="list")(migrations_list) + + return cli + + +def service_callback(ctx: typer.Context): + ctx.obj = ctx.obj or {} + ctx.obj["database"] = get_service() + + +def get_cli() -> typer.Typer: + cli = typer.Typer() + + cli.callback()(service_callback) + cli.add_typer(get_migrations_cli(), name="migrations") + + return cli diff --git a/soul_diary/backend/database/migrations/env.py b/soul_diary/backend/database/migrations/env.py new file mode 100644 index 0000000..19b550d --- /dev/null +++ b/soul_diary/backend/database/migrations/env.py @@ -0,0 +1,91 @@ +import asyncio +from logging.config import fileConfig + +from sqlalchemy import pool +from sqlalchemy.engine import Connection +from sqlalchemy.ext.asyncio import async_engine_from_config + +from alembic import context + +from soul_diary.backend.database import models + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = models.Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection: Connection) -> None: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +async def run_async_migrations() -> None: + """In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + connectable = async_engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + + await connectable.dispose() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode.""" + + asyncio.run(run_async_migrations()) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/soul_diary/backend/database/migrations/script.py.mako b/soul_diary/backend/database/migrations/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/soul_diary/backend/database/migrations/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/soul_diary/backend/database/migrations/versions/bce1e66bb101_init.py b/soul_diary/backend/database/migrations/versions/bce1e66bb101_init.py new file mode 100644 index 0000000..3d9153b --- /dev/null +++ b/soul_diary/backend/database/migrations/versions/bce1e66bb101_init.py @@ -0,0 +1,66 @@ +"""init + +Revision ID: bce1e66bb101 +Revises: +Create Date: 2023-12-14 10:47:06.594959 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'bce1e66bb101' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('users', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('username', sa.String(length=64), nullable=False), + sa.Column('password', sa.String(length=72), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('username') + ) + op.create_index('users__id_idx', 'users', ['id'], unique=False, postgresql_using='hash') + op.create_index('users__username_idx', 'users', ['username'], unique=False, postgresql_using='hash') + op.create_table('senses', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.Column('data', sa.String(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('senses__created_at_idx', 'senses', ['created_at'], unique=False, postgresql_using='btree') + op.create_index('senses__id_idx', 'senses', ['id'], unique=False, postgresql_using='hash') + op.create_index('senses__user_id_idx', 'senses', ['user_id'], unique=False, postgresql_using='btree') + op.create_table('sessions', + sa.Column('token', sa.String(), nullable=False), + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('token') + ) + op.create_index('sessions__token_idx', 'sessions', ['token'], unique=False, postgresql_using='hash') + op.create_index('sessions__user_id_idx', 'sessions', ['user_id'], unique=False, postgresql_using='btree') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('sessions__user_id_idx', table_name='sessions', postgresql_using='btree') + op.drop_index('sessions__token_idx', table_name='sessions', postgresql_using='hash') + op.drop_table('sessions') + op.drop_index('senses__user_id_idx', table_name='senses', postgresql_using='btree') + op.drop_index('senses__id_idx', table_name='senses', postgresql_using='hash') + op.drop_index('senses__created_at_idx', table_name='senses', postgresql_using='btree') + op.drop_table('senses') + op.drop_index('users__username_idx', table_name='users', postgresql_using='hash') + op.drop_index('users__id_idx', table_name='users', postgresql_using='hash') + op.drop_table('users') + # ### end Alembic commands ### diff --git a/soul_diary/backend/database/models.py b/soul_diary/backend/database/models.py new file mode 100644 index 0000000..b5c03d7 --- /dev/null +++ b/soul_diary/backend/database/models.py @@ -0,0 +1,61 @@ +import random +import string +import uuid +from datetime import datetime + +from sqlalchemy import ForeignKey, Index, String +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship + + +class Base(DeclarativeBase): + pass + + +class User(Base): + __tablename__ = "users" + + id: Mapped[uuid.UUID] = mapped_column(default=uuid.uuid4, primary_key=True) + username: Mapped[str] = mapped_column(String(64), unique=True) + password: Mapped[str] = mapped_column(String(72)) + + senses: Mapped[list["Sense"]] = relationship(back_populates="user") + sessions: Mapped[list["Session"]] = relationship(back_populates="user") + + __table_args__ = ( + Index("users__id_idx", "id", postgresql_using="hash"), + Index("users__username_idx", "username", postgresql_using="hash"), + ) + + +class Session(Base): + __tablename__ = "sessions" + + token: Mapped[str] = mapped_column( + primary_key=True, + default=lambda: "".join(random.choice(string.hexdigits) for _ in range(32)), + ) + user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id")) + + user: Mapped[User] = relationship(back_populates="sessions", lazy=False) + + __table_args__ = ( + Index("sessions__token_idx", "token", postgresql_using="hash"), + Index("sessions__user_id_idx", "user_id", postgresql_using="btree"), + ) + + +class Sense(Base): + __tablename__ = "senses" + + id: Mapped[uuid.UUID] = mapped_column(default=uuid.uuid4, primary_key=True) + user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id")) + data: Mapped[str] + created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow) + + user: Mapped[User] = relationship(back_populates="senses", lazy=False) + + __table_args__ = ( + Index("senses__id_idx", "id", postgresql_using="hash"), + Index("senses__user_id_idx", "user_id", postgresql_using="btree"), + Index("senses__created_at_idx", "created_at", postgresql_using="btree"), + ) diff --git a/soul_diary/backend/database/service.py b/soul_diary/backend/database/service.py new file mode 100644 index 0000000..13cb44d --- /dev/null +++ b/soul_diary/backend/database/service.py @@ -0,0 +1,144 @@ +import pathlib +import uuid +from contextlib import asynccontextmanager +from typing import Type + +import bcrypt +from alembic import command as alembic_command +from alembic.config import Config as AlembicConfig +from facet import ServiceMixin +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy.orm import DeclarativeBase + +from .models import Sense, Session, User +from .settings import DatabaseSettings + + +class DatabaseService(ServiceMixin): + def __init__(self, dsn: str): + self._dsn = dsn + self._engine = create_async_engine(self._dsn, pool_recycle=60) + self._sessionmaker = async_sessionmaker(self._engine, expire_on_commit=False) + + def get_alembic_config(self) -> AlembicConfig: + migrations_path = pathlib.Path(__file__).parent / "migrations" + + config = AlembicConfig() + config.set_main_option("script_location", str(migrations_path)) + config.set_main_option("sqlalchemy.url", self._dsn.replace("%", "%%")) + + return config + + def get_models(self) -> list[Type[DeclarativeBase]]: + return [User, Sense] + + @asynccontextmanager + async def transaction(self): + async with self._sessionmaker() as session: + async with session.begin(): + yield session + + def migrate(self): + alembic_command.upgrade(self.get_alembic_config(), "head") + + def rollback(self, revision: str | None = None): + revision = revision or "-1" + + alembic_command.downgrade(self.get_alembic_config(), revision) + + def show_migrations(self): + alembic_command.history(self.get_alembic_config()) + + def create_migration(self, message: str | None = None): + alembic_command.revision( + self.get_alembic_config(), message=message, autogenerate=True, + ) + + async def create_user(self, session: AsyncSession, username: str, password: str) -> User: + hashed_password = bcrypt.hashpw( + password.encode("utf-8"), + bcrypt.gensalt(), + ).decode("utf-8") + user = User(username=username, password=hashed_password) + user_session = Session(user=user) + user.sessions.append(user_session) + + session.add_all([user, user_session]) + + return user + + async def auth_user( + self, + session: AsyncSession, + username: str, + password: str, + ) -> Session | None: + query = select(User).where(User.username == username) + + result = await session.execute(query) + user = result.scalar_one_or_none() + if user is None: + return None + if not bcrypt.checkpw(password.encode("utf-8"), user.password.encode("utf-8")): + return None + + user_session = Session(user=user) + session.add(user_session) + + return user_session + + async def logout_user(self, session: AsyncSession, user_session: Session): + await session.delete(user_session) + + async def get_user_session(self, session: AsyncSession, token: str) -> Session | None: + query = select(Session).where(Session.token == token) + + result = await session.execute(query) + user_session = result.scalar_one_or_none() + + return user_session + + async def get_sense_list( + self, + session: AsyncSession, + user: User, + page: int = 1, + limit: int = 10, + ) -> list[Sense]: + query = select(Sense).where(Sense.user == user).limit(limit).offset((page - 1) * limit) + + result = await session.execute(query) + senses = result.scalars().all() + + return list(senses) + + async def create_sense(self, session: AsyncSession, user: User, data: str) -> Sense: + sense = Sense(user=user, data=data) + + session.add(sense) + + return sense + + async def get_sense(self, session: AsyncSession, sense_id: uuid.UUID) -> Sense | None: + query = select(Sense).where(Sense.id == sense_id) + + result = await session.execute(query) + sense = result.scalar_one_or_none() + + return sense + + async def update_sense(self, session: AsyncSession, sense: Sense, data: str) -> Sense: + sense.data = data + + session.add(sense) + + return sense + + async def delete_sense(self, session: AsyncSession, sense: Sense): + await session.delete(sense) + + +def get_service() -> DatabaseService: + settings = DatabaseSettings() + return DatabaseService(dsn=str(settings.dsn)) diff --git a/soul_diary/backend/database/settings.py b/soul_diary/backend/database/settings.py new file mode 100644 index 0000000..f6391d8 --- /dev/null +++ b/soul_diary/backend/database/settings.py @@ -0,0 +1,8 @@ +from pydantic import AnyUrl +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class DatabaseSettings(BaseSettings): + model_config = SettingsConfigDict(prefix="backend_database_") + + dsn: AnyUrl = "sqlite+aiosqlite:///soul_diary.sqlite3" diff --git a/soul_diary/backend/service.py b/soul_diary/backend/service.py new file mode 100644 index 0000000..8f557d5 --- /dev/null +++ b/soul_diary/backend/service.py @@ -0,0 +1,19 @@ +from facet import ServiceMixin + +from .api import APIService, get_service as get_api_service + + +class BackendService(ServiceMixin): + def __init__(self, api: APIService): + self._api = api + + @property + def dependencies(self) -> list[ServiceMixin]: + return [ + self._api, + ] + + +def get_service() -> BackendService: + api_service = get_api_service() + return BackendService(api=api_service) diff --git a/soul_diary/cli.py b/soul_diary/cli.py index fea7210..6e7923f 100644 --- a/soul_diary/cli.py +++ b/soul_diary/cli.py @@ -2,18 +2,20 @@ import asyncio import typer -from . import ui +from . import backend, ui +from .service import get_service def run(): - ui_service = ui.get_service() + soul_diary_service = get_service() - asyncio.run(ui_service.run()) + asyncio.run(soul_diary_service.run()) def get_cli() -> typer.Typer: cli = typer.Typer() + cli.add_typer(backend.get_cli(), name="backend") cli.add_typer(ui.get_cli(), name="ui") cli.command(name="run")(run) diff --git a/soul_diary/service.py b/soul_diary/service.py new file mode 100644 index 0000000..5c5078e --- /dev/null +++ b/soul_diary/service.py @@ -0,0 +1,23 @@ +from facet import ServiceMixin + +from .backend import BackendService, get_service as get_backend_service +from .ui import UIService, get_service as get_ui_service + + +class SoulDiaryService(ServiceMixin): + def __init__(self, backend: BackendService, ui: UIService): + self._backend = backend + self._ui = ui + + @property + def dependencies(self) -> list[ServiceMixin]: + return [ + self._backend, + self._ui, + ] + + +def get_service() -> SoulDiaryService: + backend_service = get_backend_service() + ui_service = get_ui_service() + return SoulDiaryService(backend=backend_service, ui=ui_service) diff --git a/soul_diary/ui/app/backend/base.py b/soul_diary/ui/app/backend/base.py index 9b6609f..71496aa 100644 --- a/soul_diary/ui/app/backend/base.py +++ b/soul_diary/ui/app/backend/base.py @@ -100,9 +100,11 @@ class BaseBackend: ) async def logout(self): + await self.deauth() self._token = None self._encryption_key = None self._username = None + await self._local_storage.remove_auth_data() @property def is_auth(self) -> bool: @@ -167,6 +169,9 @@ class BaseBackend: async def auth(self, username: str, password: str) -> str: raise NotImplementedError + async def deauth(self): + raise NotImplementedError + async def get_options(self) -> Options: raise NotImplementedError diff --git a/soul_diary/ui/app/backend/exceptions.py b/soul_diary/ui/app/backend/exceptions.py index a5ba4cd..502a057 100644 --- a/soul_diary/ui/app/backend/exceptions.py +++ b/soul_diary/ui/app/backend/exceptions.py @@ -2,6 +2,10 @@ class BackendException(Exception): pass +class RegistrationNotSupportedException(BackendException): + pass + + class UserAlreadyExistsException(BackendException): pass diff --git a/soul_diary/ui/app/backend/local.py b/soul_diary/ui/app/backend/local.py index c556f86..0e2ef00 100644 --- a/soul_diary/ui/app/backend/local.py +++ b/soul_diary/ui/app/backend/local.py @@ -54,6 +54,9 @@ class LocalBackend(BaseBackend): return auth_block + async def deauth(self): + pass + async def get_options(self) -> Options: return Options(registration_enabled=True) diff --git a/soul_diary/ui/app/backend/models.py b/soul_diary/ui/app/backend/models.py index 14cf559..184d607 100644 --- a/soul_diary/ui/app/backend/models.py +++ b/soul_diary/ui/app/backend/models.py @@ -1,9 +1,10 @@ import uuid +from datetime import datetime -from pydantic import AwareDatetime, BaseModel +from pydantic import BaseModel class SenseBackendData(BaseModel): id: uuid.UUID data: str - created_at: AwareDatetime + created_at: datetime diff --git a/soul_diary/ui/app/backend/soul.py b/soul_diary/ui/app/backend/soul.py new file mode 100644 index 0000000..4348fbb --- /dev/null +++ b/soul_diary/ui/app/backend/soul.py @@ -0,0 +1,173 @@ +import uuid +from typing import Any + +import httpx +import yarl + +from soul_diary.ui.app.local_storage import LocalStorage +from soul_diary.ui.app.models import BackendType, Options +from .base import BaseBackend +from .exceptions import ( + IncorrectCredentialsException, + NonAuthenticatedException, + RegistrationNotSupportedException, + SenseNotFoundException, + UserAlreadyExistsException, +) +from .models import SenseBackendData + + +class SoulBackend(BaseBackend): + BACKEND = BackendType.SOUL + + def __init__( + self, + url: yarl.URL | str, + local_storage: LocalStorage, + username: str | None = None, + encryption_key: str | None = None, + token: str | None = None, + ): + self._url = yarl.URL(url) + self._client = httpx.AsyncClient() + + super().__init__( + local_storage=local_storage, + username=username, + encryption_key=encryption_key, + token=token, + ) + + def get_backend_data(self) -> dict[str, Any]: + return {"url": str(self._url)} + + async def request( + self, + method: str, + path: str, + json = None, + params: dict[str, Any] | None = None, + ): + url = self._url / path.lstrip("/") + headers = {} + if self._token: + headers["Authorization"] = f"Bearer {self._token}" + + response = await self._client.request( + method=method, + url=str(url), + json=json, + params=params, + headers=headers, + ) + + try: + response.raise_for_status() + except httpx.HTTPStatusError as exc: + if exc.response.status_code == 401: + raise NonAuthenticatedException() + else: + raise exc + + return response.json() + + async def create_user(self, username: str, password: str) -> str: + path = "/signup" + data = { + "username": username, + "password": password, + } + + try: + response = await self.request(method="POST", path=path, json=data) + except httpx.HTTPStatusError as exc: + response = exc.response.json() + if response == {"detail": "User already exists."}: + raise UserAlreadyExistsException() + elif response == {"detail": "Registration not supported."}: + raise RegistrationNotSupportedException() + else: + raise exc + + return response["token"] + + async def auth(self, username: str, password: str) -> str: + path = "/signin" + data = { + "username": username, + "password": password, + } + + try: + response = await self.request(method="POST", path=path, json=data) + except NonAuthenticatedException: + raise IncorrectCredentialsException() + + return response["token"] + + async def deauth(self): + path = "/logout" + + await self.request(method="POST", path=path) + + async def get_options(self) -> Options: + path = "/options" + + response = await self.request(method="GET", path=path) + + return Options.model_validate(response) + + async def fetch_sense_list( + self, + page: int = 1, + limit: int = 10, + ) -> list[SenseBackendData]: + path = "/senses/" + params = {"page": page, "limit": limit} + + response = await self.request(method="GET", path=path, params=params) + senses = [SenseBackendData.model_validate(sense) for sense in response["data"]] + + return senses + + async def fetch_sense(self, sense_id: uuid.UUID) -> SenseBackendData: + path = f"/senses/{sense_id}" + + try: + response = await self.request(method="GET", path=path) + except httpx.HTTPStatusError as exc: + if exc.response.status_code == 404: + raise SenseNotFoundException() + else: + raise exc + + return SenseBackendData.model_validate(response) + + async def pull_sense_data( + self, + data: str, + sense_id: uuid.UUID | None = None, + ) -> SenseBackendData: + path = "/senses/" if sense_id is None else f"/senses/{sense_id}" + request_data = {"data": data} + + try: + response = await self.request(method="POST", path=path, json=request_data) + except httpx.HTTPStatusError as exc: + if exc.response.status_code == 404: + raise SenseNotFoundException() + else: + raise exc + + return SenseBackendData.model_validate(response) + + async def delete_sense(self, sense_id: uuid.UUID): + path = f"/senses/{sense_id}" + + try: + await self.request(method="DELETE", path=path) + except httpx.HTTPStatusError as exc: + if exc.response.status_code == 404: + raise SenseNotFoundException() + else: + raise exc diff --git a/soul_diary/ui/app/local_storage.py b/soul_diary/ui/app/local_storage.py index 937296b..e050210 100644 --- a/soul_diary/ui/app/local_storage.py +++ b/soul_diary/ui/app/local_storage.py @@ -35,9 +35,11 @@ class LocalStorage: await self.raw_write("soul_diary.client", auth_data.model_dump(mode="json")) async def get_auth_data(self) -> AuthData | None: - if await self.raw_contains("soul_diary.client"): - data = await self.raw_read("soul_diary.client") - return AuthData.model_validate(data) + if not await self.raw_contains("soul_diary.client"): + return None + + data = await self.raw_read("soul_diary.client") + return AuthData.model_validate(data) async def remove_auth_data(self): if await self.raw_contains("soul_diary.client"): diff --git a/soul_diary/ui/app/middleware.py b/soul_diary/ui/app/middleware.py index 8cad5e3..8bcee65 100644 --- a/soul_diary/ui/app/middleware.py +++ b/soul_diary/ui/app/middleware.py @@ -9,6 +9,7 @@ from soul_diary.ui.app.routes import AUTH, SENSE_LIST async def middleware(page: flet.Page, params: Params, basket: Basket): local_storage = LocalStorage(client_storage=page.client_storage) auth_data = await local_storage.get_auth_data() + # await local_storage._client_storage.clear_async() if auth_data is None: await page.go_async(AUTH) return diff --git a/soul_diary/ui/app/models.py b/soul_diary/ui/app/models.py index f2eeae2..168ff86 100644 --- a/soul_diary/ui/app/models.py +++ b/soul_diary/ui/app/models.py @@ -1,7 +1,8 @@ import enum import uuid +from datetime import datetime -from pydantic import AwareDatetime, BaseModel, constr +from pydantic import BaseModel, constr class Emotion(str, enum.Enum): @@ -24,7 +25,7 @@ class Sense(BaseModel): feelings: constr(min_length=1, strip_whitespace=True) body: constr(min_length=1, strip_whitespace=True) desires: constr(min_length=1, strip_whitespace=True) - created_at: AwareDatetime + created_at: datetime class Options(BaseModel): diff --git a/soul_diary/ui/app/views/auth.py b/soul_diary/ui/app/views/auth.py index 1b29923..a1f5615 100644 --- a/soul_diary/ui/app/views/auth.py +++ b/soul_diary/ui/app/views/auth.py @@ -6,7 +6,7 @@ import flet from pydantic import AnyHttpUrl from soul_diary.ui.app.backend.exceptions import IncorrectCredentialsException, UserAlreadyExistsException -from soul_diary.ui.app.backend.local import LocalBackend +from soul_diary.ui.app.backend.soul import SoulBackend from soul_diary.ui.app.local_storage import LocalStorage from soul_diary.ui.app.middlewares.base import BaseMiddleware from soul_diary.ui.app.models import BackendType, Options @@ -243,11 +243,16 @@ class AuthView(BaseView): async def connect_to_soul_server(self) -> Options: try: - AnyHttpUrl(self.backend_data.get("url")) + backend_url = AnyHttpUrl(self.backend_data.get("url")) except ValueError: raise SoulServerIncorrectURL() - raise - + + backend_client = SoulBackend( + local_storage=self.local_storage, + url=str(backend_url), + ) + return await backend_client.get_options() + async def callback_change_username(self, event: flet.ControlEvent): self.username = event.control.value @@ -268,10 +273,13 @@ class AuthView(BaseView): await event.page.update_async() return - if self.backend == BackendType.LOCAL: - backend_client = LocalBackend(local_storage=self.local_storage) - else: + backend_client_class = self.BACKEND_MAPPING.get(self.backend) + if backend_client_class is None: raise + backend_client = backend_client_class( + local_storage=self.local_storage, + **self.backend_data, + ) async with self.in_progress(page=event.page): try: @@ -297,10 +305,13 @@ class AuthView(BaseView): await event.page.update_async() return - if self.backend == BackendType.LOCAL: - backend_client = LocalBackend(local_storage=self.local_storage) - else: + backend_client_class = self.BACKEND_MAPPING.get(self.backend) + if backend_client_class is None: raise + backend_client = backend_client_class( + local_storage=self.local_storage, + **self.backend_data, + ) async with self.in_progress(page=event.page): try: diff --git a/soul_diary/ui/app/views/base.py b/soul_diary/ui/app/views/base.py index 0b8ef6f..ebf0467 100644 --- a/soul_diary/ui/app/views/base.py +++ b/soul_diary/ui/app/views/base.py @@ -8,6 +8,7 @@ from flet_route import Basket, Params from soul_diary.ui.app.backend.base import BaseBackend from soul_diary.ui.app.backend.exceptions import NonAuthenticatedException from soul_diary.ui.app.backend.local import LocalBackend +from soul_diary.ui.app.backend.soul import SoulBackend from soul_diary.ui.app.local_storage import LocalStorage from soul_diary.ui.app.middlewares.base import BaseMiddleware from soul_diary.ui.app.models import BackendType @@ -67,6 +68,11 @@ class MetaView(type): class BaseView(metaclass=MetaView): + BACKEND_MAPPING = { + BackendType.LOCAL: LocalBackend, + BackendType.SOUL: SoulBackend, + } + is_abstract = True _initial_view: Callable | None @@ -126,14 +132,13 @@ class BaseView(metaclass=MetaView): if self._initial_view is not None: await self._initial_view(page=page) - async def get_backend_client(self, page: flet.Page) -> BaseBackend: + async def get_backend_client(self) -> BaseBackend: auth_data = await self.local_storage.get_auth_data() if auth_data is None: raise NonAuthenticatedException() - if auth_data.backend == BackendType.LOCAL: - backend_client_class = LocalBackend - else: + backend_client_class = self.BACKEND_MAPPING.get(auth_data.backend, None) + if backend_client_class is None: raise return backend_client_class( diff --git a/soul_diary/ui/app/views/sense_add.py b/soul_diary/ui/app/views/sense_add.py index a18fecf..0932786 100644 --- a/soul_diary/ui/app/views/sense_add.py +++ b/soul_diary/ui/app/views/sense_add.py @@ -225,7 +225,7 @@ class SenseAddView(BaseView): await event.page.update_async() return - backend_client = await self.get_backend_client(page=event.page) + backend_client = await self.get_backend_client() async with self.in_progress(page=event.page): await backend_client.create_sense( emotions=self.emotions, diff --git a/soul_diary/ui/app/views/sense_list.py b/soul_diary/ui/app/views/sense_list.py index 1dcad41..db8ce53 100644 --- a/soul_diary/ui/app/views/sense_list.py +++ b/soul_diary/ui/app/views/sense_list.py @@ -74,9 +74,7 @@ class SenseListView(BaseView): if auth_data is None: raise NonAuthenticatedException() - if auth_data.backend == BackendType.LOCAL: - pass - backend_client = await self.get_backend_client(page=page) + backend_client = await self.get_backend_client() senses = await backend_client.get_sense_list() self.cards.controls = [await self.render_card_from_sense(sense) for sense in senses] await page.update_async() @@ -98,5 +96,6 @@ class SenseListView(BaseView): await event.page.go_async(SENSE_ADD) async def callback_logout(self, event: flet.ControlEvent): - await self.local_storage.remove_auth_data() + backend_client = await self.get_backend_client() + await backend_client.logout() await event.page.go_async(AUTH) diff --git a/soul_diary/ui/cli.py b/soul_diary/ui/cli.py index 3be2151..1397eb7 100644 --- a/soul_diary/ui/cli.py +++ b/soul_diary/ui/cli.py @@ -1,11 +1,21 @@ +import asyncio + import typer from . import web +from .service import get_service + + +def run(): + ui_service = get_service() + + asyncio.run(ui_service.run()) def get_cli() -> typer.Typer: cli = typer.Typer() + cli.command(name="run")(run) cli.add_typer(web.get_cli(), name="web") return cli diff --git a/soul_diary/ui/web/service.py b/soul_diary/ui/web/service.py index 01590f8..77b5efc 100644 --- a/soul_diary/ui/web/service.py +++ b/soul_diary/ui/web/service.py @@ -25,9 +25,8 @@ class WebService(ServiceMixin): async def start(self): app = flet_fastapi.app(SoulDiaryApp( - # backend=BackendType.SOUL, - # backend_data=self._backend_data, - backend=BackendType.LOCAL, + backend=BackendType.SOUL, + backend_data=self._backend_data, ).run) config = uvicorn.Config(app=app, host="0.0.0.0", port=self._port) server = UvicornServer(config) diff --git a/soul_diary/ui/web/settings.py b/soul_diary/ui/web/settings.py index 5933ceb..db7dd15 100644 --- a/soul_diary/ui/web/settings.py +++ b/soul_diary/ui/web/settings.py @@ -1,10 +1,12 @@ from typing import Any from pydantic import conint -from pydantic_settings import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict class WebSettings(BaseSettings): + model_config = SettingsConfigDict(env_prefix="ui_web_") + port: conint(ge=1, le=65535) = 8000 backend_data: dict[str, Any] = { "url": "http://localhost:8001",