Compare commits

..

6 Commits

Author SHA1 Message Date
IluaAir
92ee087e5d add user schema 2025-06-22 13:22:20 +03:00
IluaAir
37f2b39bd2 add user manager 2025-06-22 13:17:12 +03:00
IluaAir
c1fab8feea ruff formatter 2025-06-22 12:52:52 +03:00
IluaAir
0a5e8a62eb add transport and strategy 2025-06-22 12:50:51 +03:00
IluaAir
4e2cee6625 add transport and strategy 2025-06-22 12:31:16 +03:00
IluaAir
222f528b5e add access token 2025-06-22 12:12:13 +03:00
61 changed files with 591 additions and 1381 deletions

7
.gitignore vendored
View File

@@ -1,7 +1,4 @@
/.venv/ /.venv/
/.idea /.idea
__pycache__/ /src/db/*.db
*.db .env
.DS_Store
.env
.vscode/

View File

@@ -23,7 +23,6 @@ ___
- 🔥 Установка приоритетов и дедлайнов - 🔥 Установка приоритетов и дедлайнов
- 🔔 Напоминания и уведомления - 🔔 Напоминания и уведомления
- ⚙️ Асинхронная обработка задач - ⚙️ Асинхронная обработка задач
- 📄 Пагинация и фильтрация задач с поддержкой limit/offset
- 💡 Современный и интуитивно понятный интерфейс - 💡 Современный и интуитивно понятный интерфейс
--- ---

View File

@@ -9,10 +9,6 @@ ___
- `GET /users` — Получить список всех пользователей - `GET /users` — Получить список всех пользователей
- `GET /users/{user_id}` — Получить конкретного пользователя - `GET /users/{user_id}` — Получить конкретного пользователя
- `GET /users/{user_id}/tasks` — Получить задачи пользователя - `GET /users/{user_id}/tasks` — Получить задачи пользователя
- **Query параметры:**
- `limit` (int, опционально) — Максимальное количество задач для возврата
- `offset` (int, опционально, по умолчанию 0) — Количество задач для пропуска
- **Пример:** `GET /users/1/tasks?limit=10&offset=20`
- `POST /users` — Создать нового пользователя - `POST /users` — Создать нового пользователя
- `PUT /users/{user_id}` — Обновить данные пользователя - `PUT /users/{user_id}` — Обновить данные пользователя
- `PATCH /users/{user_id}` — Частично обновить данные пользователя - `PATCH /users/{user_id}` — Частично обновить данные пользователя

463
poetry.lock generated
View File

@@ -74,40 +74,209 @@ test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "except
trio = ["trio (>=0.26.1)"] trio = ["trio (>=0.26.1)"]
[[package]] [[package]]
name = "bcrypt" name = "argon2-cffi"
version = "4.0.1" version = "23.1.0"
description = "Modern password hashing for your software and your servers" description = "Argon2 for Python"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"},
{file = "argon2_cffi-23.1.0.tar.gz", hash = "sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08"},
]
[package.dependencies]
argon2-cffi-bindings = "*"
[package.extras]
dev = ["argon2-cffi[tests,typing]", "tox (>4)"]
docs = ["furo", "myst-parser", "sphinx", "sphinx-copybutton", "sphinx-notfound-page"]
tests = ["hypothesis", "pytest"]
typing = ["mypy"]
[[package]]
name = "argon2-cffi-bindings"
version = "21.2.0"
description = "Low-level CFFI bindings for Argon2"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, {file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86"},
{file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"},
{file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"},
{file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"},
{file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, {file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"},
{file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, {file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"},
{file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3e385d1c39c520c08b53d63300c3ecc28622f076f4c2b0e6d7e796e9f6502194"},
{file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3e3cc67fdb7d82c4718f19b4e7a87123caf8a93fde7e23cf66ac0337d3cb3f"},
{file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a22ad9800121b71099d0fb0a65323810a15f2e292f2ba450810a7316e128ee5"},
{file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9f8b450ed0547e3d473fdc8612083fd08dd2120d6ac8f73828df9b7d45bb351"},
{file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, {file = "argon2_cffi_bindings-21.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:93f9bf70084f97245ba10ee36575f0c3f1e7d7724d67d8e5b08e61787c320ed7"},
{file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"},
{file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"},
{file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"},
{file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"},
{file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, {file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"},
]
[package.dependencies]
cffi = ">=1.0.1"
[package.extras]
dev = ["cogapp", "pre-commit", "pytest", "wheel"]
tests = ["pytest"]
[[package]]
name = "bcrypt"
version = "4.3.0"
description = "Modern password hashing for your software and your servers"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281"},
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb"},
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180"},
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f"},
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09"},
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d"},
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd"},
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af"},
{file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231"},
{file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c"},
{file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f"},
{file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d"},
{file = "bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4"},
{file = "bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669"},
{file = "bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d"},
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b"},
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e"},
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59"},
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753"},
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761"},
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb"},
{file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d"},
{file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f"},
{file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732"},
{file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef"},
{file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304"},
{file = "bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51"},
{file = "bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62"},
{file = "bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3"},
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24"},
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef"},
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b"},
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676"},
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1"},
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe"},
{file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0"},
{file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f"},
{file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23"},
{file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe"},
{file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505"},
{file = "bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a"},
{file = "bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b"},
{file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1"},
{file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d"},
{file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492"},
{file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90"},
{file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a"},
{file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce"},
{file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8"},
{file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938"},
{file = "bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18"},
] ]
[package.extras] [package.extras]
tests = ["pytest (>=3.2.1,!=3.3.0)"] tests = ["pytest (>=3.2.1,!=3.3.0)"]
typecheck = ["mypy"] typecheck = ["mypy"]
[[package]]
name = "cffi"
version = "1.17.1"
description = "Foreign Function Interface for Python calling C code."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
{file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
{file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
{file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
{file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
{file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
{file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
{file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
{file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
{file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
{file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
{file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
{file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
{file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
{file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
{file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
{file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
{file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
{file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
{file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
{file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
{file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
{file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
{file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
{file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
{file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
{file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
]
[package.dependencies]
pycparser = "*"
[[package]] [[package]]
name = "click" name = "click"
version = "8.2.1" version = "8.2.1"
@@ -129,12 +298,72 @@ version = "0.4.6"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["main", "test"] groups = ["main"]
markers = "platform_system == \"Windows\""
files = [ files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
] ]
markers = {main = "platform_system == \"Windows\"", test = "sys_platform == \"win32\""}
[[package]]
name = "cryptography"
version = "45.0.3"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = "!=3.9.0,!=3.9.1,>=3.7"
groups = ["main"]
files = [
{file = "cryptography-45.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:7573d9eebaeceeb55285205dbbb8753ac1e962af3d9640791d12b36864065e71"},
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d377dde61c5d67eb4311eace661c3efda46c62113ff56bf05e2d679e02aebb5b"},
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae1e637f527750811588e4582988932c222f8251f7b7ea93739acb624e1487f"},
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ca932e11218bcc9ef812aa497cdf669484870ecbcf2d99b765d6c27a86000942"},
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af3f92b1dc25621f5fad065288a44ac790c5798e986a34d393ab27d2b27fcff9"},
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f8f8f0b73b885ddd7f3d8c2b2234a7d3ba49002b0223f58cfde1bedd9563c56"},
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9cc80ce69032ffa528b5e16d217fa4d8d4bb7d6ba8659c1b4d74a1b0f4235fca"},
{file = "cryptography-45.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c824c9281cb628015bfc3c59335163d4ca0540d49de4582d6c2637312907e4b1"},
{file = "cryptography-45.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5833bb4355cb377ebd880457663a972cd044e7f49585aee39245c0d592904578"},
{file = "cryptography-45.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bb5bf55dcb69f7067d80354d0a348368da907345a2c448b0babc4215ccd3497"},
{file = "cryptography-45.0.3-cp311-abi3-win32.whl", hash = "sha256:3ad69eeb92a9de9421e1f6685e85a10fbcfb75c833b42cc9bc2ba9fb00da4710"},
{file = "cryptography-45.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:97787952246a77d77934d41b62fb1b6f3581d83f71b44796a4158d93b8f5c490"},
{file = "cryptography-45.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:c92519d242703b675ccefd0f0562eb45e74d438e001f8ab52d628e885751fb06"},
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5edcb90da1843df85292ef3a313513766a78fbbb83f584a5a58fb001a5a9d57"},
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38deed72285c7ed699864f964a3f4cf11ab3fb38e8d39cfcd96710cd2b5bb716"},
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5555365a50efe1f486eed6ac7062c33b97ccef409f5970a0b6f205a7cfab59c8"},
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e4253ed8f5948a3589b3caee7ad9a5bf218ffd16869c516535325fece163dcc"},
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cfd84777b4b6684955ce86156cfb5e08d75e80dc2585e10d69e47f014f0a5342"},
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:a2b56de3417fd5f48773ad8e91abaa700b678dc7fe1e0c757e1ae340779acf7b"},
{file = "cryptography-45.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:57a6500d459e8035e813bd8b51b671977fb149a8c95ed814989da682314d0782"},
{file = "cryptography-45.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f22af3c78abfbc7cbcdf2c55d23c3e022e1a462ee2481011d518c7fb9c9f3d65"},
{file = "cryptography-45.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:232954730c362638544758a8160c4ee1b832dc011d2c41a306ad8f7cccc5bb0b"},
{file = "cryptography-45.0.3-cp37-abi3-win32.whl", hash = "sha256:cb6ab89421bc90e0422aca911c69044c2912fc3debb19bb3c1bfe28ee3dff6ab"},
{file = "cryptography-45.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:d54ae41e6bd70ea23707843021c778f151ca258081586f0cfa31d936ae43d1b2"},
{file = "cryptography-45.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ed43d396f42028c1f47b5fec012e9e12631266e3825e95c00e3cf94d472dac49"},
{file = "cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fed5aaca1750e46db870874c9c273cd5182a9e9deb16f06f7bdffdb5c2bde4b9"},
{file = "cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:00094838ecc7c6594171e8c8a9166124c1197b074cfca23645cee573910d76bc"},
{file = "cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:92d5f428c1a0439b2040435a1d6bc1b26ebf0af88b093c3628913dd464d13fa1"},
{file = "cryptography-45.0.3-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:ec64ee375b5aaa354b2b273c921144a660a511f9df8785e6d1c942967106438e"},
{file = "cryptography-45.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:71320fbefd05454ef2d457c481ba9a5b0e540f3753354fff6f780927c25d19b0"},
{file = "cryptography-45.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:edd6d51869beb7f0d472e902ef231a9b7689508e83880ea16ca3311a00bf5ce7"},
{file = "cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:555e5e2d3a53b4fabeca32835878b2818b3f23966a4efb0d566689777c5a12c8"},
{file = "cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:25286aacb947286620a31f78f2ed1a32cded7be5d8b729ba3fb2c988457639e4"},
{file = "cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:050ce5209d5072472971e6efbfc8ec5a8f9a841de5a4db0ebd9c2e392cb81972"},
{file = "cryptography-45.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:dc10ec1e9f21f33420cc05214989544727e776286c1c16697178978327b95c9c"},
{file = "cryptography-45.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9eda14f049d7f09c2e8fb411dda17dd6b16a3c76a1de5e249188a32aeb92de19"},
{file = "cryptography-45.0.3.tar.gz", hash = "sha256:ec21313dd335c51d7877baf2972569f40a4291b76a0ce51391523ae358d05899"},
]
[package.dependencies]
cffi = {version = ">=1.14", markers = "platform_python_implementation != \"PyPy\""}
[package.extras]
docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs ; python_full_version >= \"3.8.0\"", "sphinx-rtd-theme (>=3.0.0) ; python_full_version >= \"3.8.0\""]
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_full_version >= \"3.8.0\""]
pep8test = ["check-sdist ; python_full_version >= \"3.8.0\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
sdist = ["build (>=1.0.0)"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi (>=2024)", "cryptography-vectors (==45.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
test-randomorder = ["pytest-randomly"]
[[package]] [[package]]
name = "dnspython" name = "dnspython"
@@ -175,14 +404,14 @@ idna = ">=2.0.0"
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.115.14" version = "0.115.12"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca"}, {file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"},
{file = "fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739"}, {file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"},
] ]
[package.dependencies] [package.dependencies]
@@ -194,6 +423,49 @@ typing-extensions = ">=4.8.0"
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
[[package]]
name = "fastapi-users"
version = "14.0.1"
description = "Ready-to-use and customizable users management for FastAPI"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "fastapi_users-14.0.1-py3-none-any.whl", hash = "sha256:074df59676dccf79412d2880bdcb661ab1fabc2ecec1f043b4e6a23be97ed9e1"},
{file = "fastapi_users-14.0.1.tar.gz", hash = "sha256:8c032b3a75c6fb2b1f5eab8ffce5321176e9916efe1fe93e7c15ee55f0b02236"},
]
[package.dependencies]
email-validator = ">=1.1.0,<2.3"
fastapi = ">=0.65.2"
fastapi-users-db-sqlalchemy = {version = ">=7.0.0", optional = true, markers = "extra == \"sqlalchemy\""}
makefun = ">=1.11.2,<2.0.0"
pwdlib = {version = "0.2.1", extras = ["argon2", "bcrypt"]}
pyjwt = {version = "2.10.1", extras = ["crypto"]}
python-multipart = "0.0.20"
[package.extras]
beanie = ["fastapi-users-db-beanie (>=4.0.0)"]
oauth = ["httpx-oauth (>=0.13)"]
redis = ["redis (>=4.3.3,<6.0.0)"]
sqlalchemy = ["fastapi-users-db-sqlalchemy (>=7.0.0)"]
[[package]]
name = "fastapi-users-db-sqlalchemy"
version = "7.0.0"
description = "FastAPI Users database adapter for SQLAlchemy"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "fastapi_users_db_sqlalchemy-7.0.0-py3-none-any.whl", hash = "sha256:5fceac018e7cfa69efc70834dd3035b3de7988eb4274154a0dbe8b14f5aa001e"},
{file = "fastapi_users_db_sqlalchemy-7.0.0.tar.gz", hash = "sha256:6823eeedf8a92f819276a2b2210ef1dcfd71fe8b6e37f7b4da8d1c60e3dfd595"},
]
[package.dependencies]
fastapi-users = ">=10.0.0"
sqlalchemy = {version = ">=2.0.0,<2.1.0", extras = ["asyncio"]}
[[package]] [[package]]
name = "greenlet" name = "greenlet"
version = "3.2.2" version = "3.2.2"
@@ -291,15 +563,15 @@ files = [
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]] [[package]]
name = "iniconfig" name = "makefun"
version = "2.1.0" version = "1.16.0"
description = "brain-dead simple config-ini parsing" description = "Small library to dynamically create python functions."
optional = false optional = false
python-versions = ">=3.8" python-versions = "*"
groups = ["test"] groups = ["main"]
files = [ files = [
{file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "makefun-1.16.0-py2.py3-none-any.whl", hash = "sha256:43baa4c3e7ae2b17de9ceac20b669e9a67ceeadff31581007cca20a07bbe42c4"},
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, {file = "makefun-1.16.0.tar.gz", hash = "sha256:e14601831570bff1f6d7e68828bcd30d2f5856f24bad5de0ccb22921ceebc947"},
] ]
[[package]] [[package]]
@@ -394,61 +666,47 @@ files = [
] ]
[[package]] [[package]]
name = "packaging" name = "pwdlib"
version = "25.0" version = "0.2.1"
description = "Core utilities for Python packages" description = "Modern password hashing for Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["test"]
files = [
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
]
[[package]]
name = "passlib"
version = "1.7.4"
description = "comprehensive password hashing framework supporting over 30 schemes"
optional = false
python-versions = "*"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, {file = "pwdlib-0.2.1-py3-none-any.whl", hash = "sha256:1823dc6f22eae472b540e889ecf57fd424051d6a4023ec0bcf7f0de2d9d7ef8c"},
{file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, {file = "pwdlib-0.2.1.tar.gz", hash = "sha256:9a1d8a8fa09a2f7ebf208265e55d7d008103cbdc82b9e4902ffdd1ade91add5e"},
] ]
[package.dependencies]
argon2-cffi = {version = ">=23.1.0,<24", optional = true, markers = "extra == \"argon2\""}
bcrypt = {version = ">=4.1.2,<5", optional = true, markers = "extra == \"bcrypt\""}
[package.extras] [package.extras]
argon2 = ["argon2-cffi (>=18.2.0)"] argon2 = ["argon2-cffi (>=23.1.0,<24)"]
bcrypt = ["bcrypt (>=3.1.0)"] bcrypt = ["bcrypt (>=4.1.2,<5)"]
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
totp = ["cryptography"]
[[package]] [[package]]
name = "pluggy" name = "pycparser"
version = "1.6.0" version = "2.22"
description = "plugin and hook calling mechanisms for python" description = "C parser in Python"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.8"
groups = ["test"] groups = ["main"]
files = [ files = [
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
] ]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["coverage", "pytest", "pytest-benchmark"]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.11.7" version = "2.11.5"
description = "Data validation using Python type hints" description = "Data validation using Python type hints"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, {file = "pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7"},
{file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, {file = "pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a"},
] ]
[package.dependencies] [package.dependencies]
@@ -597,21 +855,6 @@ gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"]
toml = ["tomli (>=2.0.1)"] toml = ["tomli (>=2.0.1)"]
yaml = ["pyyaml (>=6.0.1)"] yaml = ["pyyaml (>=6.0.1)"]
[[package]]
name = "pygments"
version = "2.19.2"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
groups = ["test"]
files = [
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
]
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]] [[package]]
name = "pyjwt" name = "pyjwt"
version = "2.10.1" version = "2.10.1"
@@ -624,53 +867,15 @@ files = [
{file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"},
] ]
[package.dependencies]
cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""}
[package.extras] [package.extras]
crypto = ["cryptography (>=3.4.0)"] crypto = ["cryptography (>=3.4.0)"]
dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
[[package]]
name = "pytest"
version = "8.4.1"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.9"
groups = ["test"]
files = [
{file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"},
{file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"},
]
[package.dependencies]
colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
iniconfig = ">=1"
packaging = ">=20"
pluggy = ">=1.5,<2"
pygments = ">=2.7.2"
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-asyncio"
version = "1.1.0"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.9"
groups = ["test"]
files = [
{file = "pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf"},
{file = "pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea"},
]
[package.dependencies]
pytest = ">=8.2,<9"
[package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.1.0" version = "1.1.0"
@@ -806,7 +1011,7 @@ files = [
] ]
[package.dependencies] [package.dependencies]
greenlet = {version = ">=1", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} greenlet = {version = ">=1", optional = true, markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") or extra == \"asyncio\""}
typing-extensions = ">=4.6.0" typing-extensions = ">=4.6.0"
[package.extras] [package.extras]
@@ -901,4 +1106,4 @@ standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.12" python-versions = ">=3.12"
content-hash = "cc2947613c2711ad32ccfa1b8f04a0d8bad6043c6e52bffaff12761ec76cc805" content-hash = "40669246978dc70b09cd8fef22d81e8140d3474feeba1023d8178124914ebcf8"

View File

@@ -9,89 +9,18 @@ license = {text = "MIT"}
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"fastapi (>=0.115.12,<0.116.0)",
"sqlalchemy (>=2.0.41,<3.0.0)", "sqlalchemy (>=2.0.41,<3.0.0)",
"fastapi-users[sqlalchemy] (>=14.0.1,<15.0.0)",
"uvicorn (>=0.34.2,<0.35.0)", "uvicorn (>=0.34.2,<0.35.0)",
"aiosqlite (>=0.21.0,<0.22.0)", "aiosqlite (>=0.21.0,<0.22.0)",
"pydantic-settings (>=2.9.1,<3.0.0)", "pydantic-settings (>=2.9.1,<3.0.0)",
"alembic (>=1.16.1,<2.0.0)", "alembic (>=1.16.1,<2.0.0)",
"ruff (>=0.11.12,<0.12.0)", "ruff (>=0.11.12,<0.12.0)",
"greenlet (>=3.2.2,<4.0.0)", "greenlet (>=3.2.2,<4.0.0)"
"pydantic (>=2.11.7,<3.0.0)",
"idna (>=3.10,<4.0)",
"fastapi (>=0.115.14,<0.116.0)",
"pyjwt (>=2.10.1,<3.0.0)",
"passlib (>=1.7.4,<2.0.0)",
"email-validator (>=2.2.0,<3.0.0)",
"bcrypt (==4.0.1)",
"python-multipart (>=0.0.20,<0.0.21)",
] ]
[build-system] [build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"] requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.ruff]
# Exclude commonly ignored directories and files.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
]
# Set the maximum line length for both linting and formatting.
line-length = 88
# Assume Python 3.9 for compatibility checks.
target-version = "py312"
# Enable preview features for early access to new rules and formatting changes.
preview = true
[tool.ruff.lint]
# Select specific rule groups to enable.
# 'E' for pycodestyle, 'F' for Pyflakes, 'I' for isort, 'B' for flake8-bugbear.
select = [
"E",
"F",
"I",
"B",
"UP", # pyupgrade
"SIM", # flake8-simplify
]
# Ignore specific rules within the selected groups.
ignore = [
"UP",
"B903",
"B904",
"E501",
"B008",
]
[tool.ruff.format]
# Enable Ruff's formatter.
docstring-code-format = true
[tool.poetry.group.test.dependencies]
pytest = "^8.4.1"
pytest-asyncio = "^1.1.0"

View File

@@ -1,3 +0,0 @@
[pytest]
pythonpath = . src
asyncio_mode = auto

View File

@@ -1,8 +1,8 @@
from fastapi import APIRouter from fastapi import APIRouter
from src.api.users import router as users_router
from src.api.tasks import router as tasks_router
from src.api.v1 import router as v1_router router = APIRouter()
from src.core.settings import settings
router = APIRouter(prefix=settings.api.prefix) router.include_router(router=users_router)
router.include_router(router=tasks_router)
router.include_router(router=v1_router)

View File

@@ -1,15 +0,0 @@
from typing import Annotated, AsyncGenerator
from fastapi import Depends
from src.core.database import async_session_maker
from src.core.db_manager import DBManager
from src.core.interfaces import IUOWDB
async def get_db() -> AsyncGenerator[IUOWDB, None]:
async with DBManager(async_session_maker) as db:
yield db
sessionDep = Annotated[IUOWDB, Depends(get_db)]

View File

@@ -0,0 +1,38 @@
from typing import Annotated, AsyncGenerator
from fastapi import Depends
from fastapi_users.authentication.strategy import AccessTokenDatabase
from fastapi_users_db_sqlalchemy import SQLAlchemyUserDatabase
from fastapi_users_db_sqlalchemy.access_token import SQLAlchemyAccessTokenDatabase
from sqlalchemy.ext.asyncio import AsyncSession
from src.db.database import async_session_maker
from src.models import UsersORM, AccessToken
from src.utils.user_manager import UserManager
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session_maker as db:
yield db
DBDep = Annotated[AsyncSession, Depends(get_db)]
async def get_users_db(session: DBDep):
yield SQLAlchemyUserDatabase(session, UsersORM)
async def get_access_token_db(
session: DBDep,
):
yield SQLAlchemyAccessTokenDatabase(session, AccessToken)
ATDep = Annotated[AccessTokenDatabase[AccessToken], Depends(get_access_token_db)]
async def get_user_manager(
users_db: Annotated[SQLAlchemyUserDatabase, Depends(get_users_db)],
):
yield UserManager(users_db)

View File

@@ -0,0 +1,21 @@
from fastapi import Depends
from fastapi_users.authentication import AuthenticationBackend
from fastapi_users.authentication.strategy import DatabaseStrategy, AccessTokenDatabase
from src.api.dependacies.dependancies import get_access_token_db
from src.api.dependacies.transport import bearer_transport
from src.models import AccessToken
from src.settings import settings
def get_database_strategy(
access_token_db: AccessTokenDatabase[AccessToken] = Depends(get_access_token_db),
) -> DatabaseStrategy:
return DatabaseStrategy(access_token_db, lifetime_seconds=settings.lifetime)
auth_backend = AuthenticationBackend(
name="database",
transport=bearer_transport,
get_strategy=get_database_strategy,
)

View File

@@ -1,43 +0,0 @@
from datetime import date
from enum import Enum
from typing import Annotated
from fastapi import Depends, Query
from fastapi.exceptions import HTTPException
from pydantic import BaseModel, model_validator
class StatEnum(str, Enum):
open = "open"
closed = "closed"
in_progress = "in_progress"
todo = "todo"
class Pagination(BaseModel):
page: int | None = Query(default=0, ge=0)
limit: int | None = Query(default=30, ge=0, le=50)
PaginationTasksDep = Annotated[Pagination, Depends()]
class Status(BaseModel):
status: StatEnum | None = Query(default=None)
StatusTaskDep = Annotated[Status, Depends()]
class Date(BaseModel):
date_from: date | None = Query(default=None)
date_to: date | None = Query(default=None)
@model_validator(mode="after")
def check_dates(self):
if self.date_from and self.date_to and self.date_to < self.date_from:
raise HTTPException(status_code=422, detail="date_to cannot be less than date_from")
return self
DateDep = Annotated[Date, Depends()]

View File

@@ -0,0 +1,4 @@
from fastapi_users.authentication import BearerTransport
bearer_transport = BearerTransport(tokenUrl="auth/login")

View File

@@ -1,86 +0,0 @@
from typing import Annotated
from fastapi import Depends, HTTPException, Path
from fastapi.security import OAuth2PasswordBearer
from jwt import InvalidTokenError
from src.api.dependacies.db_dep import sessionDep
from src.core.auth_manager import AuthManager
from src.core.settings import settings
from src.schemas.auth import TokenData
from src.services.tasks import TaskService
from src.services.users import UserService
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.api.v1_login_url}/login")
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
credentials_exception = HTTPException(
status_code=401,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = AuthManager.decode_access_token(token=token)
if payload is None:
raise credentials_exception
user = TokenData(**payload)
except InvalidTokenError:
raise credentials_exception
return user
CurrentUser = Annotated[TokenData, Depends(get_current_user)]
def get_current_active_user(
current_user: CurrentUser,
):
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
ActiveUser = Annotated[TokenData, Depends(get_current_active_user)]
async def get_admin_user(db: sessionDep, current_user: ActiveUser):
await UserService(db).validate_admin_user(current_user.sub)
return current_user
AdminUser = Annotated[TokenData, Depends(get_admin_user)]
async def user_or_admin(db: sessionDep, current_user: ActiveUser, owner_id: int):
if current_user.id == owner_id:
return current_user
else:
admin = await get_admin_user(db, current_user)
return admin
async def CurrentOrAdminOwner(
db: sessionDep, current_user: ActiveUser, id: Annotated[int, Path()]
):
authorized_user = await user_or_admin(db, current_user, id)
if not authorized_user:
raise HTTPException(status_code=403, detail="Not authorized")
return authorized_user
async def CurrentOrAdminTask(
db: sessionDep,
id: Annotated[int, Path()],
current_user: ActiveUser,
):
task = await TaskService(db).get_task(id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return await CurrentOrAdminOwner(db, current_user, task.user_id)
OwnerDep = Annotated[TokenData, Depends(CurrentOrAdminOwner)]
TaskOwnerDep = Annotated[TokenData, Depends(CurrentOrAdminTask)]

27
src/api/tasks.py Normal file
View File

@@ -0,0 +1,27 @@
from fastapi import APIRouter
router = APIRouter(prefix="/tasks", tags=["Tasks"])
@router.get("/")
async def get_tasks(): ...
@router.get("/{id}")
async def get_task_id(id: int): ...
@router.post("/")
async def post_task(): ...
@router.put("/{id}")
async def put_task(id: int): ...
@router.patch("/{id}")
async def patch_task(id: int): ...
@router.delete("/{id}")
async def delete_task(id: int): ...

3
src/api/users.py Normal file
View File

@@ -0,0 +1,3 @@
from fastapi import APIRouter
router = APIRouter(prefix="/users", tags=["Users"])

View File

@@ -1,12 +0,0 @@
from fastapi import APIRouter
from src.api.v1.auth import router as auth_router
from src.api.v1.tasks import router as tasks_router
from src.api.v1.users import router as users_router
from src.core.settings import settings
router = APIRouter(prefix=settings.api.v1.prefix)
router.include_router(router=auth_router)
router.include_router(router=users_router)
router.include_router(router=tasks_router)

View File

@@ -1,28 +0,0 @@
from typing import Annotated
from fastapi import APIRouter, Depends
from fastapi.security import OAuth2PasswordRequestForm
from src.api.dependacies.db_dep import sessionDep
from src.core.settings import settings
from src.schemas.users import UserRequestADD
from src.services.auth import AuthService
router = APIRouter(prefix=settings.api.v1.auth, tags=["Auth"])
@router.post(path="/signup")
async def registration(session: sessionDep, credential: UserRequestADD):
auth = await AuthService(session).registration(credential)
return auth
@router.post(path="/login")
async def login(
session: sessionDep,
credential: Annotated[OAuth2PasswordRequestForm, Depends()],
):
access_token = await AuthService(session).login(
credential.username, credential.password
)
return access_token

View File

@@ -1,51 +0,0 @@
from typing import Annotated
from fastapi import APIRouter, Depends
from src.api.dependacies.db_dep import sessionDep
from src.api.dependacies.task_dep import DateDep, PaginationTasksDep, StatusTaskDep
from src.api.dependacies.user_dep import ActiveUser, TaskOwnerDep
from src.schemas.tasks import TaskADDRequest
from src.services.tasks import TaskService
from src.services.users import UserService
router = APIRouter(prefix="/tasks", tags=["Tasks"])
@router.get("/")
async def get_tasks(
session: sessionDep,
user: ActiveUser,
page: PaginationTasksDep,
status: StatusTaskDep,
date: DateDep,
):
result = await UserService(session).get_user_with_tasks(user.id)
return result
@router.get("/{id}")
async def get_task_id(session: sessionDep, id: int, _: TaskOwnerDep):
task = await TaskService(session).get_task(id)
return task
@router.post("/")
async def post_task(
task_data: Annotated[TaskADDRequest, Depends()],
session: sessionDep,
user: ActiveUser,
):
result = await TaskService(session).create_task(
user_id=user.id, task_data=task_data
)
return result
@router.delete("/{id}")
async def delete_task(
session: sessionDep,
id: int,
_: TaskOwnerDep,
):
await TaskService(session).delete_task(id)

View File

@@ -1,49 +0,0 @@
from fastapi import APIRouter, Body
from src.api.dependacies.db_dep import sessionDep
from src.api.dependacies.user_dep import (
ActiveUser,
AdminUser,
OwnerDep,
)
from src.core.settings import settings
from src.schemas.users import UserUpdate
from src.services.users import UserService
router = APIRouter(prefix=settings.api.v1.users, tags=["Users"])
@router.get("/me")
async def get_me(user: ActiveUser):
return user
@router.get("/")
async def get_all_users(session: sessionDep, _: AdminUser):
users = await UserService(session).get_all_users()
return users
@router.get("/{id}")
async def get_user_by_id(session: sessionDep, id: int, _: OwnerDep):
user = await UserService(session).get_user_by_filter_or_raise(id=id)
return user
@router.patch("/{id}")
async def patch_user(
session: sessionDep,
id: int,
_: OwnerDep,
user_update: UserUpdate = Body(),
):
updated_user = await UserService(session).update_user(
id=id, update_data=user_update
)
return updated_user
@router.delete("/{id}")
async def delete_user(session: sessionDep, id: int, _: AdminUser):
await UserService(session).delete_user(id)
return {"message": "User deleted successfully"}

View File

View File

@@ -1,43 +0,0 @@
from datetime import datetime, timedelta, timezone
import jwt
from passlib.context import CryptContext
from src.core.settings import settings
class AuthManager:
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@classmethod
def verify_password(cls, plain_password, hashed_password):
return cls.pwd_context.verify(plain_password, hashed_password)
@classmethod
def get_password_hash(cls, password):
return cls.pwd_context.hash(password)
@classmethod
def create_access_token(cls, data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(
minutes=settings.access_token.expire_minutes
)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(
to_encode,
settings.access_token.secret_key,
algorithm=settings.access_token.algorithm,
)
return encoded_jwt
@classmethod
def decode_access_token(cls, token: str) -> dict:
return jwt.decode(
token,
settings.access_token.secret_key,
algorithms=[settings.access_token.algorithm],
)

View File

@@ -1,26 +0,0 @@
from datetime import datetime
from sqlalchemy import TIMESTAMP, event, func
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from src.core.settings import settings
engine = create_async_engine(settings.db.url, echo=True)
@event.listens_for(engine.sync_engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
if "sqlite" in settings.db.url:
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
async_session_maker = async_sessionmaker(bind=engine, expire_on_commit=False)
class Base(DeclarativeBase):
created_at: Mapped[datetime] = mapped_column(
TIMESTAMP(timezone=True), server_default=func.now()
)

View File

@@ -1,24 +0,0 @@
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from src.repository.tasks import TasksRepo
from src.repository.users import UsersRepo
class DBManager:
def __init__(self, session_factory: async_sessionmaker[AsyncSession]):
self.session_factory = session_factory
async def __aenter__(self) -> "DBManager":
self.session: AsyncSession = self.session_factory()
self.user = UsersRepo(self.session)
self.task = TasksRepo(self.session)
return self
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
await self.session.rollback()
await self.session.close()
async def commit(self) -> None:
await self.session.commit()

View File

@@ -1,18 +0,0 @@
from typing import Any, Protocol
from sqlalchemy.ext.asyncio import AsyncSession
from src.repository.tasks import TasksRepo
from src.repository.users import UsersRepo
class IUOWDB(Protocol):
session: AsyncSession
user: UsersRepo
task: TasksRepo
async def __aenter__(self) -> "IUOWDB": ...
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: ...
async def commit(self) -> None: ...

View File

@@ -1,49 +0,0 @@
from pathlib import Path
from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict
BASE_DIR = Path(__file__).parent.parent
DB_PATH = BASE_DIR / "db/taskncoffee.db"
class ApiV1Prefix(BaseModel):
prefix: str = "/v1"
auth: str = "/auth"
users: str = "/users"
@property
def login_url(self) -> str:
return f"{self.prefix}{self.auth}"
class ApiPrefix(BaseModel):
prefix: str = "/api"
v1: ApiV1Prefix = ApiV1Prefix()
@property
def v1_login_url(self) -> str:
return f"{self.prefix}{self.v1.login_url}"
class DbSettings(BaseModel):
url: str = f"sqlite+aiosqlite:///{DB_PATH}"
class AccessToken(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env", env_file_encoding="utf-8", env_prefix="ACCESS_TOKEN_"
)
expire_minutes: int
secret_key: str
algorithm: str = "HS256"
token_type: str = "bearer" # noqa: S105
class Settings(BaseSettings):
api: ApiPrefix = ApiPrefix()
db: DbSettings = DbSettings()
access_token: AccessToken = AccessToken() # type: ignore
settings = Settings()

15
src/db/database.py Normal file
View File

@@ -0,0 +1,15 @@
from datetime import datetime
from sqlalchemy import TIMESTAMP, func
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
engine = create_async_engine("sqlite+aiosqlite:///src/db/taskncoffee.db", echo=True)
async_session_maker = async_sessionmaker(bind=engine, expire_on_commit=False)
class Base(DeclarativeBase):
created_at: Mapped[datetime] = mapped_column(
TIMESTAMP(timezone=True), server_default=func.now()
)

View File

@@ -1,14 +1,10 @@
import sys
from pathlib import Path
import uvicorn import uvicorn
from fastapi import FastAPI from fastapi import FastAPI
sys.path.append(str(Path(__file__).parent.parent))
from src.api import router from src.api import router
app = FastAPI(title="Task&Coffee") app = FastAPI()
app.include_router(router=router) app.include_router(router=router)
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run("src.main:app", port=8000, log_level="info", reload=True) uvicorn.run("src.main:app", port=5000, log_level="info", reload=True)

View File

@@ -1,9 +1,11 @@
from logging.config import fileConfig from logging.config import fileConfig
from alembic import context from sqlalchemy import engine_from_config
from sqlalchemy import engine_from_config, event, pool from sqlalchemy import pool
from src.core.database import Base from alembic import context
from src.db.database import Base
from src.models import * # noqa from src.models import * # noqa
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides
@@ -65,14 +67,6 @@ def run_migrations_online() -> None:
poolclass=pool.NullPool, poolclass=pool.NullPool,
) )
# Enable foreign keys for SQLite in migrations
@event.listens_for(connectable, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
print("⚙️ Enabling PRAGMA foreign_keys=ON for Alembic")
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
with connectable.connect() as connection: with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata) context.configure(connection=connection, target_metadata=target_metadata)

View File

@@ -1,18 +1,18 @@
"""init """init
Revision ID: a2fdd0ec4a96 Revision ID: 932121e6b220
Revises: Revises:
Create Date: 2025-07-06 00:02:09.254907 Create Date: 2025-06-22 11:52:49.691545
""" """
from typing import Sequence, Union from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "a2fdd0ec4a96" revision: str = "932121e6b220"
down_revision: Union[str, None] = None down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None
@@ -20,17 +20,17 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None: def upgrade() -> None:
"""Upgrade schema.""" """Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table( op.create_table(
"users", "users",
sa.Column("id", sa.Integer(), nullable=False), sa.Column("id", sa.Integer(), nullable=False),
sa.Column("username", sa.String(length=30), nullable=False), sa.Column("username", sa.String(length=30), nullable=False),
sa.Column("hashed_password", sa.String(length=255), nullable=False),
sa.Column("email", sa.String(length=255), nullable=True),
sa.Column("telegram_id", sa.BigInteger(), nullable=True), sa.Column("telegram_id", sa.BigInteger(), nullable=True),
sa.Column("avatar_path", sa.String(length=255), nullable=True), sa.Column("avatar_path", sa.String(length=255), nullable=True),
sa.Column("email", sa.String(length=320), nullable=False),
sa.Column("hashed_password", sa.String(length=1024), nullable=False),
sa.Column("is_active", sa.Boolean(), nullable=False), sa.Column("is_active", sa.Boolean(), nullable=False),
sa.Column("is_superuser", sa.Boolean(), nullable=False), sa.Column("is_superuser", sa.Boolean(), nullable=False),
sa.Column("is_verified", sa.Boolean(), nullable=False),
sa.Column( sa.Column(
"created_at", "created_at",
sa.TIMESTAMP(timezone=True), sa.TIMESTAMP(timezone=True),
@@ -38,8 +38,8 @@ def upgrade() -> None:
nullable=False, nullable=False,
), ),
sa.PrimaryKeyConstraint("id"), sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("email"),
) )
op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True)
op.create_index(op.f("ix_users_username"), "users", ["username"], unique=True) op.create_index(op.f("ix_users_username"), "users", ["username"], unique=True)
op.create_table( op.create_table(
"tasks", "tasks",
@@ -77,13 +77,11 @@ def upgrade() -> None:
), ),
sa.PrimaryKeyConstraint("id"), sa.PrimaryKeyConstraint("id"),
) )
# ### end Alembic commands ###
def downgrade() -> None: def downgrade() -> None:
"""Downgrade schema.""" """Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("tasks") op.drop_table("tasks")
op.drop_index(op.f("ix_users_username"), table_name="users") op.drop_index(op.f("ix_users_username"), table_name="users")
op.drop_index(op.f("ix_users_email"), table_name="users")
op.drop_table("users") op.drop_table("users")
# ### end Alembic commands ###

View File

@@ -0,0 +1,44 @@
"""access token
Revision ID: bc0bdd74718c
Revises: 932121e6b220
Create Date: 2025-06-22 12:11:19.223212
"""
from typing import Sequence, Union
import fastapi_users_db_sqlalchemy
from alembic import op
import sqlalchemy as sa
revision: str = "bc0bdd74718c"
down_revision: Union[str, None] = "932121e6b220"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
op.create_table(
"accesstoken",
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column("token", sa.String(length=43), nullable=False),
sa.Column(
"created_at",
fastapi_users_db_sqlalchemy.generics.TIMESTAMPAware(timezone=True),
nullable=False,
),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="cascade"),
sa.PrimaryKeyConstraint("token"),
)
op.create_index(
op.f("ix_accesstoken_created_at"), "accesstoken", ["created_at"], unique=False
)
def downgrade() -> None:
"""Downgrade schema."""
op.drop_index(op.f("ix_accesstoken_created_at"), table_name="accesstoken")
op.drop_table("accesstoken")

View File

@@ -1,58 +0,0 @@
"""add_cascade_delete_to_tasks
Revision ID: 197b195208e8
Revises: a2fdd0ec4a96
Create Date: 2025-08-06 23:41:56.778423
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "197b195208e8"
down_revision: Union[str, None] = "a2fdd0ec4a96"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade():
"""Upgrade schema."""
op.execute("PRAGMA foreign_keys=ON")
with op.batch_alter_table("tasks", schema=None) as batch_op:
connection = op.get_bind()
inspector = sa.inspect(connection)
foreign_keys = inspector.get_foreign_keys("tasks")
constraint_name = None
for fk in foreign_keys:
if "user_id" in fk["constrained_columns"]:
constraint_name = fk["name"]
break
if constraint_name:
try: # noqa: SIM105
batch_op.drop_constraint(constraint_name, type_="foreignkey")
except: # noqa E722
pass
batch_op.create_foreign_key(
"fk_tasks_user_id_users", "users", ["user_id"], ["id"], ondelete="CASCADE"
)
def downgrade():
"""Downgrade schema."""
with op.batch_alter_table("tasks", schema=None) as batch_op:
try: # noqa: SIM105
batch_op.drop_constraint("fk_tasks_user_id_users", type_="foreignkey")
except: # noqa E722
pass
batch_op.create_foreign_key(
"fk_tasks_user_id_users", "users", ["user_id"], ["id"]
)

View File

@@ -1,52 +0,0 @@
"""fix_duplicate_foreign_keys
Revision ID: 4b0f3ea2fd26
Revises: 197b195208e8
Create Date: 2025-08-06 23:54:24.308488
"""
from typing import Sequence, Union # noqa: UP035
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "4b0f3ea2fd26"
down_revision: Union[str, None] = "197b195208e8"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade():
"""Upgrade schema."""
op.execute("PRAGMA foreign_keys=ON")
with op.batch_alter_table("tasks", schema=None) as batch_op:
connection = op.get_bind()
inspector = sa.inspect(connection)
foreign_keys = inspector.get_foreign_keys("tasks")
for fk in foreign_keys:
if "user_id" in fk["constrained_columns"]:
try: # noqa: SIM105
batch_op.drop_constraint(fk["name"], type_="foreignkey")
except: # noqa E722
pass
batch_op.create_foreign_key(
"fk_tasks_user_id_users", "users", ["user_id"], ["id"], ondelete="CASCADE"
)
def downgrade():
"""Downgrade schema."""
with op.batch_alter_table("tasks", schema=None) as batch_op:
try: # noqa: SIM105
batch_op.drop_constraint("fk_tasks_user_id_users", type_="foreignkey")
except: # noqa E722
pass
batch_op.create_foreign_key(
"fk_tasks_user_id_users", "users", ["user_id"], ["id"]
)

View File

@@ -1,7 +1,9 @@
from src.models.tasks import TasksORM from src.models.token import AccessToken
from src.models.users import UsersORM from src.models.users import UsersORM
from src.models.tasks import TasksORM
__all__ = [ __all__ = [
"UsersORM", "UsersORM",
"TasksORM", "TasksORM",
"AccessToken",
] ]

View File

@@ -1,10 +1,10 @@
from datetime import date from datetime import date
from typing import TYPE_CHECKING, Optional from typing import Optional, TYPE_CHECKING
from sqlalchemy import Date, Enum, ForeignKey, String, Text from sqlalchemy import ForeignKey, Text, Date, Enum, String
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from src.core.database import Base from src.db.database import Base
if TYPE_CHECKING: if TYPE_CHECKING:
from src.models.users import UsersORM from src.models.users import UsersORM
@@ -16,7 +16,7 @@ priority_enum = Enum("low", "medium", "high", "critical", name="priority_enum")
class TasksORM(Base): class TasksORM(Base):
__tablename__ = "tasks" __tablename__ = "tasks"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE")) user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
title: Mapped[str] = mapped_column(String(100)) title: Mapped[str] = mapped_column(String(100))
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
due_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True) due_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True)

13
src/models/token.py Normal file
View File

@@ -0,0 +1,13 @@
from fastapi_users_db_sqlalchemy.access_token import SQLAlchemyBaseAccessTokenTable
from sqlalchemy import Integer, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, declared_attr
from src.db.database import Base
class AccessToken(SQLAlchemyBaseAccessTokenTable[int], Base):
@declared_attr
def user_id(cls) -> Mapped[int]:
return mapped_column(
Integer, ForeignKey("users.id", ondelete="cascade"), nullable=False
)

View File

@@ -1,28 +1,22 @@
from typing import TYPE_CHECKING, Optional from typing import Optional, TYPE_CHECKING
from sqlalchemy import BigInteger, Boolean, Integer, String from fastapi_users_db_sqlalchemy import SQLAlchemyBaseUserTable
from sqlalchemy import String, BigInteger, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from src.core.database import Base from src.db.database import Base
if TYPE_CHECKING: if TYPE_CHECKING:
from src.models.tasks import TasksORM from src.models.tasks import TasksORM
class UsersORM(Base): class UsersORM(SQLAlchemyBaseUserTable[int], Base):
__tablename__ = "users" __tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True) id: Mapped[int] = mapped_column(Integer, primary_key=True)
username: Mapped[str] = mapped_column( username: Mapped[Optional[str]] = mapped_column(
String(30), nullable=False, unique=True, index=True String(30), nullable=False, unique=True, index=True
) )
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
email: Mapped[Optional[str]] = mapped_column(
String(255), unique=True, nullable=True
)
telegram_id: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True) telegram_id: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True)
avatar_path: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) avatar_path: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
is_superuser: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) tasks: Mapped[list["TasksORM"]] = relationship(back_populates="user")
tasks: Mapped[list["TasksORM"]] = relationship(
back_populates="user", cascade="all, delete-orphan"
)

View File

@@ -1,46 +0,0 @@
from typing import Any, Generic, Mapping, Sequence, Type, TypeVar
from sqlalchemy import delete, insert, select
from sqlalchemy.ext.asyncio import AsyncSession
from src.core.database import Base
ModelType = TypeVar("ModelType", bound=Base)
class BaseRepo(Generic[ModelType]):
model: Type[ModelType]
def __init__(self, session: AsyncSession) -> None:
self.session: AsyncSession = session
async def get_filtered(
self, *filters: Any, **filter_by: Any
) -> Sequence[ModelType]:
query = select(self.model).filter(*filters).filter_by(**filter_by)
result = await self.session.execute(query)
models = result.scalars().all()
return models
async def create_one(self, data: Mapping[str, Any]) -> ModelType:
statement = insert(self.model).values(data).returning(self.model)
result = await self.session.execute(statement)
obj: ModelType = result.scalar_one()
return obj
async def create_bulk(self, data: Sequence[Mapping[str, Any]]) -> list[ModelType]:
result = [await self.create_one(item) for item in data]
return result
async def get_one_or_none(self, **filter_by: Any) -> ModelType | None:
query = select(self.model).filter_by(**filter_by)
result = await self.session.execute(query)
model_obj: ModelType | None = result.scalars().one_or_none()
return model_obj
async def get_all(self, *args: Any, **kwargs: Any) -> Sequence[ModelType]:
result: Sequence[ModelType] = await self.get_filtered(*args, **kwargs)
return result
async def delete_one(self, **filter_by) -> None:
await self.session.execute(delete(self.model).filter_by(**filter_by))

View File

@@ -1,6 +0,0 @@
from src.models.tasks import TasksORM
from src.repository.base import BaseRepo
class TasksRepo(BaseRepo):
model: type[TasksORM] = TasksORM

View File

@@ -1,84 +0,0 @@
from datetime import date
from typing import Optional
from sqlalchemy import func, select, update
from sqlalchemy.orm import selectinload
from src.models import UsersORM
from src.models.tasks import TasksORM
from src.repository.base import BaseRepo
class UsersRepo(BaseRepo):
model: type[UsersORM] = UsersORM
async def update_one(self, id: int, data: dict) -> UsersORM:
stmt = (
update(self.model)
.where(self.model.id == id)
.values(data)
.returning(self.model)
)
result = await self.session.execute(stmt)
model = result.scalar_one()
return model
async def get_one_with_load(
self,
user_id: int,
tasks_limit: Optional[int] = None,
tasks_offset: int = 0,
date_to: Optional[date] = None,
date_from: Optional[date] = None,
) -> UsersORM | None:
tasks_subquery = self._tasks_subquary(
date_from=date_from, date_to=date_to, user_id=user_id
)
if tasks_limit is not None:
tasks_subquery = tasks_subquery.limit(tasks_limit)
if tasks_offset > 0:
tasks_subquery = tasks_subquery.offset(tasks_offset)
query = (
select(self.model)
.where(self.model.id == user_id)
.options(
selectinload(
self.model.tasks.and_(TasksORM.id.in_(tasks_subquery))
).load_only(
TasksORM.id, TasksORM.title, TasksORM.due_date, TasksORM.priority
)
)
)
result = await self.session.execute(query)
obj = result.scalar_one_or_none()
if obj and obj.tasks:
obj.tasks.sort(
key=lambda task: (
task.due_date or date.max,
-self._priority(task.priority),
)
)
return obj
async def get_tasks_count(
self, date_from: date | None, date_to: date | None, **filter_by
) -> int:
subq = self._tasks_subquary(date_from, date_to, **filter_by).subquery()
stmt = select(func.count()).select_from(subq)
result = await self.session.execute(stmt)
return result.scalar_one()
def _priority(self, priority: str):
priority_map = {"low": 1, "medium": 2, "high": 3, "critical": 4}
return priority_map.get(priority, 0)
def _tasks_subquary(
self, date_from: date | None, date_to: date | None, **filter_by
):
subq = select(TasksORM.id).filter_by(**filter_by)
if date_from is not None:
subq = subq.filter(TasksORM.due_date >= date_from)
if date_to is not None:
subq = subq.filter(TasksORM.due_date <= date_to)
return subq

View File

@@ -1,12 +0,0 @@
from pydantic import BaseModel
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
id: int
sub: str
is_active: bool

View File

@@ -1,28 +0,0 @@
from datetime import date
from typing import Literal
from pydantic import BaseModel, ConfigDict
class TaskShort(BaseModel):
title: str
due_date: date | None = None
priority: Literal["low", "medium", "high", "critical"] = "medium"
model_config = ConfigDict(from_attributes=True)
class TaskWithId(TaskShort):
id: int
class TaskADDRequest(TaskShort):
description: str | None = None
class Task(TaskADDRequest):
id: int
user_id: int
status: Literal["open", "closed", "in_progress", "todo"]
time_spent: int
model_config = ConfigDict(from_attributes=True)

View File

@@ -1,47 +1,15 @@
from typing import Annotated from typing import Optional
from pydantic import BaseModel, BeforeValidator, ConfigDict, EmailStr from fastapi_users import schemas
from src.schemas.tasks import TaskWithId
from src.schemas.validators import ensure_password
class UserUpdate(BaseModel): class UserRead(schemas.BaseUser[int]):
email: EmailStr | None = None
username: str | None = None
is_active: bool | None = None
model_config = ConfigDict(from_attributes=True, extra="ignore")
class User(BaseModel):
id: int
email: EmailStr | None
username: str username: str
is_active: bool
is_superuser: bool
model_config = ConfigDict(from_attributes=True, extra="ignore")
class UserWithHashedPass(User): class UserCreate(schemas.BaseUserCreate):
hashed_password: str
class UserWithTasks(User):
tasks: list[TaskWithId]
class UserRequest(BaseModel):
username: str username: str
password: str
class UserRequestADD(BaseModel): class UserUpdate(schemas.BaseUserUpdate):
username: str username: Optional[str] = None
email: EmailStr | None = None
password: Annotated[str, BeforeValidator(ensure_password)]
class UserAdd(BaseModel):
email: EmailStr | None
username: str
hashed_password: str

View File

@@ -1,11 +0,0 @@
from typing import Any
def ensure_password(value: Any) -> Any:
if not isinstance(value, str):
raise TypeError("Password must be a string")
if len(value) < 8:
raise ValueError("Password must be at least 8 characters")
if value.strip() == "":
raise ValueError("Password cannot be empty")
return value

View File

@@ -1,43 +0,0 @@
from fastapi import HTTPException
from src.core.auth_manager import AuthManager
from src.core.settings import settings
from src.schemas.auth import Token
from src.schemas.users import User, UserAdd, UserRequestADD, UserWithHashedPass
from src.services.base import BaseService
class AuthService(BaseService):
async def registration(self, cred: UserRequestADD) -> User:
hashed_pass = AuthManager.get_password_hash(cred.password)
user_to_insert = UserAdd(
username=cred.username,
email=cred.email,
hashed_password=hashed_pass,
)
result = await self.session.user.create_one(user_to_insert.model_dump())
await self.session.commit()
return User.model_validate(result)
async def login(self, username: str, password: str):
result = await self.session.user.get_one_or_none(username=username)
if result is None:
raise HTTPException(
status_code=401,
detail="Incorrect username or password",
)
user = UserWithHashedPass.model_validate(result)
verify = AuthManager.verify_password(
plain_password=password, hashed_password=user.hashed_password
)
if not verify or user.is_active is False:
raise HTTPException(
status_code=401,
detail="Incorrect username or password",
)
access_token = AuthManager.create_access_token(
data={"id": user.id, "sub": user.username, "is_active": user.is_active}
)
return Token(
access_token=access_token, token_type=settings.access_token.token_type
)

View File

@@ -1,8 +0,0 @@
from src.core.interfaces import IUOWDB
class BaseService:
session: IUOWDB
def __init__(self, session: "IUOWDB"):
self.session = session

View File

@@ -1,27 +0,0 @@
from fastapi import HTTPException
from src.models.tasks import TasksORM
from src.schemas.tasks import Task, TaskADDRequest
from src.services.base import BaseService
class TaskService(BaseService):
model = TasksORM
async def create_task(self, user_id: int, task_data: TaskADDRequest) -> Task:
user = await self.session.user.get_one_or_none(id=user_id)
if user is None:
raise HTTPException(status_code=404, detail="User not found.")
data_to_insert = task_data.model_dump(exclude_none=True)
data_to_insert["user_id"] = user.id
created_task_orm = await self.session.task.create_one(data_to_insert)
await self.session.commit()
return Task.model_validate(created_task_orm)
async def get_task(self, task_id: int):
task = await self.session.task.get_one_or_none(id=task_id)
return Task.model_validate(task)
async def delete_task(self, task_id: int):
await self.session.task.delete_one(id=task_id)
await self.session.commit()

View File

@@ -1,46 +0,0 @@
from fastapi import HTTPException
from src.schemas.users import User, UserUpdate, UserWithTasks
from src.services.base import BaseService
class UserService(BaseService):
async def get_user_by_filter(self, **filter_by) -> User | None:
result = await self.session.user.get_one_or_none(**filter_by)
if result is None:
return None
return User.model_validate(result)
async def get_user_by_filter_or_raise(self, **filter_by) -> User:
user = await self.get_user_by_filter(**filter_by)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return user
async def validate_admin_user(self, username: str) -> User:
user = await self.get_user_by_filter_or_raise(username=username)
if not user.is_superuser:
raise HTTPException(status_code=403, detail="Admin access required")
return user
async def get_all_users(self) -> list[User]:
users = await self.session.user.get_all()
return [User.model_validate(user) for user in users]
async def delete_user(self, id: int) -> None:
await self.session.user.delete_one(id=id)
await self.session.commit()
async def update_user(self, id: int, update_data: UserUpdate) -> User:
await self.get_user_by_filter_or_raise(id=id)
user = await self.session.user.update_one(
id=id, data=update_data.model_dump(exclude_unset=True)
)
await self.session.commit()
return User.model_validate(user)
async def get_user_with_tasks(self, user_id: int):
user = await self.session.user.get_one_with_load(user_id)
if user is None:
raise HTTPException(status_code=404, detail="User not found.")
return UserWithTasks.model_validate(user)

10
src/settings.py Normal file
View File

@@ -0,0 +1,10 @@
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
LIFETIME: int
SECRET: str
settings = Settings()

34
src/utils/user_manager.py Normal file
View File

@@ -0,0 +1,34 @@
import logging
from typing import Optional
from fastapi import Request
from fastapi_users import BaseUserManager, IntegerIDMixin
from src.models import UsersORM
from src.settings import settings
logger = logging.getLogger()
class UserManager(IntegerIDMixin, BaseUserManager[UsersORM, int]):
reset_password_token_secret = settings.SECRET
verification_token_secret = settings.SECRET
async def on_after_register(
self, user: UsersORM, request: Optional[Request] = None
):
logger.warning("User %r has registered.", user.id)
async def on_after_forgot_password(
self, user: UsersORM, token: str, request: Optional[Request] = None
):
logger.warning(
"User %r has forgot their password. Reset token: %r", user.id, token
)
async def on_after_request_verify(
self, user: UsersORM, token: str, request: Optional[Request] = None
):
logger.warning(
"Verification requested for user %r. Verification token: %r", user.id, token
)

View File

View File

@@ -1,78 +0,0 @@
import json
import pytest
from httpx import ASGITransport, AsyncClient
from sqlalchemy import NullPool, insert
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from src.api.dependacies.db_dep import get_db
from src.core.auth_manager import AuthManager
from src.core.database import Base
from src.core.db_manager import DBManager
from src.core.settings import settings
from src.main import app
from src.models import * # noqa: F403
engine_null_pool = create_async_engine(
"sqlite+aiosqlite:///tests/test_db.db", poolclass=NullPool
)
test_session_maker = async_sessionmaker(engine_null_pool, expire_on_commit=False)
class TestDBManager(DBManager):
def __init__(self):
self.session_factory = test_session_maker
async def get_test_db():
async with TestDBManager() as db:
yield db
@pytest.fixture(scope="function")
async def db():
async for db in get_test_db():
yield db
@pytest.fixture(scope="function")
async def ac():
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://test"
) as ac:
yield ac
app.dependency_overrides[get_db] = get_test_db
@pytest.fixture(scope="session", autouse=True)
async def setup_database():
async with engine_null_pool.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
@pytest.fixture(scope="session", autouse=True)
async def add_admin(setup_database):
hashed_pass = AuthManager.get_password_hash("admin")
user_admin = {
"username": "admin",
"hashed_password": hashed_pass,
"is_superuser": True,
}
async with test_session_maker() as conn:
result = await conn.execute(
insert(UsersORM).values(user_admin).returning(UsersORM) # noqa: F405
)
await conn.commit()
admin = result.scalar_one()
assert admin.is_superuser is True
@pytest.fixture
def auth_token(ac, add_admin):
response = ac.post(
f"{settings.api.v1_login_url}/login",
data={"username": "admin", "password": "admin"},
)
return response.json()["access_token"]

View File

@@ -1,28 +0,0 @@
from httpx import AsyncClient
from src.core.settings import settings
from src.schemas.users import User
async def test_registration(ac):
user = {"username": "kot", "email": "super@kot.ru", "password": "P@ssw0rd"}
result = await ac.post(
f"{settings.api.v1_login_url}/signup",
json=user,
)
assert result.status_code == 200
assert User.model_validate(result.json())
assert result.json()["is_active"]
async def test_login(ac: AsyncClient):
result = await ac.post(
f"{settings.api.v1_login_url}/login",
data={
"grant_type": "password",
"username": "kot",
"password": "P@ssw0rd",
},
)
assert result.status_code == 200
assert result.json().get("access_token")

View File

@@ -1,38 +0,0 @@
[
{
"title": "test1",
"description": "test1",
"due_date": "2026-06-01",
"status": "open",
"priority": "medium"
},
{
"title": "test2",
"description": "test2",
"due_date": "2026-06-01",
"status": "open",
"priority": "high"
},
{
"title": "test3",
"description": "test3",
"due_date": "2026-06-02",
"status": "todo",
"priority": "medium"
},
{
"title": "test4",
"description": "test4",
"due_date": "2026-06-02",
"status": "open",
"priority": "high"
},
{
"title": "test5",
"description": "test5",
"due_date": "2026-06-02",
"status": "todo",
"priority": "low"
}
]

View File

@@ -1,10 +0,0 @@
from src.core.auth_manager import AuthManager
async def test_jwt():
token = AuthManager.create_access_token(
data={"id": 1, "sub": "testuser", "is_active": "True"}
)
assert token
encode_token = AuthManager.decode_access_token(token=token)
assert encode_token["id"] == 1 and encode_token["sub"] == "testuser"

View File

@@ -1,55 +0,0 @@
import json
from datetime import datetime
from typing import TYPE_CHECKING
from src.models.users import UsersORM
from src.schemas.users import User
if TYPE_CHECKING:
from tests.conftest import TestDBManager
async def test_user_crud(db: "TestDBManager"):
data = {
"username": "test",
"hashed_password": "hashed_pass",
"email": "test@mail.ru",
"is_active": True,
"is_superuser": False,
}
user = await db.user.create_one(data=data)
assert user.username == data["username"]
filtered_user = await db.user.get_filtered(username=data["username"])
assert filtered_user[0] == user
new_user = User.model_validate(user)
new_user.username = "Test2"
new_user.email = None
await db.user.update_one(id=new_user.id, data=User.model_dump(new_user))
updated_user = await db.user.get_one_or_none(id=new_user.id)
assert updated_user
assert updated_user.username == new_user.username
assert not updated_user.email
await db.user.delete_one(id=updated_user.id)
delete_user = await db.user.get_one_or_none(id=new_user.id)
assert not delete_user
async def test_tasks_user(db: "TestDBManager"):
with open("tests/mock_tasks.json") as jsonfile:
data = json.load(jsonfile)
admin_user: UsersORM | None = await db.user.get_one_or_none(id=1)
assert admin_user
data = [
{
**item,
"user_id": admin_user.id,
"due_date": datetime.strptime(item["due_date"], "%Y-%m-%d"),
}
for item in data
]
result = await db.task.create_bulk(data)
await db.commit()
assert result
user_with_tasks = await db.user.get_one_with_load(user_id=admin_user.id)
assert user_with_tasks
assert user_with_tasks.tasks