Compare commits

..

73 Commits

Author SHA1 Message Date
IluaAir
7e9346ec4d add base update one 2025-09-06 13:53:18 +03:00
IluaAir
d7e522d362 add patch endpoint and service update_task 2025-09-03 23:55:26 +03:00
IluaAir
6e6613662a patch schema 2025-09-03 23:32:31 +03:00
IluaAir
e1c554e4f0 add statusenum 2025-09-03 23:12:27 +03:00
IluaAir
b1ae775706 fix status for tasks 2025-08-31 23:36:58 +03:00
IluaAir
a9fc764c38 add status to user with loads 2025-08-31 23:27:02 +03:00
IluaAir
bd9786c14c add to tasks service 2025-08-29 22:10:15 +03:00
IluaAir
07f14b0564 add date_to date_from endpoints 2025-08-29 21:48:38 +03:00
IluaAir
3e6468fa38 add quary status 2025-08-29 21:37:42 +03:00
IluaAir
c941b25a90 repo tasks count 2025-08-28 00:36:59 +03:00
IluaAir
45c5492ff8 api pagin, db _task_subquary, tasks depends 2025-08-28 00:27:27 +03:00
IluaAir
cd4eb11604 tests user_with_load 2025-08-27 01:17:24 +03:00
IluaAir
2d54f595db add date from and date to 2025-08-24 21:55:42 +03:00
IluaAir
186b497130 repo user with loads filters 2025-08-24 13:43:07 +03:00
2dde22eb19 Merge pull request 'add tests for project' (#1) from test into dev1
Reviewed-on: #1
2025-08-24 09:52:07 +03:00
IluaAir
543920f476 test crud 2025-08-20 00:15:15 +03:00
IluaAir
a8c4b622a4 test auth api, add auth fixture 2025-08-19 17:06:53 +03:00
IluaAir
cc3aad5c20 test auth api 2025-08-18 18:26:45 +03:00
IluaAir
eb5a1b9d85 add test_db dep 2025-08-17 14:10:32 +03:00
IluaAir
340c3e1077 add jwt check 2025-08-17 12:55:41 +03:00
IluaAir
4104dd7e15 add conftest 2025-08-16 13:16:46 +03:00
IluaAir
053f97daf0 add pytest 2025-08-16 12:40:25 +03:00
IluaAir
9a1b2b4f93 add get-task 2025-08-16 12:37:39 +03:00
IluaAir
9346aae5ef add delete task, update dep 2025-08-16 11:52:53 +03:00
IluaAir
464689bd29 add user with tasks and endpoint 2025-08-15 16:20:56 +03:00
IluaAir
a9997b1207 get one with load 2025-08-15 15:47:58 +03:00
IluaAir
9e5cde8e28 fix token data 2025-08-15 01:06:25 +03:00
IluaAir
723f59d35e add task post 2025-08-15 01:03:48 +03:00
IluaAir
fdc688cf5e repo typing fix 2025-08-11 10:58:41 +03:00
IluaAir
860962bcc3 pydantic validation in service 2025-08-11 10:15:27 +03:00
IluaAir
ddc38dbd07 type cheking 2025-08-11 10:13:08 +03:00
IluaAir
644d5614b9 add get filtered 2025-08-10 20:51:25 +03:00
IluaAir
d6646716b8 move get_all_users to get_all 2025-08-09 01:00:39 +03:00
IluaAir
b2824c2bb2 move delete_one 2025-08-09 00:56:13 +03:00
IluaAir
2471c2981f big fix imports 2025-08-08 15:30:17 +03:00
IluaAir
9c20b7690c add tasks repo update uow and manager 2025-08-08 14:04:33 +03:00
IluaAir
6657f55545 ruff check 2025-08-07 00:22:32 +03:00
IluaAir
45ebed40bc simple get all tasks 2025-08-07 00:20:14 +03:00
IluaAir
c8e1863ee6 ruff format 2025-08-07 00:20:05 +03:00
IluaAir
c61db4bc9d casacade delete tasks 2025-08-06 23:58:29 +03:00
IluaAir
93cf7b2d24 event listens for sqlalchemy 2025-08-06 23:38:17 +03:00
IluaAir
acb3eefcbe fix typing 2025-08-04 10:59:23 +03:00
IluaAir
a6c00e20c3 create patch user 2025-08-02 18:59:43 +03:00
IluaAir
0ab17f3a99 add delete user endpoint 2025-08-02 18:22:01 +03:00
IluaAir
22ee887238 del old user or admin 2025-08-02 15:55:16 +03:00
IluaAir
cdee74210e fix user or admin dep 2025-07-29 00:25:34 +03:00
IluaAir
b9a9e4e094 current or admin dep created 2025-07-27 13:54:39 +03:00
IluaAir
9f105c32c6 fix router request user data 2025-07-27 13:06:00 +03:00
IluaAir
7c334491c3 add user get, access token add id 2025-07-27 12:55:58 +03:00
IluaAir
d3eba77444 add get users 2025-07-27 12:41:35 +03:00
IluaAir
36dad7b441 add admin check 2025-07-27 12:30:06 +03:00
IluaAir
1a495e28c5 update .gitignore 2025-07-21 12:02:41 +03:00
IluaAir
4789da1b70 create active user 2025-07-20 12:37:57 +03:00
IluaAir
8ec8639848 create cur user 2025-07-20 12:34:18 +03:00
IluaAir
d639abfbc5 service auth fix 2025-07-19 12:10:00 +03:00
IluaAir
d530412805 ready login endpoint 2025-07-14 12:41:03 +03:00
IluaAir
19a9b36173 add OAuth2PasswordRequestForm 2025-07-14 12:08:50 +03:00
IluaAir
dd6f06cd12 fix auth manager 2025-07-14 11:58:12 +03:00
IluaAir
a0f9aaeaea add endpoint /login 2025-07-13 11:59:03 +03:00
IluaAir
e5dec473cb fix alembic env path 2025-07-13 11:48:25 +03:00
IluaAir
f3030593da add User return schema 2025-07-13 11:41:58 +03:00
IluaAir
d27aafb4f6 ensure password 2025-07-12 13:10:22 +03:00
IluaAir
18b5a573c1 add password validation example 2025-07-11 21:54:01 +03:00
IluaAir
b12bb65268 create interface uow update services to uow 2025-07-11 21:48:48 +03:00
IluaAir
61512f70d4 add singup endpoint 2025-07-07 12:01:02 +03:00
IluaAir
31879ddf30 fix auth 2025-07-06 12:20:41 +03:00
IluaAir
b6ec15af29 fix pydantic schema 2025-07-06 12:15:01 +03:00
IluaAir
459969f2be create AuthManager 2025-07-06 12:10:58 +03:00
IluaAir
4de11f4149 base logic 2025-07-06 11:58:45 +03:00
IluaAir
817a799ef5 update database settings 2025-07-06 01:09:18 +03:00
IluaAir
ef38c45f14 update settings 2025-07-06 01:07:25 +03:00
IluaAir
1a45e2c284 fix alembic init 2025-07-06 00:07:35 +03:00
IluaAir
231caf7c6e update alembic 2025-07-06 00:03:55 +03:00
61 changed files with 1458 additions and 592 deletions

7
.gitignore vendored
View File

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

View File

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

View File

@@ -9,6 +9,10 @@ ___
- `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}` — Частично обновить данные пользователя

467
poetry.lock generated
View File

@@ -74,209 +74,40 @@ test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "except
trio = ["trio (>=0.26.1)"] trio = ["trio (>=0.26.1)"]
[[package]] [[package]]
name = "argon2-cffi" name = "bcrypt"
version = "23.1.0" version = "4.0.1"
description = "Argon2 for Python" description = "Modern password hashing for your software and your servers"
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 = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"}, {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"},
{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.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"},
{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_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"},
{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_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"},
{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_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"},
{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_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"},
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f"}, {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_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e"}, {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"},
{file = "argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082"}, {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-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f"}, {file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"},
{file = "argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93"}, {file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"},
{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_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"},
{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_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"},
{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-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"},
{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_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"},
{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_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"},
{file = "argon2_cffi_bindings-21.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3b9ef65804859d335dc6b31582cad2c5166f0c3e7975f324d9ffaa34ee7e6583"}, {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-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4966ef5848d820776f5f562a7d45fdd70c2f330c961d0d745b784034bd9f48d"}, {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_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ef543a89dee4db46a1a6e206cd015360e5a75822f76df533845c3cbaf72670"}, {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_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed2937d286e2ad0cc79a7087d3c272832865f779430e0cc2b4f3718d3159b0cb"}, {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-win_amd64.whl", hash = "sha256:5e00316dabdaea0b2dd82d141cc66889ced0cdcbfa599e8b471cf22c620c329a"}, {file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"},
]
[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"
@@ -298,72 +129,12 @@ 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"] groups = ["main", "test"]
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"
@@ -404,14 +175,14 @@ idna = ">=2.0.0"
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.115.12" version = "0.115.14"
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.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"}, {file = "fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca"},
{file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"}, {file = "fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739"},
] ]
[package.dependencies] [package.dependencies]
@@ -423,49 +194,6 @@ 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"
@@ -563,15 +291,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 = "makefun" name = "iniconfig"
version = "1.16.0" version = "2.1.0"
description = "Small library to dynamically create python functions." description = "brain-dead simple config-ini parsing"
optional = false optional = false
python-versions = "*" python-versions = ">=3.8"
groups = ["main"] groups = ["test"]
files = [ files = [
{file = "makefun-1.16.0-py2.py3-none-any.whl", hash = "sha256:43baa4c3e7ae2b17de9ceac20b669e9a67ceeadff31581007cca20a07bbe42c4"}, {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
{file = "makefun-1.16.0.tar.gz", hash = "sha256:e14601831570bff1f6d7e68828bcd30d2f5856f24bad5de0ccb22921ceebc947"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
] ]
[[package]] [[package]]
@@ -666,47 +394,61 @@ files = [
] ]
[[package]] [[package]]
name = "pwdlib" name = "packaging"
version = "0.2.1" version = "25.0"
description = "Modern password hashing for Python" description = "Core utilities for Python packages"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"] groups = ["test"]
files = [ files = [
{file = "pwdlib-0.2.1-py3-none-any.whl", hash = "sha256:1823dc6f22eae472b540e889ecf57fd424051d6a4023ec0bcf7f0de2d9d7ef8c"}, {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
{file = "pwdlib-0.2.1.tar.gz", hash = "sha256:9a1d8a8fa09a2f7ebf208265e55d7d008103cbdc82b9e4902ffdd1ade91add5e"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
] ]
[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]
argon2 = ["argon2-cffi (>=23.1.0,<24)"]
bcrypt = ["bcrypt (>=4.1.2,<5)"]
[[package]] [[package]]
name = "pycparser" name = "passlib"
version = "2.22" version = "1.7.4"
description = "C parser in Python" description = "comprehensive password hashing framework supporting over 30 schemes"
optional = false optional = false
python-versions = ">=3.8" python-versions = "*"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"},
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"},
] ]
[package.extras]
argon2 = ["argon2-cffi (>=18.2.0)"]
bcrypt = ["bcrypt (>=3.1.0)"]
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
totp = ["cryptography"]
[[package]]
name = "pluggy"
version = "1.6.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.9"
groups = ["test"]
files = [
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["coverage", "pytest", "pytest-benchmark"]
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.11.5" version = "2.11.7"
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.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7"}, {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"},
{file = "pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a"}, {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"},
] ]
[package.dependencies] [package.dependencies]
@@ -855,6 +597,21 @@ 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"
@@ -867,15 +624,53 @@ 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"
@@ -1011,7 +806,7 @@ files = [
] ]
[package.dependencies] [package.dependencies]
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\""} 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\")"}
typing-extensions = ">=4.6.0" typing-extensions = ">=4.6.0"
[package.extras] [package.extras]
@@ -1106,4 +901,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 = "40669246978dc70b09cd8fef22d81e8140d3474feeba1023d8178124914ebcf8" content-hash = "cc2947613c2711ad32ccfa1b8f04a0d8bad6043c6e52bffaff12761ec76cc805"

View File

@@ -9,18 +9,89 @@ 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"

3
pytest.ini Normal file
View File

@@ -0,0 +1,3 @@
[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
router = APIRouter() from src.api.v1 import router as v1_router
from src.core.settings import settings
router.include_router(router=users_router) router = APIRouter(prefix=settings.api.prefix)
router.include_router(router=tasks_router)
router.include_router(router=v1_router)

View File

@@ -0,0 +1,15 @@
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

@@ -1,38 +0,0 @@
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

@@ -1,21 +0,0 @@
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

@@ -0,0 +1,26 @@
from datetime import date
from typing import Annotated
from fastapi import Depends, Query
from fastapi.exceptions import HTTPException
from pydantic import BaseModel, model_validator
from src.schemas.tasks import TaskFilter
TaskFilterDep = Annotated[TaskFilter, 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

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

View File

@@ -0,0 +1,86 @@
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)]

View File

@@ -1,27 +0,0 @@
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): ...

View File

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

12
src/api/v1/__init__.py Normal file
View File

@@ -0,0 +1,12 @@
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)

28
src/api/v1/auth.py Normal file
View File

@@ -0,0 +1,28 @@
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

58
src/api/v1/tasks.py Normal file
View File

@@ -0,0 +1,58 @@
from typing import Annotated
from fastapi import APIRouter, Body, Depends
from src.api.dependacies.db_dep import sessionDep
from src.api.dependacies.task_dep import TaskFilterDep
from src.api.dependacies.user_dep import ActiveUser, TaskOwnerDep
from src.schemas.tasks import TaskADDRequest, TaskPATCHRequest
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, filter: TaskFilterDep):
result = await UserService(session).get_user_with_tasks(
user_id=user.id, **filter.model_dump(exclude_unset=True)
)
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.patch("/{id}")
async def patch_task(
session: sessionDep,
id: int,
_: TaskOwnerDep,
task_data: TaskPATCHRequest = Body(),
):
task = await TaskService(session).update_task(id, task_data)
return task
@router.delete("/{id}")
async def delete_task(
session: sessionDep,
id: int,
_: TaskOwnerDep,
):
await TaskService(session).delete_task(id)

49
src/api/v1/users.py Normal file
View File

@@ -0,0 +1,49 @@
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"}

0
src/core/__init__.py Normal file
View File

43
src/core/auth_manager.py Normal file
View File

@@ -0,0 +1,43 @@
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],
)

26
src/core/database.py Normal file
View File

@@ -0,0 +1,26 @@
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()
)

24
src/core/db_manager.py Normal file
View File

@@ -0,0 +1,24 @@
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()

23
src/core/interfaces.py Normal file
View File

@@ -0,0 +1,23 @@
from typing import TYPE_CHECKING, Any, Protocol
from sqlalchemy.ext.asyncio import AsyncSession
if TYPE_CHECKING:
from src.repository.tasks import TasksRepo
from src.repository.users import UsersRepo
class HasId(Protocol):
id: Any
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: ...

49
src/core/settings.py Normal file
View File

@@ -0,0 +1,49 @@
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()

View File

@@ -1,15 +0,0 @@
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,10 +1,14 @@
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() app = FastAPI(title="Task&Coffee")
app.include_router(router=router) app.include_router(router=router)
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run("src.main:app", port=5000, log_level="info", reload=True) uvicorn.run("src.main:app", port=8000, log_level="info", reload=True)

View File

@@ -1,11 +1,9 @@
from logging.config import fileConfig from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context from alembic import context
from sqlalchemy import engine_from_config, event, pool
from src.db.database import Base from src.core.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
@@ -67,6 +65,14 @@ 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,44 +0,0 @@
"""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,18 +1,18 @@
"""init """init
Revision ID: 932121e6b220 Revision ID: a2fdd0ec4a96
Revises: Revises:
Create Date: 2025-06-22 11:52:49.691545 Create Date: 2025-07-06 00:02:09.254907
""" """
from typing import Sequence, Union from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "932121e6b220" revision: str = "a2fdd0ec4a96"
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,11 +77,13 @@ 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,58 @@
"""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

@@ -0,0 +1,52 @@
"""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,9 +1,7 @@
from src.models.token import AccessToken
from src.models.users import UsersORM
from src.models.tasks import TasksORM from src.models.tasks import TasksORM
from src.models.users import UsersORM
__all__ = [ __all__ = [
"UsersORM", "UsersORM",
"TasksORM", "TasksORM",
"AccessToken",
] ]

View File

@@ -1,10 +1,10 @@
from datetime import date from datetime import date
from typing import Optional, TYPE_CHECKING from typing import TYPE_CHECKING, Optional
from sqlalchemy import ForeignKey, Text, Date, Enum, String from sqlalchemy import Date, Enum, ForeignKey, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from src.db.database import Base from src.core.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")) user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
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)

View File

@@ -1,13 +0,0 @@
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,22 +1,28 @@
from typing import Optional, TYPE_CHECKING from typing import TYPE_CHECKING, Optional
from fastapi_users_db_sqlalchemy import SQLAlchemyBaseUserTable from sqlalchemy import BigInteger, Boolean, Integer, String
from sqlalchemy import String, BigInteger, Integer
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from src.db.database import Base from src.core.database import Base
if TYPE_CHECKING: if TYPE_CHECKING:
from src.models.tasks import TasksORM from src.models.tasks import TasksORM
class UsersORM(SQLAlchemyBaseUserTable[int], Base): class UsersORM(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[Optional[str]] = mapped_column( username: Mapped[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)
tasks: Mapped[list["TasksORM"]] = relationship(back_populates="user") is_superuser: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
tasks: Mapped[list["TasksORM"]] = relationship(
back_populates="user", cascade="all, delete-orphan"
)

View File

57
src/repository/base.py Normal file
View File

@@ -0,0 +1,57 @@
from typing import Any, Generic, Mapping, Sequence, Type, TypeVar
from sqlalchemy import delete, insert, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from src.core.interfaces import HasId
ModelType = TypeVar("ModelType", bound=HasId)
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))
async def update_one(self, id: int, data: Mapping[str, Any]) -> ModelType:
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

6
src/repository/tasks.py Normal file
View File

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

92
src/repository/users.py Normal file
View File

@@ -0,0 +1,92 @@
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,
status: str | None = None,
tasks_limit: Optional[int] = None,
tasks_offset: Optional[int] = 0,
date_to: Optional[date] = None,
date_from: Optional[date] = None,
) -> UsersORM | None:
filters_sq: dict = {"user_id": user_id}
if status:
filters_sq["status"] = status
tasks_subquery = self._tasks_subquary(
date_from=date_from, date_to=date_to, **filters_sq
)
if tasks_limit is not None:
tasks_subquery = tasks_subquery.limit(tasks_limit)
if tasks_offset is not None and 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,
TasksORM.status,
)
)
)
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

12
src/schemas/auth.py Normal file
View File

@@ -0,0 +1,12 @@
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

@@ -0,0 +1,60 @@
from datetime import date
from enum import Enum
from pydantic import BaseModel, ConfigDict
class PriorityEnum(str, Enum):
low = "low"
medium = "medium"
high = "high"
critical = "critical"
class StatusEnum(str, Enum):
open = "open"
closed = "closed"
in_progress = "in_progress"
todo = "todo"
class TaskShort(BaseModel):
title: str
due_date: date | None = None
priority: PriorityEnum = PriorityEnum.medium
model_config = ConfigDict(from_attributes=True)
class TaskWithId(TaskShort):
id: int
status: str
class TaskADDRequest(TaskShort):
description: str | None = None
class TaskPATCHRequest(BaseModel):
title: str | None = None
description: str | None = None
due_date: date | None = None
status: StatusEnum | None = None
priority: PriorityEnum | None = None
time_spent: int | None = None
class Task(TaskADDRequest):
id: int
user_id: int
status: StatusEnum
time_spent: int
model_config = ConfigDict(from_attributes=True)
class TaskFilter(BaseModel):
status: StatusEnum | None = None
limit: int | None = 30
offset: int | None = None
date_from: date | None = None
date_to: date | None = None

View File

@@ -1,15 +1,47 @@
from typing import Optional from typing import Annotated
from fastapi_users import schemas from pydantic import BaseModel, BeforeValidator, ConfigDict, EmailStr
from src.schemas.tasks import TaskWithId
from src.schemas.validators import ensure_password
class UserRead(schemas.BaseUser[int]): class UserUpdate(BaseModel):
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 UserCreate(schemas.BaseUserCreate): class UserWithHashedPass(User):
hashed_password: str
class UserWithTasks(User):
tasks: list[TaskWithId]
class UserRequest(BaseModel):
username: str username: str
password: str
class UserUpdate(schemas.BaseUserUpdate): class UserRequestADD(BaseModel):
username: Optional[str] = None username: str
email: EmailStr | None = None
password: Annotated[str, BeforeValidator(ensure_password)]
class UserAdd(BaseModel):
email: EmailStr | None
username: str
hashed_password: str

11
src/schemas/validators.py Normal file
View File

@@ -0,0 +1,11 @@
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

0
src/services/__init__.py Normal file
View File

43
src/services/auth.py Normal file
View File

@@ -0,0 +1,43 @@
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
)

8
src/services/base.py Normal file
View File

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

38
src/services/tasks.py Normal file
View File

@@ -0,0 +1,38 @@
from fastapi import HTTPException
from src.models.tasks import TasksORM
from src.schemas.tasks import Task, TaskADDRequest, TaskPATCHRequest
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)
if task is None:
raise HTTPException(status_code=404, detail="Task not found.")
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()
async def update_task(
self, task_id: int, task_data: TaskPATCHRequest, exclude_unset: bool = True
):
task = await self.session.task.update_one(
id=task_id, data=task_data.model_dump(exclude_unset=exclude_unset)
)
await self.session.commit()
return Task.model_validate(task)

63
src/services/users.py Normal file
View File

@@ -0,0 +1,63 @@
from datetime import date
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,
status: str | None,
limit: int | None,
offset: int | None,
date_to: date | None,
date_from: date | None,
):
user = await self.session.user.get_one_with_load(
user_id=user_id,
status=status,
tasks_limit=limit,
tasks_offset=offset,
date_from=date_from,
date_to=date_to,
)
if user is None:
raise HTTPException(status_code=404, detail="User not found.")
return UserWithTasks.model_validate(user)

View File

@@ -1,10 +0,0 @@
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()

View File

@@ -1,34 +0,0 @@
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
)

0
tests/__init__.py Normal file
View File

78
tests/conftest.py Normal file
View File

@@ -0,0 +1,78 @@
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

View File

@@ -0,0 +1,28 @@
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")

38
tests/mock_tasks.json Normal file
View File

@@ -0,0 +1,38 @@
[
{
"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

View File

@@ -0,0 +1,10 @@
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

@@ -0,0 +1,57 @@
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
tasks = await db.task.get_filtered(user_id=admin_user.id)
assert tasks
user_with_tasks = await db.user.get_one_with_load(user_id=admin_user.id)
assert user_with_tasks
assert user_with_tasks.tasks