From f014b74552de20a56b9851782050acd874fbbdc7 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Wed, 23 Aug 2023 21:51:19 +0800 Subject: [PATCH 1/9] =?UTF-8?q?=E6=96=B0=E7=89=88=E6=9C=AC=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E6=8E=A8=E9=80=81=EF=BC=88=E5=9F=BA=E7=A1=80=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E5=8D=87=E7=BA=A7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 8 + .pre-commit-config.yaml | 25 ++ .ruff.toml | 45 ++++ Dockerfile | 23 ++ LICENSE | 21 ++ README.en.md | 36 --- README.md | 115 ++++++-- backend/app/.env.example | 14 + backend/app/__init__.py | 2 + backend/app/api/__init__.py | 2 + backend/app/api/routers.py | 13 + backend/app/api/v1/__init__.py | 2 + backend/app/api/v1/auth/__init__.py | 11 + backend/app/api/v1/auth/auth.py | 35 +++ backend/app/api/v1/auth/captcha.py | 22 ++ backend/app/api/v1/user.py | 100 +++++++ backend/app/common/__init__.py | 2 + backend/app/common/enums.py | 25 ++ backend/app/common/exception/__init__.py | 2 + backend/app/common/exception/errors.py | 80 ++++++ .../app/common/exception/exception_handler.py | 209 +++++++++++++++ backend/app/common/jwt.py | 103 ++++++++ backend/app/common/log.py | 53 ++++ backend/app/common/pagination.py | 84 ++++++ backend/app/common/redis.py | 44 +++ backend/app/common/response/__init__.py | 2 + backend/app/common/response/response_code.py | 25 ++ .../app/common/response/response_schema.py | 100 +++++++ backend/app/core/__init__.py | 2 + backend/app/core/conf.py | 106 ++++++++ backend/app/core/path_conf.py | 18 ++ backend/app/core/registrar.py | 138 ++++++++++ backend/app/crud/__init__.py | 2 + backend/app/crud/base.py | 55 ++++ backend/app/crud/crud_user.py | 88 ++++++ backend/app/database/__init__.py | 2 + backend/app/database/db_mysql.py | 29 ++ backend/app/main.py | 34 +++ backend/app/middleware/__init__.py | 2 + backend/app/middleware/access_middle.py | 21 ++ backend/app/migration.py | 18 ++ backend/app/models/__init__.py | 8 + backend/app/models/base.py | 25 ++ backend/app/models/user.py | 26 ++ backend/app/pyproject.toml | 4 + backend/app/schemas/__init__.py | 2 + backend/app/schemas/base.py | 166 ++++++++++++ backend/app/schemas/token.py | 14 + backend/app/schemas/user.py | 68 +++++ backend/app/services/__init__.py | 2 + backend/app/services/user_service.py | 250 ++++++++++++++++++ backend/app/utils/__init__.py | 2 + backend/app/utils/format_string.py | 15 ++ backend/app/utils/generate_string.py | 22 ++ backend/app/utils/health_check.py | 36 +++ backend/app/utils/re_verify.py | 43 +++ backend/app/utils/send_email.py | 43 +++ deploy/docker-compose/.env.server | 14 + deploy/docker-compose/docker-compose.yml | 80 ++++++ deploy/gunicorn.conf.py | 46 ++++ deploy/nginx.conf | 56 ++++ deploy/supervisor.conf | 165 ++++++++++++ pre-commit.sh | 3 + requirements.txt | 27 ++ 64 files changed, 2774 insertions(+), 61 deletions(-) create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .ruff.toml create mode 100644 Dockerfile create mode 100644 LICENSE delete mode 100644 README.en.md create mode 100644 backend/app/.env.example create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/api/routers.py create mode 100644 backend/app/api/v1/__init__.py create mode 100644 backend/app/api/v1/auth/__init__.py create mode 100644 backend/app/api/v1/auth/auth.py create mode 100644 backend/app/api/v1/auth/captcha.py create mode 100644 backend/app/api/v1/user.py create mode 100644 backend/app/common/__init__.py create mode 100644 backend/app/common/enums.py create mode 100644 backend/app/common/exception/__init__.py create mode 100644 backend/app/common/exception/errors.py create mode 100644 backend/app/common/exception/exception_handler.py create mode 100644 backend/app/common/jwt.py create mode 100644 backend/app/common/log.py create mode 100644 backend/app/common/pagination.py create mode 100644 backend/app/common/redis.py create mode 100644 backend/app/common/response/__init__.py create mode 100644 backend/app/common/response/response_code.py create mode 100644 backend/app/common/response/response_schema.py create mode 100644 backend/app/core/__init__.py create mode 100644 backend/app/core/conf.py create mode 100644 backend/app/core/path_conf.py create mode 100644 backend/app/core/registrar.py create mode 100644 backend/app/crud/__init__.py create mode 100644 backend/app/crud/base.py create mode 100644 backend/app/crud/crud_user.py create mode 100644 backend/app/database/__init__.py create mode 100644 backend/app/database/db_mysql.py create mode 100644 backend/app/main.py create mode 100644 backend/app/middleware/__init__.py create mode 100644 backend/app/middleware/access_middle.py create mode 100644 backend/app/migration.py create mode 100644 backend/app/models/__init__.py create mode 100644 backend/app/models/base.py create mode 100644 backend/app/models/user.py create mode 100644 backend/app/pyproject.toml create mode 100644 backend/app/schemas/__init__.py create mode 100644 backend/app/schemas/base.py create mode 100644 backend/app/schemas/token.py create mode 100644 backend/app/schemas/user.py create mode 100644 backend/app/services/__init__.py create mode 100644 backend/app/services/user_service.py create mode 100644 backend/app/utils/__init__.py create mode 100644 backend/app/utils/format_string.py create mode 100644 backend/app/utils/generate_string.py create mode 100644 backend/app/utils/health_check.py create mode 100644 backend/app/utils/re_verify.py create mode 100644 backend/app/utils/send_email.py create mode 100644 deploy/docker-compose/.env.server create mode 100644 deploy/docker-compose/docker-compose.yml create mode 100644 deploy/gunicorn.conf.py create mode 100644 deploy/nginx.conf create mode 100644 deploy/supervisor.conf create mode 100644 pre-commit.sh create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..057354b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +.idea/ +.env +venv/ +.mypy_cache/ +backend/app/log/ +backend/app/migrations/ +.ruff_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..ec9fbfb --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: check-yaml + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.285 + hooks: + - id: ruff + args: + - '--config' + - '.ruff.toml' + - '--fix' + - repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black + language_version: python3.10 + args: + - '--skip-string-normalization' + - '--line-length' + - '120' diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..bd45725 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,45 @@ +select = [ + "E", + "F", + "W505", + "PT018", + "Q000", + "SIM101", + "SIM114", + "PGH004", + "PLE1142", + "RUF100", + "I002", + "F404", + "TCH", + "UP007" +] +line-length = 120 +format = "grouped" +target-version = "py310" +cache-dir = "./.ruff_cache" + +[flake8-pytest-style] +mark-parentheses = false +parametrize-names-type = "list" +parametrize-values-row-type = "list" +parametrize-values-type = "tuple" + +[flake8-quotes] +avoid-escape = false +docstring-quotes = "double" +inline-quotes = "single" +multiline-quotes = "single" + +[flake8-unused-arguments] +ignore-variadic-names = true + +[isort] +lines-between-types = 1 +order-by-type = true + +[per-file-ignores] +"backend/app/api/v1/*.py" = ["TCH"] +"backend/app/models/*.py" = ["TCH003"] +"backend/app/**/__init__.py" = ["F401"] +"backend/app/tests/*.py" = ["E402"] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3218a3a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.10-slim + +WORKDIR /ftm + +COPY . . + +RUN sed -i s@/deb.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list \ + && sed -i s@/security.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list + +RUN apt-get update \ + && apt-get install -y --no-install-recommends gcc python3-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN pip install --upgrade pip -i https://mirrors.aliyun.com/pypi/simple \ + && pip install --no-cache-dir -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple + +ENV TZ = Asia/Shanghai + +RUN mkdir -p /var/log/fastapi_server + +EXPOSE 8001 + +CMD ["uvicorn", "backend.app.main:app", "--host", "127.0.0.1", "--port", "8000"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1226c69 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 xiaowu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.en.md b/README.en.md deleted file mode 100644 index 2c3fb49..0000000 --- a/README.en.md +++ /dev/null @@ -1,36 +0,0 @@ -# FastAutoTest - -#### Description -基于 fastapi + tortoise + mysql8 自动化接口测试平台 - -#### Software Architecture -Software architecture description - -#### Installation - -1. xxxx -2. xxxx -3. xxxx - -#### Instructions - -1. xxxx -2. xxxx -3. xxxx - -#### Contribution - -1. Fork the repository -2. Create Feat_xxx branch -3. Commit your code -4. Create Pull Request - - -#### Gitee Feature - -1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md -2. Gitee blog [blog.gitee.com](https://blog.gitee.com) -3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) -4. The most valuable open source project [GVP](https://gitee.com/gvp) -5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) -6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/README.md b/README.md index e565f82..f05becf 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,102 @@ -# FastAutoTest +# FastAPI Tortoise Architecture -#### 介绍 -基于 fastapi + tortoise + mysql8 自动化接口测试平台 +作为 FastAPI 框架的一个基础项目,基于 python3.10 开发 -#### 软件架构 -软件架构说明 +## 特征 +- [x] FastAPI > 0.100.0 +- [x] Async design +- [x] Restful API +- [x] Tortoise-orm > 0.20.0 +- [x] Pydantic 2.0 +- [x] Docker +- [ ] ...... -#### 安装教程 +## 使用 -1. xxxx -2. xxxx -3. xxxx +> ⚠️: 此过程请格外注意端口占用情况, 特别是 8000, 3306, 6379... -#### 使用说明 +### 1: 传统 -1. xxxx -2. xxxx -3. xxxx +1. 安装依赖项 -#### 参与贡献 + ```shell + pip install -r requirements.txt + ``` -1. Fork 本仓库 -2. 新建 Feat_xxx 分支 -3. 提交代码 -4. 新建 Pull Request +2. 创建一个数据库 `ftm`, 选择 utf8mb4 编码 +3. 安装启动 redis +4. 在 `backend/app/` 目录下创建一个 `.env` 文件 + ```shell + cd backend/app/ + touch .env + ``` -#### 特技 +5. 复制 `.env.example` 到 `.env` -1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md -2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) -3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 -4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 -5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) -6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) + ```shell + cp .env.example .env + ``` + +6. 数据库迁移 + + ```shell + cd backend/app + + # 初始化配置 + aerich init -t migration.TORTOISE_ORM + + # 初始化数据库,生成迁移文件 + aerich init-db + + # 执行迁移 + aerich upgrade + + # 当更新数据库 model 后,执行下面两个命令进行迁移 + aerich migrate + aerich upgrade + ``` + +7. 执行 backend/app/main.py 文件启动服务 +8. 浏览器访问: http://127.0.0.1:8000/api/v1/docs + +--- + +### 2: docker + +1. 进入 `docker-compose.yml` 文件所在目录,创建环境变量文件 `.env` + + ```shell + dcd deploy/docker-compose/ + + cp .env.server ../../backend/app/.env + ``` + +2. 执行一键启动命令 + + ```shell + docker-compose up -d --build + ``` + +3. 等待命令自动完成 +4. 浏览器访问:http://127.0.0.1:8000/api/v1/docs + +## 赞助 + +> 如果此项目能够帮助到你,你可以赞助作者一些咖啡豆表示鼓励 :coffee: + + + + + + + + +
+ +
微信支付宝
+ +## 许可证 + +本项目根据 MIT 许可证的条款进行许可 diff --git a/backend/app/.env.example b/backend/app/.env.example new file mode 100644 index 0000000..0d25232 --- /dev/null +++ b/backend/app/.env.example @@ -0,0 +1,14 @@ +# Env: dev、pro +ENVIRONMENT='dev' +# MySQL +DB_HOST='127.0.0.1' +DB_PORT=3306 +DB_USER='root' +DB_PASSWORD='123456' +# Redis +REDIS_HOST='127.0.0.1' +REDIS_PORT=6379 +REDIS_PASSWORD='' +REDIS_DATABASE=0 +# Token +TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk' diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/app/api/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/api/routers.py b/backend/app/api/routers.py new file mode 100644 index 0000000..455d2f4 --- /dev/null +++ b/backend/app/api/routers.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from fastapi import APIRouter + +from backend.app.api.v1.auth import router as auth_router +from backend.app.api.v1.user import router as user_router +from backend.app.core.conf import settings + +v1 = APIRouter(prefix=settings.API_V1_STR) + +v1.include_router(auth_router, prefix='/auth', tags=['认证']) + +v1.include_router(user_router, prefix='/users', tags=['用户']) diff --git a/backend/app/api/v1/__init__.py b/backend/app/api/v1/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/app/api/v1/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/api/v1/auth/__init__.py b/backend/app/api/v1/auth/__init__.py new file mode 100644 index 0000000..e08b54e --- /dev/null +++ b/backend/app/api/v1/auth/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from fastapi import APIRouter + +from backend.app.api.v1.auth.auth import router as auth_router +from backend.app.api.v1.auth.captcha import router as captcha_router + +router = APIRouter() + +router.include_router(auth_router) +router.include_router(captcha_router) diff --git a/backend/app/api/v1/auth/auth.py b/backend/app/api/v1/auth/auth.py new file mode 100644 index 0000000..12513ea --- /dev/null +++ b/backend/app/api/v1/auth/auth.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from fastapi import APIRouter, Depends, Request +from fastapi.security import OAuth2PasswordRequestForm + +from backend.app.common.jwt import DependsJwtUser +from backend.app.common.response.response_schema import response_base +from backend.app.schemas.token import Token +from backend.app.schemas.user import Auth, Auth2 +from backend.app.services.user_service import UserService + +router = APIRouter() + + +@router.post('/swagger_login', summary='swagger 表单登录', description='form 格式登录,仅用于 swagger 文档调试接口') +async def login1(form_data: OAuth2PasswordRequestForm = Depends()) -> Token: + token, user = await UserService.login_swagger(form_data) + return Token(access_token=token, user=user) + + +@router.post('/login', summary='json登录') +async def login2(obj: Auth) -> Token: + token, user = await UserService.login_json(obj) + return Token(access_token=token, user=user) + + +@router.post('/captcha_login', summary='验证码登录') +async def login3(request: Request, obj: Auth2) -> Token: + token, user = await UserService.login_captcha(obj=obj, request=request) + return Token(access_token=token, user=user) + + +@router.post('/logout', summary='登出', dependencies=[DependsJwtUser]) +async def user_logout(): + return await response_base.response_200() diff --git a/backend/app/api/v1/auth/captcha.py b/backend/app/api/v1/auth/captcha.py new file mode 100644 index 0000000..073190e --- /dev/null +++ b/backend/app/api/v1/auth/captcha.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from fast_captcha import img_captcha +from fastapi import APIRouter, Request, Depends +from fastapi_limiter.depends import RateLimiter +from starlette.concurrency import run_in_threadpool +from starlette.responses import StreamingResponse + +from backend.app.common.redis import redis_client +from backend.app.core.conf import settings +from backend.app.utils.generate_string import get_uuid_str + +router = APIRouter() + + +@router.get('/captcha', summary='获取验证码', dependencies=[Depends(RateLimiter(times=5, seconds=10))]) +async def get_captcha(request: Request): + img, code = await run_in_threadpool(img_captcha) + uuid = get_uuid_str() + request.app.state.captcha_uuid = uuid + await redis_client.set(uuid, code, settings.CAPTCHA_EXPIRATION_TIME) + return StreamingResponse(content=img, media_type='image/jpeg') diff --git a/backend/app/api/v1/user.py b/backend/app/api/v1/user.py new file mode 100644 index 0000000..b430dc2 --- /dev/null +++ b/backend/app/api/v1/user.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from fastapi import APIRouter, Request, Response, UploadFile + +from backend.app.common.jwt import CurrentUser, DependsJwtUser +from backend.app.common.pagination import PageDepends, paging_data +from backend.app.common.response.response_schema import response_base +from backend.app.schemas.user import CreateUser, GetUserInfo, ResetPassword, UpdateUser +from backend.app.services.user_service import UserService + +router = APIRouter() + + +@router.post('/register', summary='注册') +async def create_user(obj: CreateUser): + await UserService.register(obj) + return await response_base.success(msg='用户注册成功') + + +@router.post('/password/reset/code', summary='获取密码重置验证码', description='可以通过用户名或者邮箱重置密码') +async def password_reset_captcha(username_or_email: str, response: Response): + await UserService.get_pwd_rest_captcha(username_or_email=username_or_email, response=response) + return await response_base.success(msg='验证码发送成功') + + +@router.post('/password/reset', summary='密码重置请求') +async def password_reset(obj: ResetPassword, request: Request, response: Response): + await UserService.pwd_reset(obj=obj, request=request, response=response) + return await response_base.success(msg='密码重置成功') + + +@router.get('/password/reset/done', summary='重置密码完成') +async def password_reset_done(): + return await response_base.success(msg='重置密码完成') + + +@router.get('/{username}', summary='查看用户信息', dependencies=[DependsJwtUser]) +async def get_user_info(username: str): + current_user = await UserService.get_user_info(username) + return await response_base.success(data=current_user, exclude={'password'}) + + +@router.put('/{username}', summary='更新用户信息') +async def update_userinfo(username: str, obj: UpdateUser, current_user: CurrentUser): + count = await UserService.update(username=username, current_user=current_user, obj=obj) + if count > 0: + return await response_base.success(msg='更新用户信息成功') + return await response_base.fail() + + +@router.put('/{username}/avatar', summary='更新头像') +async def update_avatar(username: str, avatar: UploadFile, current_user: CurrentUser): + count = await UserService.update_avatar(username=username, current_user=current_user, avatar=avatar) + if count > 0: + return await response_base.success(msg='更新头像成功') + return await response_base.fail() + + +@router.delete('/{username}/avatar', summary='删除头像文件') +async def delete_avatar(username: str, current_user: CurrentUser): + count = await UserService.delete_avatar(username=username, current_user=current_user) + if count > 0: + return await response_base.success(msg='删除用户头像成功') + return await response_base.fail() + + +@router.get('', summary='获取所有用户', dependencies=[DependsJwtUser, PageDepends]) +async def get_all_users(): + data = await UserService.get_user_list() + page_data = await paging_data(data, GetUserInfo) + return await response_base.success(data=page_data) + + +@router.post('/{pk}/super', summary='修改用户超级权限', dependencies=[DependsJwtUser]) +async def super_set(pk: int): + count = await UserService.update_permission(pk) + if count > 0: + return await response_base.success(msg='修改超级权限成功') + return await response_base.fail() + + +@router.post('/{pk}/action', summary='修改用户状态', dependencies=[DependsJwtUser]) +async def status_set(pk: int): + count = await UserService.update_status(pk) + if count > 0: + return await response_base.success(msg='修改用户状态成功') + return await response_base.fail() + + +@router.delete( + '/{username}', + summary='用户注销', + description='用户注销 != 用户退出,注销之后用户将从数据库删除', + dependencies=[DependsJwtUser], +) +async def delete_user(username: str, current_user: CurrentUser): + count = await UserService.delete(username=username, current_user=current_user) + if count > 0: + return await response_base.success(msg='用户注销成功') + return await response_base.fail() diff --git a/backend/app/common/__init__.py b/backend/app/common/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/app/common/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/common/enums.py b/backend/app/common/enums.py new file mode 100644 index 0000000..cc1d145 --- /dev/null +++ b/backend/app/common/enums.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from enum import Enum + + +class EnumBase(Enum): + @classmethod + def get_member_values(cls): + return [item.value for item in cls._member_map_.values()] + + @classmethod + def get_member_names(cls): + return [name for name in cls._member_names_] + + +class IntEnum(int, EnumBase): + """整型枚举""" + + pass + + +class StrEnum(str, EnumBase): + """字符串枚举""" + + pass diff --git a/backend/app/common/exception/__init__.py b/backend/app/common/exception/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/app/common/exception/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/common/exception/errors.py b/backend/app/common/exception/errors.py new file mode 100644 index 0000000..80f5d84 --- /dev/null +++ b/backend/app/common/exception/errors.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from typing import Any + +from fastapi import HTTPException +from starlette.background import BackgroundTask + +from backend.app.common.response.response_code import CustomCode + + +class BaseExceptionMixin(Exception): + code: int + + def __init__(self, *, msg: str = None, data: Any = None, background: BackgroundTask | None = None): + self.msg = msg + self.data = data + # The original background task: https://www.starlette.io/background/ + self.background = background + + +class HTTPError(HTTPException): + def __init__(self, *, code: int, msg: Any = None, headers: dict[str, Any] | None = None): + super().__init__(status_code=code, detail=msg, headers=headers) + + +class CustomError(BaseExceptionMixin): + def __init__(self, *, error: CustomCode, data: Any = None, background: BackgroundTask | None = None): + self.code = error.code + super().__init__(msg=error.msg, data=data, background=background) + + +class RequestError(BaseExceptionMixin): + code = 400 + + def __init__(self, *, msg: str = 'Bad Request', data: Any = None, background: BackgroundTask | None = None): + super().__init__(msg=msg, data=data, background=background) + + +class ForbiddenError(BaseExceptionMixin): + code = 403 + + def __init__(self, *, msg: str = 'Forbidden', data: Any = None, background: BackgroundTask | None = None): + super().__init__(msg=msg, data=data, background=background) + + +class NotFoundError(BaseExceptionMixin): + code = 404 + + def __init__(self, *, msg: str = 'Not Found', data: Any = None, background: BackgroundTask | None = None): + super().__init__(msg=msg, data=data, background=background) + + +class ServerError(BaseExceptionMixin): + code = 500 + + def __init__( + self, *, msg: str = 'Internal Server Error', data: Any = None, background: BackgroundTask | None = None + ): + super().__init__(msg=msg, data=data, background=background) + + +class GatewayError(BaseExceptionMixin): + code = 502 + + def __init__(self, *, msg: str = 'Bad Gateway', data: Any = None, background: BackgroundTask | None = None): + super().__init__(msg=msg, data=data, background=background) + + +class AuthorizationError(BaseExceptionMixin): + code = 401 + + def __init__(self, *, msg: str = 'Permission denied', data: Any = None, background: BackgroundTask | None = None): + super().__init__(msg=msg, data=data, background=background) + + +class TokenError(HTTPError): + code = 401 + + def __init__(self, *, msg: str = 'Not authenticated', headers: dict[str, Any] | None = None): + super().__init__(code=self.code, msg=msg, headers=headers or {'WWW-Authenticate': 'Bearer'}) diff --git a/backend/app/common/exception/exception_handler.py b/backend/app/common/exception/exception_handler.py new file mode 100644 index 0000000..e89606a --- /dev/null +++ b/backend/app/common/exception/exception_handler.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from asgiref.sync import sync_to_async +from fastapi import FastAPI, Request +from fastapi.exceptions import RequestValidationError +from pydantic import ValidationError +from pydantic.errors import PydanticUserError +from starlette.exceptions import HTTPException +from starlette.middleware.cors import CORSMiddleware +from starlette.responses import JSONResponse +from uvicorn.protocols.http.h11_impl import STATUS_PHRASES + +from backend.app.common.exception.errors import BaseExceptionMixin +from backend.app.common.log import log +from backend.app.common.response.response_schema import response_base +from backend.app.core.conf import settings +from backend.app.schemas.base import ( + convert_validation_errors, + CUSTOM_VALIDATION_ERROR_MESSAGES, + convert_usage_errors, + CUSTOM_USAGE_ERROR_MESSAGES, +) + + +@sync_to_async +def _get_exception_code(status_code): + """ + 获取返回状态码, OpenAPI, Uvicorn... 可用状态码基于 RFC 定义, 详细代码见下方链接 + + `python 状态码标准支持 `__ + + `IANA 状态码注册表 `__ + + :param status_code: + :return: + """ + try: + STATUS_PHRASES[status_code] + except Exception: + code = 400 + else: + code = status_code + return code + + +async def _validation_exception_handler(e: RequestValidationError | ValidationError): + """ + 数据验证异常处理 + + :param e: + :return: + """ + + message = '' + errors = await convert_validation_errors(e, CUSTOM_VALIDATION_ERROR_MESSAGES) + for error in errors[:1]: + if error.get('type') == 'json_invalid': + message += 'json解析失败' + else: + error_input = error.get('input') + ctx = error.get('ctx') + ctx_error = ctx.get('error') if ctx else None + field = str(error.get('loc')[-1]) + error_msg = error.get('msg') + message += f'{field} {ctx_error if ctx else error_msg}: {error_input}' + '.' + content = { + 'code': 422, + 'msg': '请求参数非法' if len(message) == 0 else f'请求参数非法: {message}', + 'data': {'errors': e.errors()} if message == '' and settings.UVICORN_RELOAD is True else None, + } + return JSONResponse(status_code=422, content=await response_base.fail(**content)) + + +def register_exception(app: FastAPI): + @app.exception_handler(HTTPException) + async def http_exception_handler(request: Request, exc: HTTPException): + """ + 全局HTTP异常处理 + + :param request: + :param exc: + :return: + """ + content = {'code': exc.status_code, 'msg': exc.detail} + request.state.__request_http_exception__ = content # 用于在中间件中获取异常信息 + return JSONResponse( + status_code=await _get_exception_code(exc.status_code), + content=await response_base.fail(**content), + headers=exc.headers, + ) + + @app.exception_handler(RequestValidationError) + async def fastapi_validation_exception_handler(request: Request, exc: RequestValidationError): + """ + fastapi 数据验证异常处理 + + :param request: + :param exc: + :return: + """ + return await _validation_exception_handler(exc) + + @app.exception_handler(ValidationError) + async def pydantic_validation_exception_handler(request: Request, exc: ValidationError): + """ + pydantic 数据验证异常处理 + + :param request: + :param exc: + :return: + """ + return await _validation_exception_handler(exc) + + @app.exception_handler(PydanticUserError) + async def pydantic_user_error_handler(request: Request, exc: PydanticUserError): + """ + Pydantic 用户异常处理 + + :param request: + :param exc: + :return: + """ + return JSONResponse( + status_code=500, + content=await response_base.fail( + code=exc.code, msg=await convert_usage_errors(exc, CUSTOM_USAGE_ERROR_MESSAGES) + ), + ) + + @app.exception_handler(Exception) + async def all_exception_handler(request: Request, exc: Exception): + """ + 全局异常处理 + + :param request: + :param exc: + :return: + """ + if isinstance(exc, BaseExceptionMixin): + return JSONResponse( + status_code=await _get_exception_code(exc.code), + content=await response_base.fail(code=exc.code, msg=str(exc.msg), data=exc.data if exc.data else None), + background=exc.background, + ) + + elif isinstance(exc, AssertionError): + return JSONResponse( + status_code=500, + content=await response_base.fail( + code=500, + msg=','.join(exc.args) + if exc.args + else exc.__repr__() + if not exc.__repr__().startswith('AssertionError()') + else exc.__doc__, + ) + if settings.ENVIRONMENT == 'dev' + else await response_base.fail(code=500, msg='Internal Server Error'), + ) + + else: + import traceback + + log.error(f'未知异常: {exc}') + log.error(traceback.format_exc()) + return JSONResponse( + status_code=500, + content=await response_base.fail(code=500, msg=str(exc)) + if settings.ENVIRONMENT == 'dev' + else await response_base.fail(code=500, msg='Internal Server Error'), + ) + + if settings.MIDDLEWARE_CORS: + + @app.exception_handler(500) + async def cors_status_code_500_exception_handler(request, exc): + """ + 跨域 500 异常处理 + + `Related issue `_ + + :param request: + :param exc: + :return: + """ + response = JSONResponse( + status_code=exc.code if isinstance(exc, BaseExceptionMixin) else 500, + content={'code': exc.code, 'msg': exc.msg, 'data': exc.data} + if isinstance(exc, BaseExceptionMixin) + else await response_base.fail(code=500, msg=str(exc)) + if settings.ENVIRONMENT == 'dev' + else await response_base.fail(code=500, msg='Internal Server Error'), + background=exc.background if isinstance(exc, BaseExceptionMixin) else None, + ) + origin = request.headers.get('origin') + if origin: + cors = CORSMiddleware( + app=app, allow_origins=['*'], allow_credentials=True, allow_methods=['*'], allow_headers=['*'] + ) + response.headers.update(cors.simple_headers) + has_cookie = 'cookie' in request.headers + if cors.allow_all_origins and has_cookie: + response.headers['Access-Control-Allow-Origin'] = origin + elif not cors.allow_all_origins and cors.is_allowed_origin(origin=origin): + response.headers['Access-Control-Allow-Origin'] = origin + response.headers.add_vary_header('Origin') + return response diff --git a/backend/app/common/jwt.py b/backend/app/common/jwt.py new file mode 100644 index 0000000..ec51e4a --- /dev/null +++ b/backend/app/common/jwt.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from datetime import datetime, timedelta +from typing import Any + +from asgiref.sync import sync_to_async +from fastapi import Depends +from fastapi.security import OAuth2PasswordBearer +from jose import jwt +from passlib.context import CryptContext +from pydantic import ValidationError +from typing_extensions import Annotated + +from backend.app.common.exception.errors import TokenError, AuthorizationError +from backend.app.core.conf import settings +from backend.app.crud.crud_user import UserDao +from backend.app.models.user import User + +pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto') + +oauth2_schema = OAuth2PasswordBearer(tokenUrl=settings.TOKEN_URL_SWAGGER) + + +@sync_to_async +def get_hash_password(password: str) -> str: + """ + 使用hash算法加密密码 + + :param password: 密码 + :return: 加密后的密码 + """ + return pwd_context.hash(password) + + +@sync_to_async +def password_verify(plain_password: str, hashed_password: str) -> bool: + """ + 密码校验 + + :param plain_password: 要验证的密码 + :param hashed_password: 要比较的hash密码 + :return: 比较密码之后的结果 + """ + return pwd_context.verify(plain_password, hashed_password) + + +@sync_to_async +def create_access_token(data: int | Any, expires_delta: timedelta | None = None) -> str: + """ + 生成加密 token + + :param data: 传进来的值 + :param expires_delta: 增加的到期时间 + :return: 加密token + """ + if expires_delta: + expires = datetime.utcnow() + expires_delta + else: + expires = datetime.utcnow() + timedelta(settings.TOKEN_EXPIRE_MINUTES) + to_encode = {'exp': expires, 'sub': str(data)} + encoded_jwt = jwt.encode(to_encode, settings.TOKEN_SECRET_KEY, settings.TOKEN_ALGORITHM) + return encoded_jwt + + +async def get_current_user(token: str = Depends(oauth2_schema)) -> User: + """ + 通过token获取当前用户 + + :param token: + :return: + """ + try: + # 解密token + payload = jwt.decode(token, settings.TOKEN_SECRET_KEY, algorithms=[settings.TOKEN_ALGORITHM]) + user_id = payload.get('sub') + if not user_id: + raise TokenError + except (jwt.JWTError, ValidationError): + raise TokenError + user = await UserDao.get_user_by_id(user_id) + if not user: + raise TokenError + return user + + +@sync_to_async +def superuser_verify(user: User): + """ + 验证当前用户是否为超级用户 + + :param user: + :return: + """ + is_superuser = user.is_superuser + if not is_superuser: + raise AuthorizationError + return is_superuser + + +# 用户依赖注入 +CurrentUser = Annotated[User, Depends(get_current_user)] +# 权限依赖注入 +DependsJwtUser = Depends(get_current_user) diff --git a/backend/app/common/log.py b/backend/app/common/log.py new file mode 100644 index 0000000..a3968ea --- /dev/null +++ b/backend/app/common/log.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from __future__ import annotations + +import os +from typing import TYPE_CHECKING + +from loguru import logger + +from backend.app.core import path_conf +from backend.app.core.conf import settings + +if TYPE_CHECKING: + import loguru + + +class Logger: + def __init__(self): + self.log_path = path_conf.LogPath + + def log(self) -> loguru.Logger: + if not os.path.exists(self.log_path): + os.mkdir(self.log_path) + + # 日志文件 + log_stdout_file = os.path.join(self.log_path, settings.LOG_STDOUT_FILENAME) + log_stderr_file = os.path.join(self.log_path, settings.LOG_STDERR_FILENAME) + + # loguru 日志: https://loguru.readthedocs.io/en/stable/api/logger.html#loguru._logger.Logger.add + log_config = dict(rotation='10 MB', retention='15 days', compression='tar.gz', enqueue=True) + # stdout + logger.add( + log_stdout_file, + level='INFO', + filter=lambda record: record['level'].name == 'INFO' or record['level'].no <= 25, + **log_config, + backtrace=False, + diagnose=False, + ) + # stderr + logger.add( + log_stderr_file, + level='ERROR', + filter=lambda record: record['level'].name == 'ERROR' or record['level'].no >= 30, + **log_config, + backtrace=True, + diagnose=True, + ) + + return logger + + +log = Logger().log() diff --git a/backend/app/common/pagination.py b/backend/app/common/pagination.py new file mode 100644 index 0000000..c91d6d4 --- /dev/null +++ b/backend/app/common/pagination.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from __future__ import annotations + +import math +from typing import TypeVar, Generic, Sequence, TYPE_CHECKING + +from fastapi import Query, Depends +from fastapi_pagination import pagination_ctx +from fastapi_pagination.bases import AbstractPage, AbstractParams, RawParams +from fastapi_pagination.ext.tortoise import paginate +from fastapi_pagination.links.bases import create_links +from pydantic import BaseModel + +if TYPE_CHECKING: + from tortoise.queryset import QuerySet + +T = TypeVar('T') +DataT = TypeVar('DataT') +SchemaT = TypeVar('SchemaT') + + +class _Params(BaseModel, AbstractParams): + page: int = Query(1, ge=1, description='Page number') + size: int = Query(20, gt=0, le=100, description='Page size') # 默认 20 条记录 + + def to_raw_params(self) -> RawParams: + return RawParams( + limit=self.size, + offset=self.size * (self.page - 1), + ) + + +class _Page(AbstractPage[T], Generic[T]): + items: Sequence[T] # 数据 + total: int # 总数据数 + page: int # 第n页 + size: int # 每页数量 + total_pages: int # 总页数 + links: dict[str, str | None] # 跳转链接 + + __params_type__ = _Params # 使用自定义的Params + + @classmethod + def create( + cls, + items: Sequence[T], + total: int, + params: _Params, + ) -> _Page[T]: + page = params.page + size = params.size + total_pages = math.ceil(total / params.size) + links = create_links( + **{ + 'first': {'page': 1, 'size': f'{size}'}, + 'last': {'page': f'{math.ceil(total / params.size)}', 'size': f'{size}'} if total > 0 else None, + 'next': {'page': f'{page + 1}', 'size': f'{size}'} if (page + 1) <= total_pages else None, + 'prev': {'page': f'{page - 1}', 'size': f'{size}'} if (page - 1) >= 1 else None, + } + ).dict() + + return cls(items=items, total=total, page=params.page, size=params.size, total_pages=total_pages, links=links) + + +class _PageData(BaseModel, Generic[DataT]): + page_data: DataT | None = None + + +async def paging_data(query_set: QuerySet, page_data_schema: SchemaT) -> dict: + """ + 基于 Tortoise 创建分页数据 + + :param query_set: + :param page_data_schema: + :return: + """ + _paginate = await paginate(query_set) + page_data = _PageData[_Page[page_data_schema]](page_data=_paginate).model_dump()['page_data'] + return page_data + + +# 分页依赖注入 +PageDepends = Depends(pagination_ctx(_Page)) diff --git a/backend/app/common/redis.py b/backend/app/common/redis.py new file mode 100644 index 0000000..ad744c3 --- /dev/null +++ b/backend/app/common/redis.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys + +from redis.asyncio.client import Redis +from redis.exceptions import TimeoutError, AuthenticationError + +from backend.app.common.log import log +from backend.app.core.conf import settings + + +class RedisCli(Redis): + def __init__(self): + super(RedisCli, self).__init__( + host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + password=settings.REDIS_PASSWORD, + db=settings.REDIS_DATABASE, + socket_timeout=settings.REDIS_TIMEOUT, + decode_responses=True, # 转码 utf-8 + ) + + async def open(self): + """ + 触发初始化连接 + + :return: + """ + try: + await self.ping() + except TimeoutError: + log.error('❌ 数据库 redis 连接超时') + sys.exit() + except AuthenticationError: + log.error('❌ 数据库 redis 连接认证失败') + sys.exit() + except Exception as e: + log.error('❌ 数据库 redis 连接异常 {}', e) + sys.exit() + + +# 创建redis连接对象 +redis_client = RedisCli() diff --git a/backend/app/common/response/__init__.py b/backend/app/common/response/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/app/common/response/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/common/response/response_code.py b/backend/app/common/response/response_code.py new file mode 100644 index 0000000..ae341e0 --- /dev/null +++ b/backend/app/common/response/response_code.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from enum import Enum + + +class CustomCode(Enum): + """ + 自定义错误码 + """ + + CAPTCHA_ERROR = (40001, '验证码错误') + + @property + def code(self): + """ + 获取错误码 + """ + return self.value[0] + + @property + def msg(self): + """ + 获取错误码码信息 + """ + return self.value[1] diff --git a/backend/app/common/response/response_schema.py b/backend/app/common/response/response_schema.py new file mode 100644 index 0000000..9defb1d --- /dev/null +++ b/backend/app/common/response/response_schema.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from typing import Any + +from asgiref.sync import sync_to_async +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel, validate_call + + +_ExcludeData = set[int | str] | dict[int | str, Any] + +__all__ = ['ResponseModel', 'response_base'] + + +class ResponseModel(BaseModel): + """ + 统一返回模型 + + .. tip:: + + 如果你不想使用 ResponseBase 中的自定义编码器,可以使用此模型,返回数据将通过 fastapi 内部的编码器直接自动解析并返回 + + E.g. :: + + @router.get('/test', response_model=ResponseModel) + def test(): + return ResponseModel(data={'test': 'test'}) + + @router.get('/test') + def test() -> ResponseModel: + return ResponseModel(data={'test': 'test'}) + """ # noqa: E501 + + # model_config = ConfigDict(json_encoders={datetime: lambda x: x.strftime(settings.DATETIME_FORMAT)}) + + code: int = 200 + msg: str = 'Success' + data: Any | None = None + + +class ResponseBase: + """ + 统一返回方法 + + .. tip:: + + 此类中的返回方法将通过自定义编码器预解析,然后由 fastapi 内部的编码器再次处理并返回,可能存在性能损耗,取决于个人喜好 + + E.g. :: + + @router.get('/test') + def test(): + return await response_base.success(data={'test': 'test'}) + """ # noqa: E501 + + @staticmethod + @sync_to_async + def __json_encoder(data: Any, exclude: _ExcludeData | None = None, **kwargs): + # custom_encoder = {datetime: lambda x: x.strftime(settings.DATETIME_FORMAT)} + # kwargs.update({'custom_encoder': custom_encoder}) + result = jsonable_encoder(data, exclude=exclude, **kwargs) + return result + + @validate_call + async def success( + self, + *, + code: int = 200, + msg: str = 'Success', + data: Any | None = None, + exclude: _ExcludeData | None = None, + **kwargs + ) -> dict: + """ + 请求成功返回通用方法 + + :param code: 返回状态码 + :param msg: 返回信息 + :param data: 返回数据 + :param exclude: 排除返回数据(data)字段 + :return: + """ + data = data if data is None else await self.__json_encoder(data, exclude, **kwargs) + return {'code': code, 'msg': msg, 'data': data} + + @validate_call + async def fail( + self, + *, + code: int = 400, + msg: str = 'Bad Request', + data: Any = None, + exclude: _ExcludeData | None = None, + **kwargs + ) -> dict: + data = data if data is None else await self.__json_encoder(data, exclude, **kwargs) + return {'code': code, 'msg': msg, 'data': data} + + +response_base = ResponseBase() diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/app/core/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/core/conf.py b/backend/app/core/conf.py new file mode 100644 index 0000000..1e143f8 --- /dev/null +++ b/backend/app/core/conf.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from functools import lru_cache + +from pydantic import model_validator +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', case_sensitive=True) + + # Env Config + ENVIRONMENT: str = 'dev' + + # Env MySQL + DB_HOST: str + DB_PORT: int + DB_USER: str + DB_PASSWORD: str + + # Env Redis + REDIS_HOST: str + REDIS_PORT: int + REDIS_PASSWORD: str + REDIS_DATABASE: int + + # Env Token + TOKEN_SECRET_KEY: str # 密钥 secrets.token_urlsafe(32) + + # FastAPI + API_V1_STR: str = '/api/v1' + TITLE: str = 'FastAPI' + VERSION: str = '0.0.1' + DESCRIPTION: str = 'FastAPI Tortoise MySQL' + DOCS_URL: str | None = f'{API_V1_STR}/docs' + REDOCS_URL: str | None = f'{API_V1_STR}/redocs' + OPENAPI_URL: str | None = f'{API_V1_STR}/openapi' + + @model_validator(mode='before') + @classmethod + def validator_api_url(cls, values): + if values['ENVIRONMENT'] == 'pro': + values['OPENAPI_URL'] = None + return values + + # Static Server + STATIC_FILE: bool = True + + # Limiter + LIMITER_REDIS_PREFIX: str = 'fba_limiter' + + # Uvicorn + UVICORN_HOST: str = '127.0.0.1' + UVICORN_PORT: int = 8000 + UVICORN_RELOAD: bool = True + + # DB + DB_AUTO_GENERATE_SCHEMAS: bool = True # 自动创建表 + DB_ECHO: bool = False + DB_DATABASE: str = 'ftm' + DB_ENCODING: str = 'utf8mb4' + DB_TIMEZONE: str = 'Asia/Shanghai' + + # DateTime + DATETIME_TIMEZONE: str = 'Asia/Shanghai' + DATETIME_FORMAT: str = '%Y-%m-%d %H:%M:%S' + + # Redis + REDIS_TIMEOUT: int = 10 + + # Captcha + CAPTCHA_EXPIRATION_TIME: int = 60 * 5 # 过期时间,单位:秒 + + # Log + LOG_STDOUT_FILENAME: str = 'ftm_access.log' + LOG_STDERR_FILENAME: str = 'ftm_error.log' + + # Token + TOKEN_ALGORITHM: str = 'HS256' + TOKEN_URL_SWAGGER: str = f'{API_V1_STR}/auth/swagger_login' + TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 3 # 单位:m + + # Email + EMAIL_DESCRIPTION: str = 'fastapi_tortoise_mysql' # 默认发件说明 + EMAIL_SERVER: str = 'smtp.qq.com' + EMAIL_PORT: int = 465 + EMAIL_USER: str = 'xxxxx-nav@qq.com' + EMAIL_PASSWORD: str = '' # 授权密码,非邮箱密码 + EMAIL_SSL: bool = True # 是否使用ssl + + # Cookies + COOKIES_MAX_AGE: int = 60 * 5 # 过期时间,单位:秒 + + # 中间件 + MIDDLEWARE_CORS: bool = True + MIDDLEWARE_GZIP: bool = True + MIDDLEWARE_ACCESS: bool = False + + +@lru_cache +def get_settings(): + """读取配置优化写法""" + return Settings() + + +settings = get_settings() diff --git a/backend/app/core/path_conf.py b/backend/app/core/path_conf.py new file mode 100644 index 0000000..f13621e --- /dev/null +++ b/backend/app/core/path_conf.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import os + +from pathlib import Path + +# 获取项目根目录 +# 或使用绝对路径,指到backend目录为止:BasePath = D:\git_project\FastAutoTest\backend +BasePath = Path(__file__).resolve().parent.parent.parent + +# 日志文件路径 +LogPath = os.path.join(BasePath, 'app', 'log') + +# 图片上传存放路径: /static/media/uploads/ +ImgPath = os.path.join(BasePath, 'app', 'static', 'media', 'uploads') + +# 头像上传存放路径: /static/media/uploads/avatars/ +AvatarPath = os.path.join(ImgPath, 'avatars', '') diff --git a/backend/app/core/registrar.py b/backend/app/core/registrar.py new file mode 100644 index 0000000..905d24e --- /dev/null +++ b/backend/app/core/registrar.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from fastapi import FastAPI +from fastapi_limiter import FastAPILimiter +from fastapi_pagination import add_pagination +from starlette.middleware.cors import CORSMiddleware +from starlette.middleware.gzip import GZipMiddleware +from tortoise.contrib.fastapi import register_tortoise + +from backend.app.api.routers import v1 +from backend.app.common.exception.exception_handler import register_exception +from backend.app.common.redis import redis_client +from backend.app.core.conf import settings +from backend.app.database.db_mysql import mysql_config +from backend.app.middleware.access_middle import AccessMiddleware +from backend.app.utils.health_check import http_limit_callback, ensure_unique_route_names + + +def register_app(): + # FastAPI + app = FastAPI( + title=settings.TITLE, + version=settings.VERSION, + description=settings.DESCRIPTION, + docs_url=settings.DOCS_URL, + redoc_url=settings.REDOCS_URL, + openapi_url=settings.OPENAPI_URL, + ) + + # 注册静态文件 + register_static_file(app) + + # 中间件 + register_middleware(app) + + # 路由 + register_router(app) + + # 初始化 + register_init(app) + + # 数据库 + register_db(app) + + # 分页 + register_page(app) + + # 全局异常处理 + register_exception(app) + + return app + + +def register_static_file(app: FastAPI): + """ + 静态文件交互开发模式, 生产使用 nginx 静态资源服务 + + :param app: + :return: + """ + if settings.STATIC_FILE: + import os + from fastapi.staticfiles import StaticFiles + + if not os.path.exists('./static'): + os.mkdir('./static') + app.mount('/static', StaticFiles(directory='static'), name='static') + + +def register_middleware(app) -> None: + # 跨域 + if settings.MIDDLEWARE_CORS: + app.add_middleware( + CORSMiddleware, + allow_origins=['*'], + allow_credentials=True, + allow_methods=['*'], + allow_headers=['*'], + ) + # gzip + if settings.MIDDLEWARE_GZIP: + app.add_middleware(GZipMiddleware) + # 接口访问日志 + if settings.MIDDLEWARE_ACCESS: + app.add_middleware(AccessMiddleware) + + +def register_router(app: FastAPI): + """ + 路由 + + :param app: FastAPI + :return: + """ + app.include_router(v1) + + # Extra + ensure_unique_route_names(app) + + +def register_init(app: FastAPI): + """ + 初始化连接 + + :param app: FastAPI + :return: + """ + + @app.on_event('startup') + async def startup(): + # 连接redis + await redis_client.open() + # 初始化 limiter + await FastAPILimiter.init(redis_client, prefix=settings.LIMITER_REDIS_PREFIX, http_callback=http_limit_callback) + + @app.on_event('shutdown') + async def shutdown(): + # 关闭redis连接 + await redis_client.close() + + +def register_db(app: FastAPI): + register_tortoise( + app, + config=mysql_config, + generate_schemas=settings.DB_AUTO_GENERATE_SCHEMAS, + ) + + +def register_page(app: FastAPI): + """ + 分页查询 + + :param app: + :return: + """ + add_pagination(app) diff --git a/backend/app/crud/__init__.py b/backend/app/crud/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/app/crud/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/crud/base.py b/backend/app/crud/base.py new file mode 100644 index 0000000..f7a1a4f --- /dev/null +++ b/backend/app/crud/base.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from typing import TypeVar, Generic, Type, Any, Dict + +from asgiref.sync import sync_to_async +from pydantic import BaseModel +from tortoise import Model +from tortoise.queryset import QuerySet + +ModelType = TypeVar('ModelType', bound=Model) +CreateSchemaType = TypeVar('CreateSchemaType', bound=BaseModel) +UpdateSchemaType = TypeVar('UpdateSchemaType', bound=BaseModel) + + +class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): + def __init__(self, model: Type[ModelType]): + self.model = model + + async def get(self, pk: int) -> ModelType: + return await self.model.filter(id=pk).first() + + async def get_or_none(self, pk: int) -> ModelType | None: + return await self.model.get_or_none(id=pk) + + @sync_to_async + def get_all(self) -> QuerySet[ModelType]: + return self.model.all() + + async def get_values(self, pk: int, *args: str, **kwargs: str) -> list[dict] | dict: + return await self.model.get(id=pk).values(*args, **kwargs) + + async def get_values_list(self, pk: int, *fields: str, flat: bool = False) -> list[Any] | tuple: + return await self.model.get(id=pk).values_list(*fields, flat=flat) + + async def create(self, obj_in: CreateSchemaType, user_id: int | None = None) -> ModelType: + if user_id: + model = self.model(**obj_in.model_dump(), create_user=user_id) + await model.save() + else: + model = await self.model.create(**obj_in.model_dump()) + return model + + async def update(self, pk: int, obj_in: UpdateSchemaType | Dict[str, Any], user_id: int | None = None) -> int: + if isinstance(obj_in, dict): + update_data = obj_in + else: + update_data = obj_in.model_dump() + if user_id: + update_data.update({'update_user': user_id}) + count = await self.model.filter(id=pk).update(**update_data) + return count + + async def delete(self, pk: int) -> int: + count = await self.model.filter(id=pk).delete() + return count diff --git a/backend/app/crud/crud_user.py b/backend/app/crud/crud_user.py new file mode 100644 index 0000000..aa53710 --- /dev/null +++ b/backend/app/crud/crud_user.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from tortoise import timezone +from tortoise.transactions import atomic + +from backend.app.common import jwt +from backend.app.crud.base import CRUDBase +from backend.app.models.user import User +from backend.app.schemas.user import CreateUser, UpdateUser + + +class CRUDUser(CRUDBase[User, CreateUser, UpdateUser]): + async def get_user_by_id(self, pk: int) -> User: + return await self.get(pk) + + async def get_user_by_username(self, name: str) -> User: + return await self.model.filter(username=name).first() + + @atomic() + async def update_user_login_time(self, pk: int) -> int: + return await self.model.filter(id=pk).update(last_login_time=timezone.now()) + + async def check_email(self, email: str) -> bool: + return await self.model.filter(email=email).exists() + + @atomic() + async def register_user(self, user: CreateUser) -> User: + user.password = await jwt.get_hash_password(user.password) + user = await self.create(user) + return user + + async def get_email_by_username(self, username: str) -> str: + user = await self.model.filter(username=username).first() + return user.email + + async def get_username_by_email(self, email: str) -> str: + user = await self.model.filter(email=email).first() + return user.username + + async def get_avatar_by_username(self, username: str) -> str: + user = await self.get_user_by_username(username) + return user.avatar + + @atomic() + async def reset_password(self, username: str, password: str) -> int: + new_password = await jwt.get_hash_password(password) + return await self.model.filter(username=username).update(password=new_password) + + @atomic() + async def update_userinfo(self, current_user: User, obj_in: UpdateUser) -> int: + return await self.update(current_user.pk, obj_in) + + @atomic() + async def update_avatar(self, current_user: User, avatar: str): + return await self.update(current_user.pk, {'avatar': avatar}) + + async def get_avatar_by_pk(self, pk: int): + user = await self.get(pk) + return user.avatar + + @atomic() + async def delete_avatar(self, pk: int) -> int: + return await self.model.filter(id=pk).update(avatar=None) + + async def get_user_super_status(self, pk: int) -> bool: + user = await self.get(pk) + return user.is_superuser + + async def get_user_active_status(self, pk: int) -> bool: + user = await self.get(pk) + return user.status + + @atomic() + async def super_set(self, pk: int) -> int: + super_status = await self.get_user_super_status(pk) + return await self.model.filter(id=pk).update(is_superuser=False if super_status else True) + + @atomic() + async def status_set(self, pk: int) -> int: + status = await self.get_user_active_status(pk) + return await self.model.filter(id=pk).update(status=False if status else True) + + @atomic() + async def delete_user(self, pk: int) -> int: + return await self.delete(pk) + + +UserDao: CRUDUser = CRUDUser(User) diff --git a/backend/app/database/__init__.py b/backend/app/database/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/app/database/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/database/db_mysql.py b/backend/app/database/db_mysql.py new file mode 100644 index 0000000..c3aa8fd --- /dev/null +++ b/backend/app/database/db_mysql.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from backend.app.core.conf import settings +from backend.app.models import models + +mysql_config = { + 'connections': { + 'default': { + 'engine': 'tortoise.backends.mysql', + 'credentials': { + 'host': f'{settings.DB_HOST}', + 'port': settings.DB_PORT, + 'user': f'{settings.DB_USER}', + 'password': f'{settings.DB_PASSWORD}', + 'database': f'{settings.DB_DATABASE}', + 'charset': f'{settings.DB_ENCODING}', + 'echo': settings.DB_ECHO, + }, + }, + }, + 'apps': { + 'ftm': { + 'models': [*models], + 'default_connection': 'default', + }, + }, + 'use_tz': False, + 'timezone': settings.DB_TIMEZONE, +} diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..bb83179 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import uvicorn +from path import Path + +from backend.app.common.log import log +from backend.app.core.conf import settings +from backend.app.core.registrar import register_app + +app = register_app() + +if __name__ == '__main__': + try: + log.info( + """\n + /$$$$$$$$ /$$ /$$$$$$ /$$$$$$$ /$$$$$$ +| $$_____/ | $$ /$$__ $$| $$__ $$|_ $$_/ +| $$ /$$$$$$ /$$$$$$$ /$$$$$$ | $$ | $$| $$ | $$ | $$ +| $$$$$|____ $$ /$$_____/|_ $$_/ | $$$$$$$$| $$$$$$$/ | $$ +| $$__/ /$$$$$$$| $$$$$$ | $$ | $$__ $$| $$____/ | $$ +| $$ /$$__ $$ |____ $$ | $$ /$$| $$ | $$| $$ | $$ +| $$ | $$$$$$$ /$$$$$$$/ | $$$$/| $$ | $$| $$ /$$$$$$ +|__/ |_______/|_______/ |___/ |__/ |__/|__/ |______/ + + """ + ) + uvicorn.run( + app=f'{Path(__file__).stem}:app', + host=settings.UVICORN_HOST, + port=settings.UVICORN_PORT, + reload=settings.UVICORN_RELOAD, + ) + except Exception as e: + log.error(f'❌ FastAPI start filed: {e}') diff --git a/backend/app/middleware/__init__.py b/backend/app/middleware/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/app/middleware/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/middleware/access_middle.py b/backend/app/middleware/access_middle.py new file mode 100644 index 0000000..0682d6a --- /dev/null +++ b/backend/app/middleware/access_middle.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from datetime import datetime + +from fastapi import Request, Response +from starlette.middleware.base import BaseHTTPMiddleware + +from backend.app.common.log import log + + +class AccessMiddleware(BaseHTTPMiddleware): + """ + 记录请求日志 + """ + + async def dispatch(self, request: Request, call_next) -> Response: + start_time = datetime.now() + response = await call_next(request) + end_time = datetime.now() + log.info(f'{response.status_code} {request.client.host} {request.method} {request.url} {end_time - start_time}') + return response diff --git a/backend/app/migration.py b/backend/app/migration.py new file mode 100644 index 0000000..dc2a60a --- /dev/null +++ b/backend/app/migration.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys + +sys.path.append('../../') + +from backend.app.database.db_mysql import mysql_config # noqa: E402 +from backend.app.models import models # noqa: E402 + +TORTOISE_ORM = { + 'connections': mysql_config['connections'], + 'apps': { + 'ftm': { + 'models': ['aerich.models', *models], + 'default_connection': 'default', + }, + }, +} diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..9cb60b3 --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from backend.app.models import user + +# 新增model后,在list引入文件,而不是model类 +models = [ + user, +] diff --git a/backend/app/models/base.py b/backend/app/models/base.py new file mode 100644 index 0000000..8d3ebc1 --- /dev/null +++ b/backend/app/models/base.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from tortoise import Model, fields + + +class UserMixin: + """ + 用户 Mixin 数据类 + """ + + create_user = fields.BigIntField(null=False, verbose_name='创建者') + update_user = fields.BigIntField(null=True, verbose_name='修改者') + + +class Base(Model): + """ + 基本模型 + """ + + created_time = fields.DatetimeField(auto_now_add=True, verbose_name='创建时间') + updated_time = fields.DatetimeField(auto_now=True, verbose_name='更新时间') + + class Meta: + table = '' + abstract = True diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 0000000..1f06c32 --- /dev/null +++ b/backend/app/models/user.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from uuid import uuid4 + +from tortoise import Model, fields + + +class User(Model): + """ + 用户类 + """ + + id = fields.BigIntField(pk=True, index=True, description='主键id') + uuid = fields.CharField(max_length=36, default=uuid4, unique=True, description='用户UID') + username = fields.CharField(max_length=32, unique=True, description='用户名') + password = fields.CharField(max_length=255, description='密码') + email = fields.CharField(max_length=64, unique=True, description='邮箱') + status = fields.SmallIntField(default=1, description='是否激活') + is_superuser = fields.BooleanField(default=False, description='是否超级管理员') + avatar = fields.CharField(max_length=256, null=True, description='头像') + phone = fields.CharField(max_length=16, null=True, description='手机号') + joined_time = fields.DatetimeField(auto_now_add=True, description='注册时间') + last_login_time = fields.DatetimeField(null=True, description='上次登录时间') + + class Meta: + table = 'user' diff --git a/backend/app/pyproject.toml b/backend/app/pyproject.toml new file mode 100644 index 0000000..b7b1c2c --- /dev/null +++ b/backend/app/pyproject.toml @@ -0,0 +1,4 @@ +[tool.aerich] +tortoise_orm = "migration.TORTOISE_ORM" +location = "./migrations" +src_folder = "./." diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/app/schemas/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/schemas/base.py b/backend/app/schemas/base.py new file mode 100644 index 0000000..e69540a --- /dev/null +++ b/backend/app/schemas/base.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from asgiref.sync import sync_to_async +from fastapi.exceptions import ValidationException +from pydantic import BaseModel, ConfigDict, ValidationError, PydanticUserError +from pydantic_core import ErrorDetails + +# 自定义验证错误信息不包含验证预期内容,如果想添加预期内容,只需在自定义错误信息中添加 {xxx(预期内容字段)} 即可,预期内容字段参考以下链接 # noqa: E501 +# https://github.com/pydantic/pydantic-core/blob/a5cb7382643415b716b1a7a5392914e50f726528/tests/test_errors.py#L266 +# 替换预期内容字段方式,参考以下链接 +# https://github.com/pydantic/pydantic/blob/caa78016433ec9b16a973f92f187a7b6bfde6cb5/docs/errors/errors.md?plain=1#L232 +CUSTOM_VALIDATION_ERROR_MESSAGES = { + 'arguments_type': '参数类型输入错误', + 'assertion_error': '断言执行错误', + 'bool_parsing': '布尔值输入解析错误', + 'bool_type': '布尔值类型输入错误', + 'bytes_too_long': '字节长度输入过长', + 'bytes_too_short': '字节长度输入过短', + 'bytes_type': '字节类型输入错误', + 'callable_type': '可调用对象类型输入错误', + 'dataclass_exact_type': '数据类实例类型输入错误', + 'dataclass_type': '数据类类型输入错误', + 'date_from_datetime_inexact': '日期分量输入非零', + 'date_from_datetime_parsing': '日期输入解析错误', + 'date_future': '日期输入非将来时', + 'date_parsing': '日期输入验证错误', + 'date_past': '日期输入非过去时', + 'date_type': '日期类型输入错误', + 'datetime_future': '日期时间输入非将来时间', + 'datetime_object_invalid': '日期时间输入对象无效', + 'datetime_parsing': '日期时间输入解析错误', + 'datetime_past': '日期时间输入非过去时间', + 'datetime_type': '日期时间类型输入错误', + 'decimal_max_digits': '小数位数输入过多', + 'decimal_max_places': '小数位数输入错误', + 'decimal_parsing': '小数输入解析错误', + 'decimal_type': '小数类型输入错误', + 'decimal_whole_digits': '小数位数输入错误', + 'dict_type': '字典类型输入错误', + 'enum': '枚举成员输入错误,允许:{expected}', + 'extra_forbidden': '禁止额外字段输入', + 'finite_number': '有限值输入错误', + 'float_parsing': '浮点数输入解析错误', + 'float_type': '浮点数类型输入错误', + 'frozen_field': '冻结字段输入错误', + 'frozen_instance': '冻结实例禁止修改', + 'frozen_set_type': '冻结类型禁止输入', + 'get_attribute_error': '获取属性错误', + 'greater_than': '输入值过大', + 'greater_than_equal': '输入值过大或相等', + 'int_from_float': '整数类型输入错误', + 'int_parsing': '整数输入解析错误', + 'int_parsing_size': '整数输入解析长度错误', + 'int_type': '整数类型输入错误', + 'invalid_key': '输入无效键值', + 'is_instance_of': '类型实例输入错误', + 'is_subclass_of': '类型子类输入错误', + 'iterable_type': '可迭代类型输入错误', + 'iteration_error': '迭代值输入错误', + 'json_invalid': 'JSON 字符串输入错误', + 'json_type': 'JSON 类型输入错误', + 'less_than': '输入值过小', + 'less_than_equal': '输入值过小或相等', + 'list_type': '列表类型输入错误', + 'literal_error': '字面值输入错误', + 'mapping_type': '映射类型输入错误', + 'missing': '缺少必填字段', + 'missing_argument': '缺少参数', + 'missing_keyword_only_argument': '缺少关键字参数', + 'missing_positional_only_argument': '缺少位置参数', + 'model_attributes_type': '模型属性类型输入错误', + 'model_type': '模型实例输入错误', + 'multiple_argument_values': '参数值输入过多', + 'multiple_of': '输入值非倍数', + 'no_such_attribute': '分配无效属性值', + 'none_required': '输入值必须为 None', + 'recursion_loop': '输入循环赋值', + 'set_type': '集合类型输入错误', + 'string_pattern_mismatch': '字符串约束模式输入不匹配', + 'string_sub_type': '字符串子类型(非严格实例)输入错误', + 'string_too_long': '字符串输入过长', + 'string_too_short': '字符串输入过短', + 'string_type': '字符串类型输入错误', + 'string_unicode': '字符串输入非 Unicode', + 'time_delta_parsing': '时间差输入解析错误', + 'time_delta_type': '时间差类型输入错误', + 'time_parsing': '时间输入解析错误', + 'time_type': '时间类型输入错误', + 'timezone_aware': '缺少时区输入信息', + 'timezone_naive': '禁止时区输入信息', + 'too_long': '输入过长', + 'too_short': '输入过短', + 'tuple_type': '元组类型输入错误', + 'unexpected_keyword_argument': '输入意外关键字参数', + 'unexpected_positional_argument': '输入意外位置参数', + 'union_tag_invalid': '联合类型字面值输入错误', + 'union_tag_not_found': '联合类型参数输入未找到', + 'url_parsing': 'URL 输入解析错误', + 'url_scheme': 'URL 输入方案错误', + 'url_syntax_violation': 'URL 输入语法错误', + 'url_too_long': 'URL 输入过长', + 'url_type': 'URL 类型输入错误', + 'uuid_parsing': 'UUID 输入解析错误', + 'uuid_type': 'UUID 类型输入错误', + 'uuid_version': 'UUID 版本类型输入错误', + 'value_error': '值输入错误', +} + +CUSTOM_USAGE_ERROR_MESSAGES = { + 'class-not-fully-defined': '类属性类型未完全定义', + 'custom-json-schema': '__modify_schema__ 方法在V2中已被弃用', + 'decorator-missing-field': '定义了无效字段验证器', + 'discriminator-no-field': '鉴别器字段未全部定义', + 'discriminator-alias-type': '鉴别器字段使用非字符串类型定义', + 'discriminator-needs-literal': '鉴别器字段需要使用字面值定义', + 'discriminator-alias': '鉴别器字段别名定义不一致', + 'discriminator-validator': '鉴别器字段禁止定义字段验证器', + 'model-field-overridden': '无类型定义字段禁止重写', + 'model-field-missing-annotation': '缺少字段类型定义', + 'config-both': '重复定义配置项', + 'removed-kwargs': '调用已移除的关键字配置参数', + 'invalid-for-json-schema': '存在无效的 JSON 类型', + 'base-model-instantiated': '禁止实例化基础模型', + 'undefined-annotation': '缺少类型定义', + 'schema-for-unknown-type': '未知类型定义', + 'create-model-field-definitions': '字段定义错误', + 'create-model-config-base': '配置项定义错误', + 'validator-no-fields': '字段验证器未指定字段', + 'validator-invalid-fields': '字段验证器字段定义错误', + 'validator-instance-method': '字段验证器必须为类方法', + 'model-serializer-instance-method': '序列化器必须为实例方法', + 'validator-v1-signature': 'V1字段验证器错误已被弃用', + 'validator-signature': '字段验证器签名错误', + 'field-serializer-signature': '字段序列化器签名无法识别', + 'model-serializer-signature': '模型序列化器签名无法识别', + 'multiple-field-serializers': '字段序列化器重复定义', + 'invalid_annotated_type': '无效的类型定义', + 'type-adapter-config-unused': '类型适配器配置项定义错误', + 'root-model-extra': '根模型禁止定义额外字段', +} + + +@sync_to_async +def convert_validation_errors( + e: ValidationError | ValidationException, custom_messages: dict[str, str] +) -> list[ErrorDetails]: + new_errors: list[ErrorDetails] = [] + for error in e.errors(): + custom_message = custom_messages.get(error['type']) + if custom_message: + ctx = error.get('ctx') + error['msg'] = custom_message.format(**ctx) if ctx else custom_message + new_errors.append(error) + return new_errors + + +@sync_to_async +def convert_usage_errors(e: PydanticUserError, custom_messages: dict[str, str]) -> str: + custom_message = custom_messages.get(e.code) + if custom_message: + return custom_message + return e.message + + +class SchemaBase(BaseModel): + model_config = ConfigDict(use_enum_values=True) diff --git a/backend/app/schemas/token.py b/backend/app/schemas/token.py new file mode 100644 index 0000000..4c628e8 --- /dev/null +++ b/backend/app/schemas/token.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + + +from backend.app.schemas.base import SchemaBase +from backend.app.schemas.user import GetUserInfo + + +class Token(SchemaBase): + code: int = 200 + msg: str = 'Success' + access_token: str + token_type: str = 'Bearer' + user: GetUserInfo diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py new file mode 100644 index 0000000..747f418 --- /dev/null +++ b/backend/app/schemas/user.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import datetime + +from email_validator import validate_email, EmailNotValidError +from pydantic import UUID4, EmailStr, field_validator, ConfigDict + +from backend.app.schemas.base import SchemaBase + + +class Auth(SchemaBase): + username: str + password: str + + +class Auth2(Auth): + captcha_code: str + + +class CreateUser(SchemaBase): + username: str + password: str + email: str + + @field_validator('email') + @classmethod + def email_validate(cls, v: str): + try: + validate_email(v, check_deliverability=False).email + except EmailNotValidError: + raise ValueError() + return v + + +class UpdateUser(SchemaBase): + username: str + email: str + phone: str | None = None + + @field_validator('email') + @classmethod + def email_validate(cls, v: str): + try: + validate_email(v, check_deliverability=False).email + except EmailNotValidError: + raise ValueError('邮箱格式错误') + return v + + +class GetUserInfo(SchemaBase): + model_config = ConfigDict(from_attributes=True) + + id: int + uuid: UUID4 + username: str + email: EmailStr + status: int + is_superuser: bool + avatar: str | None = None + phone: str | None = None + joined_time: datetime.datetime + last_login_time: datetime.datetime | None = None + + +class ResetPassword(SchemaBase): + code: str + password1: str + password2: str diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/app/services/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/services/user_service.py b/backend/app/services/user_service.py new file mode 100644 index 0000000..3e6ad8a --- /dev/null +++ b/backend/app/services/user_service.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import os +from hashlib import sha256 + +from email_validator import validate_email, EmailNotValidError +from fast_captcha import text_captcha +from fastapi import Request, HTTPException, Response, UploadFile +from fastapi.security import OAuth2PasswordRequestForm + +from backend.app.common import jwt +from backend.app.common.exception import errors +from backend.app.common.jwt import superuser_verify +from backend.app.common.log import log +from backend.app.common.redis import redis_client +from backend.app.common.response.response_code import CustomCode +from backend.app.core.conf import settings +from backend.app.core.path_conf import AvatarPath +from backend.app.crud.crud_user import UserDao +from backend.app.models.user import User +from backend.app.schemas.user import CreateUser, ResetPassword, UpdateUser, Auth, Auth2 +from backend.app.utils import re_verify +from backend.app.utils.format_string import cut_path +from backend.app.utils.generate_string import get_current_timestamp +from backend.app.utils.send_email import send_verification_code_email + + +class UserService: + @staticmethod + async def user_verify(username: str, password: str): + user = await UserDao.get_user_by_username(username) + if not user: + raise errors.NotFoundError(msg='用户名不存在') + elif not await jwt.password_verify(password, user.password): + raise errors.AuthorizationError(msg='密码错误') + elif not user.status: + raise errors.AuthorizationError(msg='该用户已被锁定,无法登录') + return user + + @staticmethod + async def login_swagger(form_data: OAuth2PasswordRequestForm): + user = await UserService.user_verify(form_data.username, form_data.password) + await UserDao.update_user_login_time(user.pk) + access_token = await jwt.create_access_token(user.pk) + return access_token, user + + @staticmethod + async def login_json(obj: Auth): + user = await UserService.user_verify(obj.username, obj.password) + await UserDao.update_user_login_time(user.pk) + access_token = await jwt.create_access_token(user.pk) + return access_token, user + + @staticmethod + async def login_captcha(*, obj: Auth2, request: Request): + user = await UserService.user_verify(obj.username, obj.password) + try: + captcha_code = request.app.state.captcha_uid + redis_code = await redis_client.get(f'{captcha_code}') + if not redis_code: + raise errors.ForbiddenError(msg='验证码失效,请重新获取') + except AttributeError: + raise errors.ForbiddenError(msg='验证码失效,请重新获取') + if redis_code.lower() != obj.captcha_code.lower(): + raise errors.CustomError(error=CustomCode.CAPTCHA_ERROR) + await UserDao.update_user_login_time(user.pk) + access_token = await jwt.create_access_token(user.pk) + return access_token, user + + @staticmethod + async def register(obj: CreateUser): + username = await UserDao.get_user_by_username(name=obj.username) + if username: + raise errors.ForbiddenError(msg='该用户名已被注册') + email = await UserDao.check_email(email=obj.email) + if email: + raise errors.ForbiddenError(msg='该邮箱已被注册') + await UserDao.register_user(obj) + + @staticmethod + async def get_pwd_rest_captcha(*, username_or_email: str, response: Response): + code = text_captcha() + if await UserDao.get_user_by_username(username_or_email): + try: + response.delete_cookie(key='fastapi_reset_pwd_code') + response.delete_cookie(key='fastapi_reset_pwd_username') + response.set_cookie( + key='fastapi_reset_pwd_code', + value=sha256(code.encode('utf-8')).hexdigest(), + max_age=settings.COOKIES_MAX_AGE, + ) + response.set_cookie( + key='fastapi_reset_pwd_username', value=username_or_email, max_age=settings.COOKIES_MAX_AGE + ) + except Exception as e: + log.exception('无法发送验证码 {}', e) + raise e + current_user_email = await UserDao.get_email_by_username(username_or_email) + await send_verification_code_email(current_user_email, code) + else: + try: + validate_email(username_or_email, check_deliverability=False) + except EmailNotValidError: + raise HTTPException(status_code=404, detail='用户名不存在') + email_result = await UserDao.check_email(username_or_email) + if not email_result: + raise HTTPException(status_code=404, detail='邮箱不存在') + try: + response.delete_cookie(key='fastapi_reset_pwd_code') + response.delete_cookie(key='fastapi_reset_pwd_username') + response.set_cookie( + key='fastapi_reset_pwd_code', + value=sha256(code.encode('utf-8')).hexdigest(), + max_age=settings.COOKIES_MAX_AGE, + ) + username = await UserDao.get_username_by_email(username_or_email) + response.set_cookie(key='fastapi_reset_pwd_username', value=username, max_age=settings.COOKIES_MAX_AGE) + except Exception as e: + log.exception('无法发送验证码 {}', e) + raise e + await send_verification_code_email(username_or_email, code) + + @staticmethod + async def pwd_reset(*, obj: ResetPassword, request: Request, response: Response): + pwd1 = obj.password1 + pwd2 = obj.password2 + cookie_reset_pwd_code = request.cookies.get('fastapi_reset_pwd_code') + cookie_reset_pwd_username = request.cookies.get('fastapi_reset_pwd_username') + if pwd1 != pwd2: + raise errors.ForbiddenError(msg='两次密码输入不一致') + if cookie_reset_pwd_username is None or cookie_reset_pwd_code is None: + raise errors.NotFoundError(msg='验证码已失效,请重新获取') + if cookie_reset_pwd_code != sha256(obj.code.encode('utf-8')).hexdigest(): + raise errors.ForbiddenError(msg='验证码错误') + await UserDao.reset_password(cookie_reset_pwd_username, obj.password2) + response.delete_cookie(key='fastapi_reset_pwd_code') + response.delete_cookie(key='fastapi_reset_pwd_username') + + @staticmethod + async def get_user_info(username: str): + user = await UserDao.get_user_by_username(username) + if not user: + raise errors.NotFoundError(msg='用户不存在') + if user.avatar is not None: + user.avatar = cut_path(AvatarPath + user.avatar)[1] + return user + + @staticmethod + async def update(*, username: str, current_user: User, obj: UpdateUser): + await superuser_verify(current_user) + input_user = await UserDao.get_user_by_username(username) + if not input_user: + raise errors.NotFoundError(msg='用户不存在') + if input_user.username != obj.username: + username = await UserDao.get_user_by_username(obj.username) + if username: + raise errors.ForbiddenError(msg='该用户名已存在') + if input_user.email != obj.email: + _email = await UserDao.check_email(obj.email) + if _email: + raise errors.ForbiddenError(msg='该邮箱已注册') + if obj.phone is not None: + if not re_verify.is_phone(obj.phone): + raise errors.ForbiddenError(msg='手机号码输入有误') + count = await UserDao.update_userinfo(input_user, obj) + return count + + @staticmethod + async def update_avatar(*, username: str, current_user: User, avatar: UploadFile): + await superuser_verify(current_user) + input_user = await UserDao.get_user_by_username(username) + if not input_user: + raise errors.NotFoundError(msg='用户不存在') + input_user_avatar = input_user.avatar + if avatar is not None: + if input_user_avatar is not None: + try: + os.remove(AvatarPath + input_user_avatar) + except Exception as e: + log.error('用户 {} 更新头像时,原头像文件 {} 删除失败\n{}', username, input_user_avatar, e) + new_file = avatar.file.read() + if 'image' not in avatar.content_type: + raise errors.ForbiddenError(msg='图片格式错误,请重新选择图片') + file_name = str(get_current_timestamp()) + '_' + avatar.filename + if not os.path.exists(AvatarPath): + os.makedirs(AvatarPath) + with open(AvatarPath + f'{file_name}', 'wb') as f: + f.write(new_file) + else: + file_name = input_user_avatar + count = await UserDao.update_avatar(input_user, file_name) + return count + + @staticmethod + async def delete_avatar(*, username: str, current_user: User): + await superuser_verify(current_user) + input_user = await UserDao.get_user_by_username(username) + if not input_user: + raise errors.NotFoundError(msg='用户不存在') + input_user_avatar = input_user.avatar + if input_user_avatar is not None: + try: + os.remove(AvatarPath + input_user_avatar) + except Exception as e: + log.error('用户 {} 删除头像文件 {} 失败\n{}', input_user.username, input_user_avatar, e) + else: + raise errors.NotFoundError(msg='用户没有头像文件,请上传头像文件后再执行此操作') + count = await UserDao.delete_avatar(input_user.id) + return count + + @staticmethod + async def get_user_list(): + data = await UserDao.get_all() + return data.order_by('-id') + + @staticmethod + async def update_permission(pk: int): + user = await UserDao.get_user_by_id(pk) + if user: + await superuser_verify(user) + count = await UserDao.super_set(pk) + return count + else: + raise errors.NotFoundError(msg='用户不存在') + + @staticmethod + async def update_status(pk: int): + user = await UserDao.get_user_by_id(pk) + if user: + await superuser_verify(user) + count = await UserDao.status_set(pk) + return count + else: + raise errors.NotFoundError(msg='用户不存在') + + @staticmethod + async def delete(*, username: str, current_user: User): + await superuser_verify(current_user) + input_user = await UserDao.get_user_by_username(username) + if not input_user: + raise errors.NotFoundError(msg='用户不存在') + input_user_avatar = input_user.avatar + try: + if input_user_avatar is not None: + os.remove(AvatarPath + input_user_avatar) + except Exception as e: + log.error(f'删除用户 {input_user.username} 头像文件:{input_user_avatar} 失败\n{e}') + finally: + count = await UserDao.delete_user(input_user.id) + return count diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/app/utils/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/utils/format_string.py b/backend/app/utils/format_string.py new file mode 100644 index 0000000..0a34ace --- /dev/null +++ b/backend/app/utils/format_string.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from backend.app.core.path_conf import AvatarPath + + +def cut_path(path: str = AvatarPath, split_point: str = 'app') -> list: + """ + 切割路径 + + :param path: + :param split_point: + :return: + """ + after_path = path.split(split_point) + return after_path diff --git a/backend/app/utils/generate_string.py b/backend/app/utils/generate_string.py new file mode 100644 index 0000000..ef60556 --- /dev/null +++ b/backend/app/utils/generate_string.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import datetime +import uuid + + +def get_uuid_str() -> str: + """ + 生成uuid + + :return: str(uuid) + """ + return str(uuid.uuid4()) + + +def get_current_timestamp() -> float: + """ + 生成当前时间戳 + + :return: + """ + return datetime.datetime.now().timestamp() diff --git a/backend/app/utils/health_check.py b/backend/app/utils/health_check.py new file mode 100644 index 0000000..0d759cc --- /dev/null +++ b/backend/app/utils/health_check.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from math import ceil + +from fastapi import FastAPI, Request, Response +from fastapi.routing import APIRoute + +from backend.app.common.exception import errors + + +def ensure_unique_route_names(app: FastAPI) -> None: + """ + 检查路由名称是否唯一 + + :param app: + :return: + """ + temp_routes = set() + for route in app.routes: + if isinstance(route, APIRoute): + if route.name in temp_routes: + raise ValueError(f'Non-unique route name: {route.name}') + temp_routes.add(route.name) + + +async def http_limit_callback(request: Request, response: Response, expire: int): + """ + 请求限制时的默认回调函数 + + :param request: + :param response: + :param expire: 剩余毫秒 + :return: + """ + expires = ceil(expire / 1000) + raise errors.HTTPError(code=429, msg='请求过于频繁,请稍后重试', headers={'Retry-After': str(expires)}) diff --git a/backend/app/utils/re_verify.py b/backend/app/utils/re_verify.py new file mode 100644 index 0000000..b85f3f9 --- /dev/null +++ b/backend/app/utils/re_verify.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import re + + +def search_string(pattern, text) -> bool: + """ + 全字段正则匹配 + + :param pattern: + :param text: + :return: + """ + result = re.search(pattern, text) + if result: + return True + else: + return False + + +def match_string(pattern, text) -> bool: + """ + 从字段开头正则匹配 + + :param pattern: + :param text: + :return: + """ + result = re.match(pattern, text) + if result: + return True + else: + return False + + +def is_phone(text: str) -> bool: + """ + 检查手机号码 + + :param text: + :return: + """ + return match_string(r'^1[3-9]\d{9}$', text) diff --git a/backend/app/utils/send_email.py b/backend/app/utils/send_email.py new file mode 100644 index 0000000..c90a331 --- /dev/null +++ b/backend/app/utils/send_email.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText + +import aiosmtplib + +from backend.app.common.log import log +from backend.app.core.conf import settings +from backend.app.utils.generate_string import get_uuid_str + +__only_code = get_uuid_str() + +SEND_RESET_PASSWORD_TEXT = ( + f'您的重置密码验证码为:{__only_code}\n为了不影响您正常使用,请在{int(settings.COOKIES_MAX_AGE / 60)}分钟内完成密码重置' # noqa: E501 +) + + +async def send_verification_code_email(to: str, code: str, text: str = SEND_RESET_PASSWORD_TEXT): + """ + 发送验证码电子邮件 + + :param to: + :param code: + :param text: + :return: + """ + text = text.replace(__only_code, code) + msg = MIMEMultipart() + msg['Subject'] = settings.EMAIL_DESCRIPTION + msg['From'] = settings.EMAIL_USER + msg.attach(MIMEText(text, _charset='utf-8')) + + # 登录smtp服务器并发送邮件 + try: + smtp = aiosmtplib.SMTP(hostname=settings.EMAIL_SERVER, port=settings.EMAIL_PORT, use_tls=settings.EMAIL_SSL) + async with smtp: + await smtp.login(settings.EMAIL_USER, settings.EMAIL_PASSWORD) + await smtp.sendmail(msg['From'], to, msg.as_string()) + await smtp.quit() + except Exception as e: + log.error('邮件发送失败 {}', e) + raise Exception('邮件发送失败 {}'.format(e)) diff --git a/deploy/docker-compose/.env.server b/deploy/docker-compose/.env.server new file mode 100644 index 0000000..8656b21 --- /dev/null +++ b/deploy/docker-compose/.env.server @@ -0,0 +1,14 @@ +# Env: dev、pro +ENVIRONMENT='dev' +# MySQL +DB_HOST='ftm_mysql' +DB_PORT=3306 +DB_USER='root' +DB_PASSWORD='123456' +# Redis +REDIS_HOST='ftm_redis' +REDIS_PORT=6379 +REDIS_PASSWORD='' +REDIS_DATABASE=0 +# Token +TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk' diff --git a/deploy/docker-compose/docker-compose.yml b/deploy/docker-compose/docker-compose.yml new file mode 100644 index 0000000..5b12619 --- /dev/null +++ b/deploy/docker-compose/docker-compose.yml @@ -0,0 +1,80 @@ +version: "3.8" + +networks: + ftm_network: + driver: bridge + +volumes: + ftm_mysql: + ftm_redis: + ftm_static: + +services: + app: + build: + context: ../../ + dockerfile: Dockerfile + container_name: "ftm_server" + restart: always + depends_on: + - ftm_mysql + - ftm_redis + volumes: + - ftm_static:/ftm/backend/app/static + networks: + ftm_network: + aliases: + - ftm_backend_server + command: + - bash + - -c + - | + wait-for-it -s ftm_mysql:3306 -s ftm_redis:6379 -t 300 + supervisord -c /ftm/deploy/supervisor.conf + + mysql: + image: mysql:8.0.29 + ports: + - "3306:3306" + container_name: "ftm_mysql" + restart: always + environment: + MYSQL_DATABASE: ftm + MYSQL_ROOT_PASSWORD: 123456 + TZ: Asia/Shanghai + volumes: + - ftm_mysql:/var/lib/mysql + networks: + - ftm_network + command: + --default-authentication-plugin=mysql_native_password + --character-set-server=utf8mb4 + --collation-server=utf8mb4_general_ci + --lower_case_table_names=1 + + redis: + image: redis:6.2.7 + ports: + - "6379:6379" + container_name: "ftm_redis" + restart: always + environment: + - TZ=Asia/Shanghai + volumes: + - ftm_redis:/var/lib/redis + networks: + - ftm_network + + nginx: + image: nginx + ports: + - "8000:80" + container_name: "ftm_nginx" + restart: always + depends_on: + - app + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ftm_static:/www/ftm/backend/app/static + networks: + - ftm_network diff --git a/deploy/gunicorn.conf.py b/deploy/gunicorn.conf.py new file mode 100644 index 0000000..c8e6914 --- /dev/null +++ b/deploy/gunicorn.conf.py @@ -0,0 +1,46 @@ +# 监听内网端口 +bind = '0.0.0.0:8001' + +# 工作目录 +chdir = '/ftm/backend/app' + +# 并行工作进程数 +workers = 4 + +# 指定每个工作者的线程数 +threads = 4 + +# 监听队列 +backlog = 512 + +# 超时时间 +timeout = 120 + +# 设置守护进程,将进程交给 supervisor 管理;如果设置为 True 时,supervisor 启动日志为: +# gave up: fastapi_server entered FATAL state, too many start retries too quickly +# 则需要将此改为: False +daemon = False + +# 工作模式协程 +worker_class = 'uvicorn.workers.UvicornWorker' + +# 设置最大并发量 +worker_connections = 2000 + +# 设置进程文件目录 +pidfile = '/ftm/gunicorn.pid' + +# 设置访问日志和错误信息日志路径 +accesslog = '/var/log/fastapi_server/gunicorn_access.log' +errorlog = '/var/log/fastapi_server/gunicorn_error.log' + +# 设置这个值为true 才会把打印信息记录到错误日志里 +capture_output = True + +# 设置日志记录水平 +loglevel = 'debug' + +# python程序 +pythonpath = '/usr/local/lib/python3.8/site-packages' + +# 启动 gunicorn -c gunicorn.conf.py main:app diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 0000000..b8c70a3 --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,56 @@ +# For more information on configuration, see: +# * Official English Documentation: http://nginx.org/en/docs/ +# * Official Russian Documentation: http://nginx.org/ru/docs/ + +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log; +pid /run/nginx.pid; + +# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic. +include /usr/share/nginx/modules/*.conf; + +events { + worker_connections 1024; +} + +http { + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + client_max_body_size 5M; + client_body_buffer_size 5M; + + gzip on; + gzip_comp_level 2; + gzip_types text/plain text/css text/javascript application/javascript application/x-javascript application/xml application/x-httpd-php image/jpeg image/gif image/png; + gzip_vary on; + + keepalive_timeout 300; + + server { + listen 80 default_server; + listen [::]:80 default_server; + server_name 127.0.0.1; + + root /ftm; + + location / { + proxy_pass http://ftm_backend_server:8001; + + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 120s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + } + + location /static { + alias /www/ftm/backend/app/static; + } + } +} diff --git a/deploy/supervisor.conf b/deploy/supervisor.conf new file mode 100644 index 0000000..13ac0ed --- /dev/null +++ b/deploy/supervisor.conf @@ -0,0 +1,165 @@ +; Sample supervisor config file. +; +; For more information on the config file, please see: +; http://supervisord.org/configuration.html +; +; Notes: +; - Shell expansion ("~" or "$HOME") is not supported. Environment +; variables can be expanded using this syntax: "%(ENV_HOME)s". +; - Quotes around values are not supported, except in the case of +; the environment= options as shown below. +; - Comments must have a leading space: "a=b ;comment" not "a=b;comment". +; - Command will be truncated if it looks like a config file comment, e.g. +; "command=bash -c 'foo ; bar'" will truncate to "command=bash -c 'foo ". +; +; Warning: +; Paths throughout this example file use /tmp because it is available on most +; systems. You will likely need to change these to locations more appropriate +; for your system. Some systems periodically delete older files in /tmp. +; Notably, if the socket file defined in the [unix_http_server] section below +; is deleted, supervisorctl will be unable to connect to supervisord. + +[unix_http_server] +file=/tmp/supervisor.sock ; the path to the socket file +;chmod=0700 ; socket file mode (default 0700) +;chown=nobody:nogroup ; socket file uid:gid owner +;username=user ; default is no username (open server) +;password=123 ; default is no password (open server) + +; Security Warning: +; The inet HTTP server is not enabled by default. The inet HTTP server is +; enabled by uncommenting the [inet_http_server] section below. The inet +; HTTP server is intended for use within a trusted environment only. It +; should only be bound to localhost or only accessible from within an +; isolated, trusted network. The inet HTTP server does not support any +; form of encryption. The inet HTTP server does not use authentication +; by default (see the username= and password= options to add authentication). +; Never expose the inet HTTP server to the public internet. + +;[inet_http_server] ; inet (TCP) server disabled by default +;port=127.0.0.1:9001 ; ip_address:port specifier, *:port for all iface +;username=user ; default is no username (open server) +;password=123 ; default is no password (open server) + +[supervisord] +logfile=/tmp/supervisord.log ; main log file; default $CWD/supervisord.log +logfile_maxbytes=50MB ; max main logfile bytes b4 rotation; default 50MB +logfile_backups=10 ; # of main logfile backups; 0 means none, default 10 +loglevel=info ; log level; default info; others: debug,warn,trace +pidfile=/tmp/supervisord.pid ; supervisord pidfile; default supervisord.pid +nodaemon=true ; start in foreground if true; default false +silent=false ; no logs to stdout if true; default false +minfds=1024 ; min. avail startup file descriptors; default 1024 +minprocs=200 ; min. avail process descriptors;default 200 +;umask=022 ; process file creation umask; default 022 +user=root ; setuid to this UNIX account at startup; recommended if root +;identifier=supervisor ; supervisord identifier, default is 'supervisor' +;directory=/tmp ; default is not to cd during start +;nocleanup=true ; don't clean up tempfiles at start; default false +;childlogdir=/tmp ; 'AUTO' child log dir, default $TEMP +;environment=KEY="value" ; key value pairs to add to environment +;strip_ansi=false ; strip ansi escape codes in logs; def. false + +; The rpcinterface:supervisor section must remain in the config file for +; RPC (supervisorctl/web interface) to work. Additional interfaces may be +; added by defining them in separate [rpcinterface:x] sections. + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +; The supervisorctl section configures how supervisorctl will connect to +; supervisord. configure it match the settings in either the unix_http_server +; or inet_http_server section. + +[supervisorctl] +serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket +;serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket +;username=chris ; should be same as in [*_http_server] if set +;password=123 ; should be same as in [*_http_server] if set +;prompt=mysupervisor ; cmd line prompt (default "supervisor") +;history_file=~/.sc_history ; use readline history if available + +; The sample program section below shows all possible program subsection values. +; Create one or more 'real' program: sections to be able to control them under +; supervisor. + +;[program:theprogramname] +;command=/bin/cat ; the program (relative uses PATH, can take args) +;process_name=%(program_name)s ; process_name expr (default %(program_name)s) +;numprocs=1 ; number of processes copies to start (def 1) +;directory=/tmp ; directory to cwd to before exec (def no cwd) +;umask=022 ; umask for process (default None) +;priority=999 ; the relative start priority (default 999) +;autostart=true ; start at supervisord start (default: true) +;startsecs=1 ; # of secs prog must stay up to be running (def. 1) +;startretries=3 ; max # of serial start failures when starting (default 3) +;autorestart=unexpected ; when to restart if exited after running (def: unexpected) +;exitcodes=0 ; 'expected' exit codes used with autorestart (default 0) +;stopsignal=QUIT ; signal used to kill process (default TERM) +;stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10) +;stopasgroup=false ; send stop signal to the UNIX process group (default false) +;killasgroup=false ; SIGKILL the UNIX process group (def false) +;user=root ; setuid to this UNIX account to run the program +;redirect_stderr=true ; redirect proc stderr to stdout (default false) +;stdout_logfile=/a/path ; stdout log path, NONE for none; default AUTO +;stdout_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB) +;stdout_logfile_backups=10 ; # of stdout logfile backups (0 means none, default 10) +;stdout_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0) +;stdout_events_enabled=false ; emit events on stdout writes (default false) +;stdout_syslog=false ; send stdout to syslog with process name (default false) +;stderr_logfile=/a/path ; stderr log path, NONE for none; default AUTO +;stderr_logfile_maxbytes=1MB ; max # logfile bytes b4 rotation (default 50MB) +;stderr_logfile_backups=10 ; # of stderr logfile backups (0 means none, default 10) +;stderr_capture_maxbytes=1MB ; number of bytes in 'capturemode' (default 0) +;stderr_events_enabled=false ; emit events on stderr writes (default false) +;stderr_syslog=false ; send stderr to syslog with process name (default false) +;environment=A="1",B="2" ; process environment additions (def no adds) +;serverurl=AUTO ; override serverurl computation (childutils) + +; The sample eventlistener section below shows all possible eventlistener +; subsection values. Create one or more 'real' eventlistener: sections to be +; able to handle event notifications sent by supervisord. + +;[eventlistener:theeventlistenername] +;command=/bin/eventlistener ; the program (relative uses PATH, can take args) +;process_name=%(program_name)s ; process_name expr (default %(program_name)s) +;numprocs=1 ; number of processes copies to start (def 1) +;events=EVENT ; event notif. types to subscribe to (req'd) +;buffer_size=10 ; event buffer queue size (default 10) +;directory=/tmp ; directory to cwd to before exec (def no cwd) +;umask=022 ; umask for process (default None) +;priority=-1 ; the relative start priority (default -1) +;autostart=true ; start at supervisord start (default: true) +;startsecs=1 ; # of secs prog must stay up to be running (def. 1) +;startretries=3 ; max # of serial start failures when starting (default 3) +;autorestart=unexpected ; autorestart if exited after running (def: unexpected) +;exitcodes=0 ; 'expected' exit codes used with autorestart (default 0) +;stopsignal=QUIT ; signal used to kill process (default TERM) +;stopwaitsecs=10 ; max num secs to wait b4 SIGKILL (default 10) +;stopasgroup=false ; send stop signal to the UNIX process group (default false) +;killasgroup=false ; SIGKILL the UNIX process group (def false) +;user=chrism ; setuid to this UNIX account to run the program +;redirect_stderr=false ; redirect_stderr=true is not allowed for eventlisteners + +;[group:thegroupname] +;programs=progname1,progname2 ; each refers to 'x' in [program:x] definitions +;priority=999 ; the relative start priority (default 999) + +; The [include] section can just contain the "files" setting. This +; setting can list multiple files (separated by whitespace or +; newlines). It can also contain wildcards. The filenames are +; interpreted as relative to this file. Included files *cannot* +; include files themselves. + +;[include] +;files = relative/directory/*.ini + +[program:fastapi_server] +directory=/ftm +command=/usr/local/bin/gunicorn -c gunicorn.conf.py main:app +user=root +autostart=true +autorestart=true +startretries=5 +redirect_stderr=true +stdout_logfile=/var/log/fastapi_server/fastapi_server.log diff --git a/pre-commit.sh b/pre-commit.sh new file mode 100644 index 0000000..c099c5f --- /dev/null +++ b/pre-commit.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +pre-commit run --all-files --verbose --show-diff-on-failure diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..936ae4b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,27 @@ +aerich==0.7.2 +aiofiles==23.2.1 +aiosmtplib==2.0.2 +asgiref==3.7.2 +bcrypt==4.0.1 +cryptography==41.0.3 +email-validator==2.0.0 +fast_captcha==0.2.1 +fastapi==0.101.1 +fastapi-limiter==0.1.5 +fastapi-pagination==0.12.8 +gunicorn==21.2.0 +loguru==0.7.0 +passlib==1.7.4 +path==16.7.1 +pre-commit==3.2.2 +pydantic==2.2.1 +pydantic-settings==2.0.3 +python-jose==3.3.0 +python-multipart==0.0.6 +redis==4.6.0 +ruff==0.0.285 +starlette==0.27.0 +supervisor==4.2.5 +tortoise-orm[asyncmy]==0.20.0 +uvicorn[standard]==0.23.2 +wait-for-it==2.2.2 From f1fa1e6aebb4e5c391cfd531afcb9077dba55213 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 20 Nov 2023 20:19:04 +0800 Subject: [PATCH 2/9] =?UTF-8?q?=E6=9B=B4=E6=96=B0UUID=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E4=B8=B2=E7=94=9F=E6=88=90=E5=B7=A5=E5=85=B7=20(#9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/v1/auth/captcha.py | 4 ++-- backend/app/models/user.py | 6 +++--- backend/app/utils/generate_string.py | 6 +++--- backend/app/utils/send_email.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/backend/app/api/v1/auth/captcha.py b/backend/app/api/v1/auth/captcha.py index 073190e..a272a83 100644 --- a/backend/app/api/v1/auth/captcha.py +++ b/backend/app/api/v1/auth/captcha.py @@ -8,7 +8,7 @@ from backend.app.common.redis import redis_client from backend.app.core.conf import settings -from backend.app.utils.generate_string import get_uuid_str +from backend.app.utils.generate_string import get_uuid4_str router = APIRouter() @@ -16,7 +16,7 @@ @router.get('/captcha', summary='获取验证码', dependencies=[Depends(RateLimiter(times=5, seconds=10))]) async def get_captcha(request: Request): img, code = await run_in_threadpool(img_captcha) - uuid = get_uuid_str() + uuid = get_uuid4_str() request.app.state.captcha_uuid = uuid await redis_client.set(uuid, code, settings.CAPTCHA_EXPIRATION_TIME) return StreamingResponse(content=img, media_type='image/jpeg') diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 1f06c32..71dce6b 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from uuid import uuid4 - from tortoise import Model, fields +from backend.app.utils.generate_string import get_uuid4_str + class User(Model): """ @@ -11,7 +11,7 @@ class User(Model): """ id = fields.BigIntField(pk=True, index=True, description='主键id') - uuid = fields.CharField(max_length=36, default=uuid4, unique=True, description='用户UID') + uuid = fields.CharField(max_length=36, default=get_uuid4_str, unique=True, description='用户UID') username = fields.CharField(max_length=32, unique=True, description='用户名') password = fields.CharField(max_length=255, description='密码') email = fields.CharField(max_length=64, unique=True, description='邮箱') diff --git a/backend/app/utils/generate_string.py b/backend/app/utils/generate_string.py index ef60556..58dee98 100644 --- a/backend/app/utils/generate_string.py +++ b/backend/app/utils/generate_string.py @@ -4,9 +4,9 @@ import uuid -def get_uuid_str() -> str: +def get_uuid4_str() -> str: """ - 生成uuid + 获取 uuid4 字符串 :return: str(uuid) """ @@ -15,7 +15,7 @@ def get_uuid_str() -> str: def get_current_timestamp() -> float: """ - 生成当前时间戳 + 获取当前时间戳 :return: """ diff --git a/backend/app/utils/send_email.py b/backend/app/utils/send_email.py index c90a331..a9afd8a 100644 --- a/backend/app/utils/send_email.py +++ b/backend/app/utils/send_email.py @@ -7,9 +7,9 @@ from backend.app.common.log import log from backend.app.core.conf import settings -from backend.app.utils.generate_string import get_uuid_str +from backend.app.utils.generate_string import get_uuid4_str -__only_code = get_uuid_str() +__only_code = get_uuid4_str() SEND_RESET_PASSWORD_TEXT = ( f'您的重置密码验证码为:{__only_code}\n为了不影响您正常使用,请在{int(settings.COOKIES_MAX_AGE / 60)}分钟内完成密码重置' # noqa: E501 From 362fb549af8e9a9d7d8071cfc9940cc0d21ec23f Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 20 Nov 2023 20:20:18 +0800 Subject: [PATCH 3/9] =?UTF-8?q?=E9=87=87=E7=94=A8ruff=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=20(#10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 14 +++----- .ruff.toml | 36 ++++++------------- backend/app/api/v1/auth/captcha.py | 2 +- .../app/common/exception/exception_handler.py | 4 +-- backend/app/common/jwt.py | 2 +- backend/app/common/log.py | 1 + backend/app/common/pagination.py | 5 +-- backend/app/common/redis.py | 2 +- .../app/common/response/response_schema.py | 5 ++- backend/app/core/registrar.py | 3 +- backend/app/crud/base.py | 2 +- backend/app/main.py | 1 + backend/app/schemas/base.py | 2 +- backend/app/schemas/user.py | 4 +-- backend/app/services/user_service.py | 7 ++-- backend/app/utils/send_email.py | 4 +-- 16 files changed, 38 insertions(+), 56 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ec9fbfb..435b0dc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,20 +6,14 @@ repos: - id: end-of-file-fixer - id: requirements-txt-fixer - id: check-yaml + - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.0.285 + rev: v0.1.6 hooks: - id: ruff args: - '--config' - '.ruff.toml' - '--fix' - - repo: https://github.com/psf/black - rev: 23.3.0 - hooks: - - id: black - language_version: python3.10 - args: - - '--skip-string-normalization' - - '--line-length' - - '120' + - '--unsafe-fixes' + - id: ruff-format diff --git a/.ruff.toml b/.ruff.toml index bd45725..5937bc3 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,45 +1,31 @@ +line-length = 120 +target-version = "py310" +cache-dir = "./.ruff_cache" + +[lint] select = [ "E", "F", + "I", "W505", "PT018", - "Q000", "SIM101", "SIM114", "PGH004", "PLE1142", "RUF100", - "I002", "F404", "TCH", "UP007" ] -line-length = 120 -format = "grouped" -target-version = "py310" -cache-dir = "./.ruff_cache" - -[flake8-pytest-style] -mark-parentheses = false -parametrize-names-type = "list" -parametrize-values-row-type = "list" -parametrize-values-type = "tuple" - -[flake8-quotes] -avoid-escape = false -docstring-quotes = "double" -inline-quotes = "single" -multiline-quotes = "single" -[flake8-unused-arguments] -ignore-variadic-names = true - -[isort] +[lint.isort] lines-between-types = 1 -order-by-type = true -[per-file-ignores] +[lint.per-file-ignores] "backend/app/api/v1/*.py" = ["TCH"] "backend/app/models/*.py" = ["TCH003"] "backend/app/**/__init__.py" = ["F401"] -"backend/app/tests/*.py" = ["E402"] + +[format] +quote-style = "single" diff --git a/backend/app/api/v1/auth/captcha.py b/backend/app/api/v1/auth/captcha.py index a272a83..c0f6aba 100644 --- a/backend/app/api/v1/auth/captcha.py +++ b/backend/app/api/v1/auth/captcha.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- from fast_captcha import img_captcha -from fastapi import APIRouter, Request, Depends +from fastapi import APIRouter, Depends, Request from fastapi_limiter.depends import RateLimiter from starlette.concurrency import run_in_threadpool from starlette.responses import StreamingResponse diff --git a/backend/app/common/exception/exception_handler.py b/backend/app/common/exception/exception_handler.py index e89606a..010dc71 100644 --- a/backend/app/common/exception/exception_handler.py +++ b/backend/app/common/exception/exception_handler.py @@ -16,10 +16,10 @@ from backend.app.common.response.response_schema import response_base from backend.app.core.conf import settings from backend.app.schemas.base import ( - convert_validation_errors, + CUSTOM_USAGE_ERROR_MESSAGES, CUSTOM_VALIDATION_ERROR_MESSAGES, convert_usage_errors, - CUSTOM_USAGE_ERROR_MESSAGES, + convert_validation_errors, ) diff --git a/backend/app/common/jwt.py b/backend/app/common/jwt.py index ec51e4a..d10a886 100644 --- a/backend/app/common/jwt.py +++ b/backend/app/common/jwt.py @@ -11,7 +11,7 @@ from pydantic import ValidationError from typing_extensions import Annotated -from backend.app.common.exception.errors import TokenError, AuthorizationError +from backend.app.common.exception.errors import AuthorizationError, TokenError from backend.app.core.conf import settings from backend.app.crud.crud_user import UserDao from backend.app.models.user import User diff --git a/backend/app/common/log.py b/backend/app/common/log.py index a3968ea..10c3d6d 100644 --- a/backend/app/common/log.py +++ b/backend/app/common/log.py @@ -3,6 +3,7 @@ from __future__ import annotations import os + from typing import TYPE_CHECKING from loguru import logger diff --git a/backend/app/common/pagination.py b/backend/app/common/pagination.py index c91d6d4..2f51d53 100644 --- a/backend/app/common/pagination.py +++ b/backend/app/common/pagination.py @@ -3,9 +3,10 @@ from __future__ import annotations import math -from typing import TypeVar, Generic, Sequence, TYPE_CHECKING -from fastapi import Query, Depends +from typing import TYPE_CHECKING, Generic, Sequence, TypeVar + +from fastapi import Depends, Query from fastapi_pagination import pagination_ctx from fastapi_pagination.bases import AbstractPage, AbstractParams, RawParams from fastapi_pagination.ext.tortoise import paginate diff --git a/backend/app/common/redis.py b/backend/app/common/redis.py index ad744c3..785024c 100644 --- a/backend/app/common/redis.py +++ b/backend/app/common/redis.py @@ -4,7 +4,7 @@ import sys from redis.asyncio.client import Redis -from redis.exceptions import TimeoutError, AuthenticationError +from redis.exceptions import AuthenticationError, TimeoutError from backend.app.common.log import log from backend.app.core.conf import settings diff --git a/backend/app/common/response/response_schema.py b/backend/app/common/response/response_schema.py index 9defb1d..8a0e4d0 100644 --- a/backend/app/common/response/response_schema.py +++ b/backend/app/common/response/response_schema.py @@ -6,7 +6,6 @@ from fastapi.encoders import jsonable_encoder from pydantic import BaseModel, validate_call - _ExcludeData = set[int | str] | dict[int | str, Any] __all__ = ['ResponseModel', 'response_base'] @@ -69,7 +68,7 @@ async def success( msg: str = 'Success', data: Any | None = None, exclude: _ExcludeData | None = None, - **kwargs + **kwargs, ) -> dict: """ 请求成功返回通用方法 @@ -91,7 +90,7 @@ async def fail( msg: str = 'Bad Request', data: Any = None, exclude: _ExcludeData | None = None, - **kwargs + **kwargs, ) -> dict: data = data if data is None else await self.__json_encoder(data, exclude, **kwargs) return {'code': code, 'msg': msg, 'data': data} diff --git a/backend/app/core/registrar.py b/backend/app/core/registrar.py index 905d24e..0daa122 100644 --- a/backend/app/core/registrar.py +++ b/backend/app/core/registrar.py @@ -14,7 +14,7 @@ from backend.app.core.conf import settings from backend.app.database.db_mysql import mysql_config from backend.app.middleware.access_middle import AccessMiddleware -from backend.app.utils.health_check import http_limit_callback, ensure_unique_route_names +from backend.app.utils.health_check import ensure_unique_route_names, http_limit_callback def register_app(): @@ -61,6 +61,7 @@ def register_static_file(app: FastAPI): """ if settings.STATIC_FILE: import os + from fastapi.staticfiles import StaticFiles if not os.path.exists('./static'): diff --git a/backend/app/crud/base.py b/backend/app/crud/base.py index f7a1a4f..26ec71f 100644 --- a/backend/app/crud/base.py +++ b/backend/app/crud/base.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from typing import TypeVar, Generic, Type, Any, Dict +from typing import Any, Dict, Generic, Type, TypeVar from asgiref.sync import sync_to_async from pydantic import BaseModel diff --git a/backend/app/main.py b/backend/app/main.py index bb83179..2b6a5db 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import uvicorn + from path import Path from backend.app.common.log import log diff --git a/backend/app/schemas/base.py b/backend/app/schemas/base.py index e69540a..f2599a6 100644 --- a/backend/app/schemas/base.py +++ b/backend/app/schemas/base.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- from asgiref.sync import sync_to_async from fastapi.exceptions import ValidationException -from pydantic import BaseModel, ConfigDict, ValidationError, PydanticUserError +from pydantic import BaseModel, ConfigDict, PydanticUserError, ValidationError from pydantic_core import ErrorDetails # 自定义验证错误信息不包含验证预期内容,如果想添加预期内容,只需在自定义错误信息中添加 {xxx(预期内容字段)} 即可,预期内容字段参考以下链接 # noqa: E501 diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 747f418..80ce3c3 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- import datetime -from email_validator import validate_email, EmailNotValidError -from pydantic import UUID4, EmailStr, field_validator, ConfigDict +from email_validator import EmailNotValidError, validate_email +from pydantic import UUID4, ConfigDict, EmailStr, field_validator from backend.app.schemas.base import SchemaBase diff --git a/backend/app/services/user_service.py b/backend/app/services/user_service.py index 3e6ad8a..671b99b 100644 --- a/backend/app/services/user_service.py +++ b/backend/app/services/user_service.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- import os + from hashlib import sha256 -from email_validator import validate_email, EmailNotValidError +from email_validator import EmailNotValidError, validate_email from fast_captcha import text_captcha -from fastapi import Request, HTTPException, Response, UploadFile +from fastapi import HTTPException, Request, Response, UploadFile from fastapi.security import OAuth2PasswordRequestForm from backend.app.common import jwt @@ -18,7 +19,7 @@ from backend.app.core.path_conf import AvatarPath from backend.app.crud.crud_user import UserDao from backend.app.models.user import User -from backend.app.schemas.user import CreateUser, ResetPassword, UpdateUser, Auth, Auth2 +from backend.app.schemas.user import Auth, Auth2, CreateUser, ResetPassword, UpdateUser from backend.app.utils import re_verify from backend.app.utils.format_string import cut_path from backend.app.utils.generate_string import get_current_timestamp diff --git a/backend/app/utils/send_email.py b/backend/app/utils/send_email.py index a9afd8a..022ffa0 100644 --- a/backend/app/utils/send_email.py +++ b/backend/app/utils/send_email.py @@ -11,9 +11,7 @@ __only_code = get_uuid4_str() -SEND_RESET_PASSWORD_TEXT = ( - f'您的重置密码验证码为:{__only_code}\n为了不影响您正常使用,请在{int(settings.COOKIES_MAX_AGE / 60)}分钟内完成密码重置' # noqa: E501 -) +SEND_RESET_PASSWORD_TEXT = f'您的重置密码验证码为:{__only_code}\n为了不影响您正常使用,请在{int(settings.COOKIES_MAX_AGE / 60)}分钟内完成密码重置' # noqa: E501 async def send_verification_code_email(to: str, code: str, text: str = SEND_RESET_PASSWORD_TEXT): From 9e23e5800e9bbdfde0972c835ed5d48723953831 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:27:24 +0800 Subject: [PATCH 4/9] Bump cryptography from 41.0.3 to 41.0.6 (#11) Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.3 to 41.0.6. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/41.0.3...41.0.6) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 936ae4b..bc4d27c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ aiofiles==23.2.1 aiosmtplib==2.0.2 asgiref==3.7.2 bcrypt==4.0.1 -cryptography==41.0.3 +cryptography==41.0.6 email-validator==2.0.0 fast_captcha==0.2.1 fastapi==0.101.1 From 55cd748cfa0fbdbbbb1fc047382ddc822ff63542 Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sat, 16 Dec 2023 16:42:11 +0800 Subject: [PATCH 5/9] =?UTF-8?q?=E6=B7=BB=E5=8A=A0Telegram=E4=BA=92?= =?UTF-8?q?=E5=8A=A8=E9=93=BE=E6=8E=A5=20(#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f05becf..82e9bfd 100644 --- a/README.md +++ b/README.md @@ -82,18 +82,33 @@ 3. 等待命令自动完成 4. 浏览器访问:http://127.0.0.1:8000/api/v1/docs +## 互动 + +有且仅有当前一个频道,请注意辨别真伪 + + + + + + + + +
直链跳转
Telegram
+ ## 赞助 -> 如果此项目能够帮助到你,你可以赞助作者一些咖啡豆表示鼓励 :coffee: +如果此项目能够帮助到你,你可以赞助作者一些咖啡豆表示鼓励 :coffee: - +
- + Wechat + Alipay + 0x40D5e2304b452256afD9CE2d3d5531dc8d293138
微信 支付宝ERC20
From 96fb1ad4f79bdd8981c8682972533f66ed140d7e Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 8 Jan 2024 00:20:56 +0800 Subject: [PATCH 6/9] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86=E5=99=A8=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 +-- backend/app/api/v1/user.py | 4 +- .../app/common/exception/exception_handler.py | 86 ++++++++++--------- backend/app/common/pagination.py | 2 +- backend/app/schemas/base.py | 27 +----- requirements.txt | 10 +-- 6 files changed, 57 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 82e9bfd..067ae7a 100644 --- a/README.md +++ b/README.md @@ -86,14 +86,9 @@ 有且仅有当前一个频道,请注意辨别真伪 - - - - - - - -
直链跳转
Telegram
+| [直链跳转](https://t.me/+ZlPhIFkPp7E4NGI1) | +|----------------------------------------| +| Telegram(科学上网) | ## 赞助 diff --git a/backend/app/api/v1/user.py b/backend/app/api/v1/user.py index b430dc2..8f13a47 100644 --- a/backend/app/api/v1/user.py +++ b/backend/app/api/v1/user.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Request, Response, UploadFile from backend.app.common.jwt import CurrentUser, DependsJwtUser -from backend.app.common.pagination import PageDepends, paging_data +from backend.app.common.pagination import DependsPagination, paging_data from backend.app.common.response.response_schema import response_base from backend.app.schemas.user import CreateUser, GetUserInfo, ResetPassword, UpdateUser from backend.app.services.user_service import UserService @@ -64,7 +64,7 @@ async def delete_avatar(username: str, current_user: CurrentUser): return await response_base.fail() -@router.get('', summary='获取所有用户', dependencies=[DependsJwtUser, PageDepends]) +@router.get('', summary='获取所有用户', dependencies=[DependsJwtUser, DependsPagination]) async def get_all_users(): data = await UserService.get_user_list() page_data = await paging_data(data, GetUserInfo) diff --git a/backend/app/common/exception/exception_handler.py b/backend/app/common/exception/exception_handler.py index 010dc71..981d83b 100644 --- a/backend/app/common/exception/exception_handler.py +++ b/backend/app/common/exception/exception_handler.py @@ -18,8 +18,6 @@ from backend.app.schemas.base import ( CUSTOM_USAGE_ERROR_MESSAGES, CUSTOM_VALIDATION_ERROR_MESSAGES, - convert_usage_errors, - convert_validation_errors, ) @@ -52,25 +50,24 @@ async def _validation_exception_handler(e: RequestValidationError | ValidationEr :param e: :return: """ - - message = '' - errors = await convert_validation_errors(e, CUSTOM_VALIDATION_ERROR_MESSAGES) - for error in errors[:1]: - if error.get('type') == 'json_invalid': - message += 'json解析失败' - else: - error_input = error.get('input') + errors = [] + for error in e.errors(): + custom_message = CUSTOM_VALIDATION_ERROR_MESSAGES.get(error['type']) + if custom_message: ctx = error.get('ctx') - ctx_error = ctx.get('error') if ctx else None - field = str(error.get('loc')[-1]) - error_msg = error.get('msg') - message += f'{field} {ctx_error if ctx else error_msg}: {error_input}' + '.' - content = { - 'code': 422, - 'msg': '请求参数非法' if len(message) == 0 else f'请求参数非法: {message}', - 'data': {'errors': e.errors()} if message == '' and settings.UVICORN_RELOAD is True else None, - } - return JSONResponse(status_code=422, content=await response_base.fail(**content)) + error['msg'] = custom_message.format(**ctx) if ctx else custom_message + errors.append(error) + error = errors[0] + if error.get('type') == 'json_invalid': + message = 'json解析失败' + else: + error_input = error.get('input') + field = str(error.get('loc')[-1]) + error_msg = error.get('msg') + message = f'{field} {error_msg},输入:{error_input}' + msg = f'请求参数非法: {message}' + data = {'errors': errors} if settings.ENVIRONMENT == 'dev' else None + return JSONResponse(status_code=422, content=await response_base.fail(code=422, msg=msg, data=data)) def register_exception(app: FastAPI): @@ -84,7 +81,6 @@ async def http_exception_handler(request: Request, exc: HTTPException): :return: """ content = {'code': exc.status_code, 'msg': exc.detail} - request.state.__request_http_exception__ = content # 用于在中间件中获取异常信息 return JSONResponse( status_code=await _get_exception_code(exc.status_code), content=await response_base.fail(**content), @@ -118,6 +114,20 @@ async def pydantic_user_error_handler(request: Request, exc: PydanticUserError): """ Pydantic 用户异常处理 + :param request: + :param exc: + :return: + """ + return JSONResponse( + status_code=500, + content=await response_base.fail(code=exc.code, msg=CUSTOM_USAGE_ERROR_MESSAGES.get(exc.code)), + ) + + @app.exception_handler(AssertionError) + async def assertion_error_handler(request: Request, exc: AssertionError): + """ + 断言错误处理 + :param request: :param exc: :return: @@ -125,8 +135,11 @@ async def pydantic_user_error_handler(request: Request, exc: PydanticUserError): return JSONResponse( status_code=500, content=await response_base.fail( - code=exc.code, msg=await convert_usage_errors(exc, CUSTOM_USAGE_ERROR_MESSAGES) - ), + code=500, + msg=str(''.join(exc.args) if exc.args else exc.__doc__), + ) + if settings.ENVIRONMENT == 'dev' + else await response_base.fail(code=500, msg='Internal Server Error'), ) @app.exception_handler(Exception) @@ -141,23 +154,12 @@ async def all_exception_handler(request: Request, exc: Exception): if isinstance(exc, BaseExceptionMixin): return JSONResponse( status_code=await _get_exception_code(exc.code), - content=await response_base.fail(code=exc.code, msg=str(exc.msg), data=exc.data if exc.data else None), - background=exc.background, - ) - - elif isinstance(exc, AssertionError): - return JSONResponse( - status_code=500, content=await response_base.fail( - code=500, - msg=','.join(exc.args) - if exc.args - else exc.__repr__() - if not exc.__repr__().startswith('AssertionError()') - else exc.__doc__, - ) - if settings.ENVIRONMENT == 'dev' - else await response_base.fail(code=500, msg='Internal Server Error'), + code=exc.code, + msg=str(exc.msg), + data=exc.data if exc.data else None, + ), + background=exc.background, ) else: @@ -197,7 +199,11 @@ async def cors_status_code_500_exception_handler(request, exc): origin = request.headers.get('origin') if origin: cors = CORSMiddleware( - app=app, allow_origins=['*'], allow_credentials=True, allow_methods=['*'], allow_headers=['*'] + app=app, + allow_origins=['*'], + allow_credentials=True, + allow_methods=['*'], + allow_headers=['*'], ) response.headers.update(cors.simple_headers) has_cookie = 'cookie' in request.headers diff --git a/backend/app/common/pagination.py b/backend/app/common/pagination.py index 2f51d53..63279f3 100644 --- a/backend/app/common/pagination.py +++ b/backend/app/common/pagination.py @@ -82,4 +82,4 @@ async def paging_data(query_set: QuerySet, page_data_schema: SchemaT) -> dict: # 分页依赖注入 -PageDepends = Depends(pagination_ctx(_Page)) +DependsPagination = Depends(pagination_ctx(_Page)) diff --git a/backend/app/schemas/base.py b/backend/app/schemas/base.py index f2599a6..eef113c 100644 --- a/backend/app/schemas/base.py +++ b/backend/app/schemas/base.py @@ -1,9 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from asgiref.sync import sync_to_async -from fastapi.exceptions import ValidationException -from pydantic import BaseModel, ConfigDict, PydanticUserError, ValidationError -from pydantic_core import ErrorDetails +from pydantic import BaseModel, ConfigDict # 自定义验证错误信息不包含验证预期内容,如果想添加预期内容,只需在自定义错误信息中添加 {xxx(预期内容字段)} 即可,预期内容字段参考以下链接 # noqa: E501 # https://github.com/pydantic/pydantic-core/blob/a5cb7382643415b716b1a7a5392914e50f726528/tests/test_errors.py#L266 @@ -140,27 +137,5 @@ } -@sync_to_async -def convert_validation_errors( - e: ValidationError | ValidationException, custom_messages: dict[str, str] -) -> list[ErrorDetails]: - new_errors: list[ErrorDetails] = [] - for error in e.errors(): - custom_message = custom_messages.get(error['type']) - if custom_message: - ctx = error.get('ctx') - error['msg'] = custom_message.format(**ctx) if ctx else custom_message - new_errors.append(error) - return new_errors - - -@sync_to_async -def convert_usage_errors(e: PydanticUserError, custom_messages: dict[str, str]) -> str: - custom_message = custom_messages.get(e.code) - if custom_message: - return custom_message - return e.message - - class SchemaBase(BaseModel): model_config = ConfigDict(use_enum_values=True) diff --git a/requirements.txt b/requirements.txt index bc4d27c..6ba08e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,8 +6,8 @@ bcrypt==4.0.1 cryptography==41.0.6 email-validator==2.0.0 fast_captcha==0.2.1 -fastapi==0.101.1 -fastapi-limiter==0.1.5 +fastapi==0.108.0 +fastapi-limiter==0.1.6 fastapi-pagination==0.12.8 gunicorn==21.2.0 loguru==0.7.0 @@ -18,10 +18,8 @@ pydantic==2.2.1 pydantic-settings==2.0.3 python-jose==3.3.0 python-multipart==0.0.6 -redis==4.6.0 -ruff==0.0.285 -starlette==0.27.0 +redis[hiredis]==5.0.1 supervisor==4.2.5 -tortoise-orm[asyncmy]==0.20.0 +tortoise-orm[asyncmy]==0.24.0 uvicorn[standard]==0.23.2 wait-for-it==2.2.2 From 016be572207902cf4f71e6833d1c6618dd9b7f0a Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Thu, 1 Feb 2024 12:03:13 +0800 Subject: [PATCH 7/9] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E8=B5=9E=E5=8A=A9?= =?UTF-8?q?=E5=95=86=E5=92=8CFUNDING=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/FUNDING.yml | 1 + README.md | 24 ++++-------------------- 2 files changed, 5 insertions(+), 20 deletions(-) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..49133f4 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: https://wu-clan.github.io/sponsor/ diff --git a/README.md b/README.md index 067ae7a..2c2014a 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ ## 使用 -> ⚠️: 此过程请格外注意端口占用情况, 特别是 8000, 3306, 6379... +> [!WARNING] +> 此过程请格外注意端口占用情况, 特别是 8000, 3306, 6379... ### 1: 传统 @@ -84,28 +85,11 @@ ## 互动 -有且仅有当前一个频道,请注意辨别真伪 - -| [直链跳转](https://t.me/+ZlPhIFkPp7E4NGI1) | -|----------------------------------------| -| Telegram(科学上网) | +[WeChat / QQ](https://github.com/wu-clan) ## 赞助 -如果此项目能够帮助到你,你可以赞助作者一些咖啡豆表示鼓励 :coffee: - - - - - - - - - -
Wechat - Alipay - 0x40D5e2304b452256afD9CE2d3d5531dc8d293138 -
微信支付宝ERC20
+如果此项目能够帮助到你,你可以赞助作者一些咖啡豆表示鼓励:[:coffee: Sponsor :coffee:](https://wu-clan.github.io/sponsor/) ## 许可证 From 6e7c7c09707387ed663811d9d94c0f8b419ba27d Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Sat, 25 May 2024 17:26:59 +0800 Subject: [PATCH 8/9] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E5=8C=85=20(#22)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 升级依赖包 * 更新依赖 * fix requirements.txt * 清除多余文件 --- .gitignore | 3 +- .pre-commit-config.yaml | 15 +- backend/app/core/registrar.py | 43 +- backend/app/migration.py | 4 +- pdm.lock | 1692 +++++++++++++++++++++++++++++++++ pyproject.toml | 48 + requirements.txt | 101 +- 7 files changed, 1860 insertions(+), 46 deletions(-) create mode 100644 pdm.lock create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore index 057354b..30ac5fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ __pycache__/ .idea/ .env +.venv/ venv/ -.mypy_cache/ backend/app/log/ backend/app/migrations/ .ruff_cache/ +.pdm-python diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 435b0dc..e01fe72 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,11 +4,11 @@ repos: hooks: - id: check-added-large-files - id: end-of-file-fixer - - id: requirements-txt-fixer - id: check-yaml + - id: check-toml - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.1.6 + rev: v0.4.5 hooks: - id: ruff args: @@ -17,3 +17,14 @@ repos: - '--fix' - '--unsafe-fixes' - id: ruff-format + + - repo: https://github.com/pdm-project/pdm + rev: 2.12.4 + hooks: + - id: pdm-export + args: + - '-o' + - 'requirements.txt' + - '--without-hashes' + files: ^pdm.lock$ + - id: pdm-lock-check diff --git a/backend/app/core/registrar.py b/backend/app/core/registrar.py index 0daa122..04f84a4 100644 --- a/backend/app/core/registrar.py +++ b/backend/app/core/registrar.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi_limiter import FastAPILimiter @@ -17,6 +18,26 @@ from backend.app.utils.health_check import ensure_unique_route_names, http_limit_callback +@asynccontextmanager +async def register_init(app: FastAPI): + """ + 启动初始化 + + :return: + """ + # 连接redis + await redis_client.open() + # 初始化 limiter + await FastAPILimiter.init(redis_client, prefix=settings.LIMITER_REDIS_PREFIX, http_callback=http_limit_callback) + + yield + + # 关闭redis连接 + await redis_client.close() + # 关闭 limiter + await FastAPILimiter.close() + + def register_app(): # FastAPI app = FastAPI( @@ -26,6 +47,7 @@ def register_app(): docs_url=settings.DOCS_URL, redoc_url=settings.REDOCS_URL, openapi_url=settings.OPENAPI_URL, + lifespan=register_init, ) # 注册静态文件 @@ -100,27 +122,6 @@ def register_router(app: FastAPI): ensure_unique_route_names(app) -def register_init(app: FastAPI): - """ - 初始化连接 - - :param app: FastAPI - :return: - """ - - @app.on_event('startup') - async def startup(): - # 连接redis - await redis_client.open() - # 初始化 limiter - await FastAPILimiter.init(redis_client, prefix=settings.LIMITER_REDIS_PREFIX, http_callback=http_limit_callback) - - @app.on_event('shutdown') - async def shutdown(): - # 关闭redis连接 - await redis_client.close() - - def register_db(app: FastAPI): register_tortoise( app, diff --git a/backend/app/migration.py b/backend/app/migration.py index dc2a60a..6ea4b02 100644 --- a/backend/app/migration.py +++ b/backend/app/migration.py @@ -4,8 +4,8 @@ sys.path.append('../../') -from backend.app.database.db_mysql import mysql_config # noqa: E402 -from backend.app.models import models # noqa: E402 +from backend.app.database.db_mysql import mysql_config +from backend.app.models import models TORTOISE_ORM = { 'connections': mysql_config['connections'], diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..2bfc66b --- /dev/null +++ b/pdm.lock @@ -0,0 +1,1692 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default"] +strategy = ["cross_platform", "inherit_metadata"] +lock_version = "4.4.1" +content_hash = "sha256:e7aa6f727ea8139883ccfce8e68214883db4d38989185a0bbb2a949c66b5c809" + +[[package]] +name = "aerich" +version = "0.7.2" +requires_python = ">=3.7,<4.0" +summary = "A database migrations tool for Tortoise ORM." +groups = ["default"] +dependencies = [ + "click", + "dictdiffer", + "pydantic", + "tomlkit", + "tortoise-orm", +] +files = [ + {file = "aerich-0.7.2-py3-none-any.whl", hash = "sha256:84c78c07d45436b89ca4db5411eca4e9292a591fb7d6fd4282fa4a7d0c6d2af1"}, + {file = "aerich-0.7.2.tar.gz", hash = "sha256:31d67de7b96184636b89de99062e059e5e6204b6251d24c33eb21fc9cf982e09"}, +] + +[[package]] +name = "aiofiles" +version = "23.2.1" +requires_python = ">=3.7" +summary = "File support for asyncio." +groups = ["default"] +files = [ + {file = "aiofiles-23.2.1-py3-none-any.whl", hash = "sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107"}, + {file = "aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a"}, +] + +[[package]] +name = "aiosmtplib" +version = "3.0.1" +requires_python = ">=3.8,<4.0" +summary = "asyncio SMTP client" +groups = ["default"] +files = [ + {file = "aiosmtplib-3.0.1-py3-none-any.whl", hash = "sha256:abcceae7e820577307b4cda2041b2c25e5121469c0e186764ddf8e15b12064cd"}, + {file = "aiosmtplib-3.0.1.tar.gz", hash = "sha256:43580604b152152a221598be3037f0ae6359c2817187ac4433bd857bc3fc6513"}, +] + +[[package]] +name = "aiosqlite" +version = "0.17.0" +requires_python = ">=3.6" +summary = "asyncio bridge to the standard sqlite3 module" +groups = ["default"] +dependencies = [ + "typing-extensions>=3.7.2", +] +files = [ + {file = "aiosqlite-0.17.0-py3-none-any.whl", hash = "sha256:6c49dc6d3405929b1d08eeccc72306d3677503cc5e5e43771efc1e00232e8231"}, + {file = "aiosqlite-0.17.0.tar.gz", hash = "sha256:f0e6acc24bc4864149267ac82fb46dfb3be4455f99fe21df82609cc6e6baee51"}, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +requires_python = ">=3.8" +summary = "Reusable constraint types to use with typing.Annotated" +groups = ["default"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.3.0" +requires_python = ">=3.8" +summary = "High level compatibility layer for multiple asynchronous event loop implementations" +groups = ["default"] +dependencies = [ + "exceptiongroup>=1.0.2; python_version < \"3.11\"", + "idna>=2.8", + "sniffio>=1.1", + "typing-extensions>=4.1; python_version < \"3.11\"", +] +files = [ + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, +] + +[[package]] +name = "asgiref" +version = "3.8.1" +requires_python = ">=3.8" +summary = "ASGI specs, helper code, and adapters" +groups = ["default"] +dependencies = [ + "typing-extensions>=4; python_version < \"3.11\"", +] +files = [ + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, +] + +[[package]] +name = "async-timeout" +version = "4.0.3" +requires_python = ">=3.7" +summary = "Timeout context manager for asyncio programs" +groups = ["default"] +marker = "python_full_version < \"3.11.3\"" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "asyncmy" +version = "0.2.9" +requires_python = ">=3.7,<4.0" +summary = "A fast asyncio MySQL driver" +groups = ["default"] +files = [ + {file = "asyncmy-0.2.9-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:d077eaee9a126f36bbe95e0412baa89e93172dd46193ef7bf7650a686e458e50"}, + {file = "asyncmy-0.2.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83cf951a44294626df43c5a85cf328297c3bac63f25ede216f9706514dabb322"}, + {file = "asyncmy-0.2.9-cp310-cp310-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:8a1d63c1bb8e3a09c90767199954fd423c48084a1f6c0d956217bc2e48d37d6d"}, + {file = "asyncmy-0.2.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ecad6826086e47596c6aa65dcbe221305f3d9232f0d4de11b8562ee2c55464a"}, + {file = "asyncmy-0.2.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a664d58f9ebe4132f6cb3128206392be8ad71ad6fb09a5f4a990b04ec142024"}, + {file = "asyncmy-0.2.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f2bbd7b75e2d751216f48c3b1b5092b812d70c2cd0053f8d2f50ec3f76a525a8"}, + {file = "asyncmy-0.2.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:55e3bc41aa0d4ab410fc3a1d0c31b9cdb6688cd3b0cae6f2ee49c2e7f42968be"}, + {file = "asyncmy-0.2.9-cp310-cp310-win32.whl", hash = "sha256:ea44eefc965c62bcfebf34e9ef00f6e807edf51046046767c56914243e0737e4"}, + {file = "asyncmy-0.2.9-cp310-cp310-win_amd64.whl", hash = "sha256:2b4a2a7cf0bd5051931756e765fefef3c9f9561550e0dd8b1e79308d048b710a"}, + {file = "asyncmy-0.2.9-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:e2b77f03a17a8db338d74311e38ca6dbd4ff9aacb07d2af6b9e0cac9cf1c7b87"}, + {file = "asyncmy-0.2.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c19f27b7ff0e297f2981335a85599ffe1c9a8a35c97230203321d5d6e9e4cb30"}, + {file = "asyncmy-0.2.9-cp311-cp311-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:bf18aef65ac98f5130ca588c55a83a56e74ae416cf0fe2c0757a2b597c4269d0"}, + {file = "asyncmy-0.2.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef02186cc02cb767ee5d5cf9ab002d5c7910a1a9f4c16a666867a9325c9ec5e"}, + {file = "asyncmy-0.2.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:696da0f71db0fe11e62fa58cd5a27d7c9d9a90699d13d82640755d0061da0624"}, + {file = "asyncmy-0.2.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84d20745bb187ced05bd4072ae8b0bff4b4622efa23b79935519edb717174584"}, + {file = "asyncmy-0.2.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea242364523f6205c4426435272bd57cbf593c20d5e5551efb28d44cfbd595c2"}, + {file = "asyncmy-0.2.9-cp311-cp311-win32.whl", hash = "sha256:47609d34e6b49fc5ad5bd2a2a593ca120e143e2a4f4206f27a543c5c598a18ca"}, + {file = "asyncmy-0.2.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d56df7342f7b5467a9d09a854f0e5602c8da09afdad8181ba40b0434d66d8a4"}, + {file = "asyncmy-0.2.9-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9f1ca623517552a637900b90d65b5bafc9c67bebf96e3427eecb9359ffa24b1"}, + {file = "asyncmy-0.2.9-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:49622dc4ec69b5a4cbddb3695a1e9249b31092c6f19604abb664b43dcb509b6f"}, + {file = "asyncmy-0.2.9-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8412e825443ee876ef0d55ac4356b56173f5cb64ca8e4638974f8cf5c912a63"}, + {file = "asyncmy-0.2.9-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:4025db2a27b1d84d3c68b5d5aacecac17258b69f25ec8a8c350c5f666003a778"}, + {file = "asyncmy-0.2.9-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da7640f3357849b176364ed546908e28c8460701ddc0d23cc3fa7113ec52a076"}, + {file = "asyncmy-0.2.9-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:d2593717fa7a92a7d361444726292ce34edea76d5aa67d469b5efeee1c9b729e"}, + {file = "asyncmy-0.2.9-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9f22e13bd77277593b56de2e4b65c40c2e81b1a42c4845d062403c5c5bc52bc"}, + {file = "asyncmy-0.2.9-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a4aa17cc6ac0f7bc6b72e08d112566e69a36e2e1ebebad43d699757b7b4ff028"}, + {file = "asyncmy-0.2.9-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7e6f5205722e67c910510e294ad483bdafa7e29d5cf455d49ffa4b819e55fd8"}, + {file = "asyncmy-0.2.9-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux_2_5_i686.manylinux1_i686.manylinux2014_i686.whl", hash = "sha256:1021796f1910a0c2ab2d878f8f5d56f939ef0681f9c1fe925b78161cad2f8297"}, + {file = "asyncmy-0.2.9-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux_2_5_x86_64.manylinux1_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b1dd463bb054138bd1fd3fec9911eb618e92f54f61abb476658f863340394d1"}, + {file = "asyncmy-0.2.9-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad06f3c02d455947e95087d29f7122411208f0eadaf8671772fe5bad97d9873a"}, + {file = "asyncmy-0.2.9.tar.gz", hash = "sha256:da188be013291d1f831d63cdd3614567f4c63bfdcde73631ddff8df00c56d614"}, +] + +[[package]] +name = "bcrypt" +version = "4.1.3" +requires_python = ">=3.7" +summary = "Modern password hashing for your software and your servers" +groups = ["default"] +files = [ + {file = "bcrypt-4.1.3-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:48429c83292b57bf4af6ab75809f8f4daf52aa5d480632e53707805cc1ce9b74"}, + {file = "bcrypt-4.1.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a8bea4c152b91fd8319fef4c6a790da5c07840421c2b785084989bf8bbb7455"}, + {file = "bcrypt-4.1.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d3b317050a9a711a5c7214bf04e28333cf528e0ed0ec9a4e55ba628d0f07c1a"}, + {file = "bcrypt-4.1.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:094fd31e08c2b102a14880ee5b3d09913ecf334cd604af27e1013c76831f7b05"}, + {file = "bcrypt-4.1.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4fb253d65da30d9269e0a6f4b0de32bd657a0208a6f4e43d3e645774fb5457f3"}, + {file = "bcrypt-4.1.3-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:193bb49eeeb9c1e2db9ba65d09dc6384edd5608d9d672b4125e9320af9153a15"}, + {file = "bcrypt-4.1.3-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:8cbb119267068c2581ae38790e0d1fbae65d0725247a930fc9900c285d95725d"}, + {file = "bcrypt-4.1.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6cac78a8d42f9d120b3987f82252bdbeb7e6e900a5e1ba37f6be6fe4e3848286"}, + {file = "bcrypt-4.1.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:01746eb2c4299dd0ae1670234bf77704f581dd72cc180f444bfe74eb80495b64"}, + {file = "bcrypt-4.1.3-cp37-abi3-win32.whl", hash = "sha256:037c5bf7c196a63dcce75545c8874610c600809d5d82c305dd327cd4969995bf"}, + {file = "bcrypt-4.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:8a893d192dfb7c8e883c4576813bf18bb9d59e2cfd88b68b725990f033f1b978"}, + {file = "bcrypt-4.1.3-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d4cf6ef1525f79255ef048b3489602868c47aea61f375377f0d00514fe4a78c"}, + {file = "bcrypt-4.1.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5698ce5292a4e4b9e5861f7e53b1d89242ad39d54c3da451a93cac17b61921a"}, + {file = "bcrypt-4.1.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec3c2e1ca3e5c4b9edb94290b356d082b721f3f50758bce7cce11d8a7c89ce84"}, + {file = "bcrypt-4.1.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3a5be252fef513363fe281bafc596c31b552cf81d04c5085bc5dac29670faa08"}, + {file = "bcrypt-4.1.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5f7cd3399fbc4ec290378b541b0cf3d4398e4737a65d0f938c7c0f9d5e686611"}, + {file = "bcrypt-4.1.3-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:c4c8d9b3e97209dd7111bf726e79f638ad9224b4691d1c7cfefa571a09b1b2d6"}, + {file = "bcrypt-4.1.3-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:31adb9cbb8737a581a843e13df22ffb7c84638342de3708a98d5c986770f2834"}, + {file = "bcrypt-4.1.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:551b320396e1d05e49cc18dd77d970accd52b322441628aca04801bbd1d52a73"}, + {file = "bcrypt-4.1.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6717543d2c110a155e6821ce5670c1f512f602eabb77dba95717ca76af79867d"}, + {file = "bcrypt-4.1.3-cp39-abi3-win32.whl", hash = "sha256:6004f5229b50f8493c49232b8e75726b568535fd300e5039e255d919fc3a07f2"}, + {file = "bcrypt-4.1.3-cp39-abi3-win_amd64.whl", hash = "sha256:2505b54afb074627111b5a8dc9b6ae69d0f01fea65c2fcaea403448c503d3991"}, + {file = "bcrypt-4.1.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:cb9c707c10bddaf9e5ba7cdb769f3e889e60b7d4fea22834b261f51ca2b89fed"}, + {file = "bcrypt-4.1.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9f8ea645eb94fb6e7bea0cf4ba121c07a3a182ac52876493870033141aa687bc"}, + {file = "bcrypt-4.1.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f44a97780677e7ac0ca393bd7982b19dbbd8d7228c1afe10b128fd9550eef5f1"}, + {file = "bcrypt-4.1.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d84702adb8f2798d813b17d8187d27076cca3cd52fe3686bb07a9083930ce650"}, + {file = "bcrypt-4.1.3.tar.gz", hash = "sha256:2ee15dd749f5952fe3f0430d0ff6b74082e159c50332a1413d51b5689cf06623"}, +] + +[[package]] +name = "certifi" +version = "2024.2.2" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["default"] +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "cffi" +version = "1.16.0" +requires_python = ">=3.8" +summary = "Foreign Function Interface for Python calling C code." +groups = ["default"] +marker = "platform_python_implementation != \"PyPy\"" +dependencies = [ + "pycparser", +] +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +requires_python = ">=3.8" +summary = "Validate configuration and produce human readable error messages." +groups = ["default"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "click" +version = "8.1.7" +requires_python = ">=3.7" +summary = "Composable command line interface toolkit" +groups = ["default"] +dependencies = [ + "colorama; platform_system == \"Windows\"", +] +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["default"] +marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "cryptography" +version = "42.0.7" +requires_python = ">=3.7" +summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +groups = ["default"] +dependencies = [ + "cffi>=1.12; platform_python_implementation != \"PyPy\"", +] +files = [ + {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477"}, + {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a"}, + {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604"}, + {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8"}, + {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55"}, + {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc"}, + {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2"}, + {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13"}, + {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da"}, + {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7"}, + {file = "cryptography-42.0.7-cp37-abi3-win32.whl", hash = "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b"}, + {file = "cryptography-42.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678"}, + {file = "cryptography-42.0.7-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4"}, + {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858"}, + {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785"}, + {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda"}, + {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9"}, + {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e"}, + {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f"}, + {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1"}, + {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886"}, + {file = "cryptography-42.0.7-cp39-abi3-win32.whl", hash = "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda"}, + {file = "cryptography-42.0.7-cp39-abi3-win_amd64.whl", hash = "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b"}, + {file = "cryptography-42.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82"}, + {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60"}, + {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd"}, + {file = "cryptography-42.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582"}, + {file = "cryptography-42.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562"}, + {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14"}, + {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9"}, + {file = "cryptography-42.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68"}, + {file = "cryptography-42.0.7.tar.gz", hash = "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2"}, +] + +[[package]] +name = "dictdiffer" +version = "0.9.0" +summary = "Dictdiffer is a library that helps you to diff and patch dictionaries." +groups = ["default"] +files = [ + {file = "dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595"}, + {file = "dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +summary = "Distribution utilities" +groups = ["default"] +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "dnspython" +version = "2.6.1" +requires_python = ">=3.8" +summary = "DNS toolkit" +groups = ["default"] +files = [ + {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, + {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, +] + +[[package]] +name = "ecdsa" +version = "0.19.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.6" +summary = "ECDSA cryptographic signature library (pure python)" +groups = ["default"] +dependencies = [ + "six>=1.9.0", +] +files = [ + {file = "ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a"}, + {file = "ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"}, +] + +[[package]] +name = "email-validator" +version = "2.1.1" +requires_python = ">=3.8" +summary = "A robust email address syntax and deliverability validation library." +groups = ["default"] +dependencies = [ + "dnspython>=2.0.0", + "idna>=2.0.0", +] +files = [ + {file = "email_validator-2.1.1-py3-none-any.whl", hash = "sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"}, + {file = "email_validator-2.1.1.tar.gz", hash = "sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.1" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +groups = ["default"] +marker = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, +] + +[[package]] +name = "fast-captcha" +version = "0.2.1" +requires_python = ">=3.7,<4.0" +summary = "Fast to use captcha" +groups = ["default"] +dependencies = [ + "Pillow<10.0.0,>=9.2.0", +] +files = [ + {file = "fast_captcha-0.2.1-py3-none-any.whl", hash = "sha256:fef14828609a96cc286f9b09c84126628221ae578d3b5b984d50e03834c07f7d"}, + {file = "fast_captcha-0.2.1.tar.gz", hash = "sha256:63130402bdf8945974702ff136c05f98191b215083182c5684f694c3c1f7d257"}, +] + +[[package]] +name = "fastapi" +version = "0.111.0" +requires_python = ">=3.8" +summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +groups = ["default"] +dependencies = [ + "email-validator>=2.0.0", + "fastapi-cli>=0.0.2", + "httpx>=0.23.0", + "jinja2>=2.11.2", + "orjson>=3.2.1", + "pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4", + "python-multipart>=0.0.7", + "starlette<0.38.0,>=0.37.2", + "typing-extensions>=4.8.0", + "ujson!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,>=4.0.1", + "uvicorn[standard]>=0.12.0", +] +files = [ + {file = "fastapi-0.111.0-py3-none-any.whl", hash = "sha256:97ecbf994be0bcbdadedf88c3150252bed7b2087075ac99735403b1b76cc8fc0"}, + {file = "fastapi-0.111.0.tar.gz", hash = "sha256:b9db9dd147c91cb8b769f7183535773d8741dd46f9dc6676cd82eab510228cd7"}, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.4" +requires_python = ">=3.8" +summary = "Run and manage FastAPI apps from the command line with FastAPI CLI. 🚀" +groups = ["default"] +dependencies = [ + "typer>=0.12.3", +] +files = [ + {file = "fastapi_cli-0.0.4-py3-none-any.whl", hash = "sha256:a2552f3a7ae64058cdbb530be6fa6dbfc975dc165e4fa66d224c3d396e25e809"}, + {file = "fastapi_cli-0.0.4.tar.gz", hash = "sha256:e2e9ffaffc1f7767f488d6da34b6f5a377751c996f397902eb6abb99a67bde32"}, +] + +[[package]] +name = "fastapi-limiter" +version = "0.1.6" +requires_python = ">=3.9,<4.0" +summary = "A request rate limiter for fastapi" +groups = ["default"] +dependencies = [ + "fastapi", + "redis>=4.2.0rc1", +] +files = [ + {file = "fastapi_limiter-0.1.6-py3-none-any.whl", hash = "sha256:2e53179a4208b8f2c8795e38bb001324d3dc37d2800ff49fd28ec5caabf7a240"}, + {file = "fastapi_limiter-0.1.6.tar.gz", hash = "sha256:6f5fde8efebe12eb33861bdffb91009f699369a3c2862cdc7c1d9acf912ff443"}, +] + +[[package]] +name = "fastapi-pagination" +version = "0.12.24" +requires_python = "<4.0,>=3.8" +summary = "FastAPI pagination" +groups = ["default"] +dependencies = [ + "fastapi>=0.93.0", + "pydantic>=1.9.1", + "typing-extensions<5.0.0,>=4.8.0", +] +files = [ + {file = "fastapi_pagination-0.12.24-py3-none-any.whl", hash = "sha256:a639df7301a89414244c6763bb97cff043815cb839070b8a38c58c007cf75d48"}, + {file = "fastapi_pagination-0.12.24.tar.gz", hash = "sha256:c9c6508e0182aab679a13b1de261d4923e3b530b410500dcb271638ff714fb14"}, +] + +[[package]] +name = "fastapi" +version = "0.111.0" +extras = ["all"] +requires_python = ">=3.8" +summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +groups = ["default"] +dependencies = [ + "email-validator>=2.0.0", + "fastapi==0.111.0", + "httpx>=0.23.0", + "itsdangerous>=1.1.0", + "jinja2>=2.11.2", + "orjson>=3.2.1", + "pydantic-extra-types>=2.0.0", + "pydantic-settings>=2.0.0", + "python-multipart>=0.0.7", + "pyyaml>=5.3.1", + "ujson!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,>=4.0.1", + "uvicorn[standard]>=0.12.0", +] +files = [ + {file = "fastapi-0.111.0-py3-none-any.whl", hash = "sha256:97ecbf994be0bcbdadedf88c3150252bed7b2087075ac99735403b1b76cc8fc0"}, + {file = "fastapi-0.111.0.tar.gz", hash = "sha256:b9db9dd147c91cb8b769f7183535773d8741dd46f9dc6676cd82eab510228cd7"}, +] + +[[package]] +name = "filelock" +version = "3.14.0" +requires_python = ">=3.8" +summary = "A platform independent file lock." +groups = ["default"] +files = [ + {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"}, + {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"}, +] + +[[package]] +name = "gunicorn" +version = "22.0.0" +requires_python = ">=3.7" +summary = "WSGI HTTP Server for UNIX" +groups = ["default"] +dependencies = [ + "packaging", +] +files = [ + {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"}, + {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"}, +] + +[[package]] +name = "h11" +version = "0.14.0" +requires_python = ">=3.7" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +groups = ["default"] +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "hiredis" +version = "2.3.2" +requires_python = ">=3.7" +summary = "Python wrapper for hiredis" +groups = ["default"] +files = [ + {file = "hiredis-2.3.2-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:742093f33d374098aa21c1696ac6e4874b52658c870513a297a89265a4d08fe5"}, + {file = "hiredis-2.3.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:9e14fb70ca4f7efa924f508975199353bf653f452e4ef0a1e47549e208f943d7"}, + {file = "hiredis-2.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d7302b4b17fcc1cc727ce84ded7f6be4655701e8d58744f73b09cb9ed2b13df"}, + {file = "hiredis-2.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed63e8b75c193c5e5a8288d9d7b011da076cc314fafc3bfd59ec1d8a750d48c8"}, + {file = "hiredis-2.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b4edee59dc089bc3948f4f6fba309f51aa2ccce63902364900aa0a553a85e97"}, + {file = "hiredis-2.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6481c3b7673a86276220140456c2a6fbfe8d1fb5c613b4728293c8634134824"}, + {file = "hiredis-2.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684840b014ce83541a087fcf2d48227196576f56ae3e944d4dfe14c0a3e0ccb7"}, + {file = "hiredis-2.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c4c0bcf786f0eac9593367b6279e9b89534e008edbf116dcd0de956524702c8"}, + {file = "hiredis-2.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66ab949424ac6504d823cba45c4c4854af5c59306a1531edb43b4dd22e17c102"}, + {file = "hiredis-2.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:322c668ee1c12d6c5750a4b1057e6b4feee2a75b3d25d630922a463cfe5e7478"}, + {file = "hiredis-2.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:bfa73e3f163c6e8b2ec26f22285d717a5f77ab2120c97a2605d8f48b26950dac"}, + {file = "hiredis-2.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7f39f28ffc65de577c3bc0c7615f149e35bc927802a0f56e612db9b530f316f9"}, + {file = "hiredis-2.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:55ce31bf4711da879b96d511208efb65a6165da4ba91cb3a96d86d5a8d9d23e6"}, + {file = "hiredis-2.3.2-cp310-cp310-win32.whl", hash = "sha256:3dd63d0bbbe75797b743f35d37a4cca7ca7ba35423a0de742ae2985752f20c6d"}, + {file = "hiredis-2.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:ea002656a8d974daaf6089863ab0a306962c8b715db6b10879f98b781a2a5bf5"}, + {file = "hiredis-2.3.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:adfbf2e9c38b77d0db2fb32c3bdaea638fa76b4e75847283cd707521ad2475ef"}, + {file = "hiredis-2.3.2-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:80b02d27864ebaf9b153d4b99015342382eeaed651f5591ce6f07e840307c56d"}, + {file = "hiredis-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd40d2e2f82a483de0d0a6dfd8c3895a02e55e5c9949610ecbded18188fd0a56"}, + {file = "hiredis-2.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfa904045d7cebfb0f01dad51352551cce1d873d7c3f80c7ded7d42f8cac8f89"}, + {file = "hiredis-2.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28bd184b33e0dd6d65816c16521a4ba1ffbe9ff07d66873c42ea4049a62fed83"}, + {file = "hiredis-2.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f70481213373d44614148f0f2e38e7905be3f021902ae5167289413196de4ba4"}, + {file = "hiredis-2.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8797b528c1ff81eef06713623562b36db3dafa106b59f83a6468df788ff0d1"}, + {file = "hiredis-2.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02fc71c8333586871602db4774d3a3e403b4ccf6446dc4603ec12df563127cee"}, + {file = "hiredis-2.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0da56915bda1e0a49157191b54d3e27689b70960f0685fdd5c415dacdee2fbed"}, + {file = "hiredis-2.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e2674a5a3168349435b08fa0b82998ed2536eb9acccf7087efe26e4cd088a525"}, + {file = "hiredis-2.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:dc1c3fd49930494a67dcec37d0558d99d84eca8eb3f03b17198424538f2608d7"}, + {file = "hiredis-2.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:14c7b43205e515f538a9defb4e411e0f0576caaeeda76bb9993ed505486f7562"}, + {file = "hiredis-2.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7bac7e02915b970c3723a7a7c5df4ba7a11a3426d2a3f181e041aa506a1ff028"}, + {file = "hiredis-2.3.2-cp311-cp311-win32.whl", hash = "sha256:63a090761ddc3c1f7db5e67aa4e247b4b3bb9890080bdcdadd1b5200b8b89ac4"}, + {file = "hiredis-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:70d226ab0306a5b8d408235cabe51d4bf3554c9e8a72d53ce0b3c5c84cf78881"}, + {file = "hiredis-2.3.2-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:5c614552c6bd1d0d907f448f75550f6b24fb56cbfce80c094908b7990cad9702"}, + {file = "hiredis-2.3.2-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:9c431431abf55b64347ddc8df68b3ef840269cb0aa5bc2d26ad9506eb4b1b866"}, + {file = "hiredis-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a45857e87e9d2b005e81ddac9d815a33efd26ec67032c366629f023fe64fb415"}, + {file = "hiredis-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e138d141ec5a6ec800b6d01ddc3e5561ce1c940215e0eb9960876bfde7186aae"}, + {file = "hiredis-2.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:387f655444d912a963ab68abf64bf6e178a13c8e4aa945cb27388fd01a02e6f1"}, + {file = "hiredis-2.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4852f4bf88f0e2d9bdf91279892f5740ed22ae368335a37a52b92a5c88691140"}, + {file = "hiredis-2.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d711c107e83117129b7f8bd08e9820c43ceec6204fff072a001fd82f6d13db9f"}, + {file = "hiredis-2.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92830c16885f29163e1c2da1f3c1edb226df1210ec7e8711aaabba3dd0d5470a"}, + {file = "hiredis-2.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:16b01d9ceae265d4ab9547be0cd628ecaff14b3360357a9d30c029e5ae8b7e7f"}, + {file = "hiredis-2.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5986fb5f380169270a0293bebebd95466a1c85010b4f1afc2727e4d17c452512"}, + {file = "hiredis-2.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:49532d7939cc51f8e99efc326090c54acf5437ed88b9c904cc8015b3c4eda9c9"}, + {file = "hiredis-2.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:8f34801b251ca43ad70691fb08b606a2e55f06b9c9fb1fc18fd9402b19d70f7b"}, + {file = "hiredis-2.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7298562a49d95570ab1c7fc4051e72824c6a80e907993a21a41ba204223e7334"}, + {file = "hiredis-2.3.2-cp312-cp312-win32.whl", hash = "sha256:e1d86b75de787481b04d112067a4033e1ecfda2a060e50318a74e4e1c9b2948c"}, + {file = "hiredis-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:6dbfe1887ffa5cf3030451a56a8f965a9da2fa82b7149357752b67a335a05fc6"}, + {file = "hiredis-2.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7e8bf4444b09419b77ce671088db9f875b26720b5872d97778e2545cd87dba4a"}, + {file = "hiredis-2.3.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bd42d0d45ea47a2f96babd82a659fbc60612ab9423a68e4a8191e538b85542a"}, + {file = "hiredis-2.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80441b55edbef868e2563842f5030982b04349408396e5ac2b32025fb06b5212"}, + {file = "hiredis-2.3.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec444ab8f27562a363672d6a7372bc0700a1bdc9764563c57c5f9efa0e592b5f"}, + {file = "hiredis-2.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f9f606e810858207d4b4287b4ef0dc622c2aa469548bf02b59dcc616f134f811"}, + {file = "hiredis-2.3.2-pp37-pypy37_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c3dde4ca00fe9eee3b76209711f1941bb86db42b8a75d7f2249ff9dfc026ab0e"}, + {file = "hiredis-2.3.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4dd676107a1d3c724a56a9d9db38166ad4cf44f924ee701414751bd18a784a0"}, + {file = "hiredis-2.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce42649e2676ad783186264d5ffc788a7612ecd7f9effb62d51c30d413a3eefe"}, + {file = "hiredis-2.3.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e3f8b1733078ac663dad57e20060e16389a60ab542f18a97931f3a2a2dd64a4"}, + {file = "hiredis-2.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:532a84a82156a82529ec401d1c25d677c6543c791e54a263aa139541c363995f"}, + {file = "hiredis-2.3.2-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d59f88c4daa36b8c38e59ac7bffed6f5d7f68eaccad471484bf587b28ccc478"}, + {file = "hiredis-2.3.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91a14dd95e24dc078204b18b0199226ee44644974c645dc54ee7b00c3157330"}, + {file = "hiredis-2.3.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb777a38797c8c7df0444533119570be18d1a4ce5478dffc00c875684df7bfcb"}, + {file = "hiredis-2.3.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d47c915897a99d0d34a39fad4be97b4b709ab3d0d3b779ebccf2b6024a8c681e"}, + {file = "hiredis-2.3.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:333b5e04866758b11bda5f5315b4e671d15755fc6ed3b7969721bc6311d0ee36"}, + {file = "hiredis-2.3.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c8937f1100435698c18e4da086968c4b5d70e86ea718376f833475ab3277c9aa"}, + {file = "hiredis-2.3.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa45f7d771094b8145af10db74704ab0f698adb682fbf3721d8090f90e42cc49"}, + {file = "hiredis-2.3.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33d5ebc93c39aed4b5bc769f8ce0819bc50e74bb95d57a35f838f1c4378978e0"}, + {file = "hiredis-2.3.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a797d8c7df9944314d309b0d9e1b354e2fa4430a05bb7604da13b6ad291bf959"}, + {file = "hiredis-2.3.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e15a408f71a6c8c87b364f1f15a6cd9c1baca12bbc47a326ac8ab99ec7ad3c64"}, + {file = "hiredis-2.3.2.tar.gz", hash = "sha256:733e2456b68f3f126ddaf2cd500a33b25146c3676b97ea843665717bda0c5d43"}, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +requires_python = ">=3.8" +summary = "A minimal low-level HTTP client." +groups = ["default"] +dependencies = [ + "certifi", + "h11<0.15,>=0.13", +] +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[[package]] +name = "httptools" +version = "0.6.1" +requires_python = ">=3.8.0" +summary = "A collection of framework independent HTTP protocol utils." +groups = ["default"] +files = [ + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"}, + {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"}, + {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"}, + {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"}, + {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"}, +] + +[[package]] +name = "httpx" +version = "0.27.0" +requires_python = ">=3.8" +summary = "The next generation HTTP client." +groups = ["default"] +dependencies = [ + "anyio", + "certifi", + "httpcore==1.*", + "idna", + "sniffio", +] +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + +[[package]] +name = "identify" +version = "2.5.36" +requires_python = ">=3.8" +summary = "File identification library for Python" +groups = ["default"] +files = [ + {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, + {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, +] + +[[package]] +name = "idna" +version = "3.7" +requires_python = ">=3.5" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["default"] +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "iso8601" +version = "1.1.0" +requires_python = ">=3.6.2,<4.0" +summary = "Simple module to parse ISO 8601 dates" +groups = ["default"] +files = [ + {file = "iso8601-1.1.0-py3-none-any.whl", hash = "sha256:8400e90141bf792bce2634df533dc57e3bee19ea120a87bebcd3da89a58ad73f"}, + {file = "iso8601-1.1.0.tar.gz", hash = "sha256:32811e7b81deee2063ea6d2e94f8819a86d1f3811e49d23623a41fa832bef03f"}, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +requires_python = ">=3.8" +summary = "Safely pass data to untrusted environments and back." +groups = ["default"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +requires_python = ">=3.7" +summary = "A very fast and expressive template engine." +groups = ["default"] +dependencies = [ + "MarkupSafe>=2.0", +] +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[[package]] +name = "loguru" +version = "0.7.2" +requires_python = ">=3.5" +summary = "Python logging made (stupidly) simple" +groups = ["default"] +dependencies = [ + "colorama>=0.3.4; sys_platform == \"win32\"", + "win32-setctime>=1.0.0; sys_platform == \"win32\"", +] +files = [ + {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, + {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +requires_python = ">=3.8" +summary = "Python port of markdown-it. Markdown parsing, done right!" +groups = ["default"] +dependencies = [ + "mdurl~=0.1", +] +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +requires_python = ">=3.7" +summary = "Safely add untrusted strings to HTML/XML markup." +groups = ["default"] +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +requires_python = ">=3.7" +summary = "Markdown URL utilities" +groups = ["default"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "nodeenv" +version = "1.8.0" +requires_python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +summary = "Node.js virtual environment builder" +groups = ["default"] +dependencies = [ + "setuptools", +] +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[[package]] +name = "orjson" +version = "3.10.3" +requires_python = ">=3.8" +summary = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +groups = ["default"] +files = [ + {file = "orjson-3.10.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9fb6c3f9f5490a3eb4ddd46fc1b6eadb0d6fc16fb3f07320149c3286a1409dd8"}, + {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:252124b198662eee80428f1af8c63f7ff077c88723fe206a25df8dc57a57b1fa"}, + {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f3e87733823089a338ef9bbf363ef4de45e5c599a9bf50a7a9b82e86d0228da"}, + {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8334c0d87103bb9fbbe59b78129f1f40d1d1e8355bbed2ca71853af15fa4ed3"}, + {file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1952c03439e4dce23482ac846e7961f9d4ec62086eb98ae76d97bd41d72644d7"}, + {file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0403ed9c706dcd2809f1600ed18f4aae50be263bd7112e54b50e2c2bc3ebd6d"}, + {file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:382e52aa4270a037d41f325e7d1dfa395b7de0c367800b6f337d8157367bf3a7"}, + {file = "orjson-3.10.3-cp310-none-win32.whl", hash = "sha256:be2aab54313752c04f2cbaab4515291ef5af8c2256ce22abc007f89f42f49109"}, + {file = "orjson-3.10.3-cp310-none-win_amd64.whl", hash = "sha256:416b195f78ae461601893f482287cee1e3059ec49b4f99479aedf22a20b1098b"}, + {file = "orjson-3.10.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:73100d9abbbe730331f2242c1fc0bcb46a3ea3b4ae3348847e5a141265479700"}, + {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:544a12eee96e3ab828dbfcb4d5a0023aa971b27143a1d35dc214c176fdfb29b3"}, + {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:520de5e2ef0b4ae546bea25129d6c7c74edb43fc6cf5213f511a927f2b28148b"}, + {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccaa0a401fc02e8828a5bedfd80f8cd389d24f65e5ca3954d72c6582495b4bcf"}, + {file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7bc9e8bc11bac40f905640acd41cbeaa87209e7e1f57ade386da658092dc16"}, + {file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3582b34b70543a1ed6944aca75e219e1192661a63da4d039d088a09c67543b08"}, + {file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c23dfa91481de880890d17aa7b91d586a4746a4c2aa9a145bebdbaf233768d5"}, + {file = "orjson-3.10.3-cp311-none-win32.whl", hash = "sha256:1770e2a0eae728b050705206d84eda8b074b65ee835e7f85c919f5705b006c9b"}, + {file = "orjson-3.10.3-cp311-none-win_amd64.whl", hash = "sha256:93433b3c1f852660eb5abdc1f4dd0ced2be031ba30900433223b28ee0140cde5"}, + {file = "orjson-3.10.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a39aa73e53bec8d410875683bfa3a8edf61e5a1c7bb4014f65f81d36467ea098"}, + {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0943a96b3fa09bee1afdfccc2cb236c9c64715afa375b2af296c73d91c23eab2"}, + {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e852baafceff8da3c9defae29414cc8513a1586ad93e45f27b89a639c68e8176"}, + {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18566beb5acd76f3769c1d1a7ec06cdb81edc4d55d2765fb677e3eaa10fa99e0"}, + {file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bd2218d5a3aa43060efe649ec564ebedec8ce6ae0a43654b81376216d5ebd42"}, + {file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cf20465e74c6e17a104ecf01bf8cd3b7b252565b4ccee4548f18b012ff2f8069"}, + {file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba7f67aa7f983c4345eeda16054a4677289011a478ca947cd69c0a86ea45e534"}, + {file = "orjson-3.10.3-cp312-none-win32.whl", hash = "sha256:17e0713fc159abc261eea0f4feda611d32eabc35708b74bef6ad44f6c78d5ea0"}, + {file = "orjson-3.10.3-cp312-none-win_amd64.whl", hash = "sha256:4c895383b1ec42b017dd2c75ae8a5b862fc489006afde06f14afbdd0309b2af0"}, + {file = "orjson-3.10.3.tar.gz", hash = "sha256:2b166507acae7ba2f7c315dcf185a9111ad5e992ac81f2d507aac39193c2c818"}, +] + +[[package]] +name = "packaging" +version = "24.0" +requires_python = ">=3.7" +summary = "Core utilities for Python packages" +groups = ["default"] +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "passlib" +version = "1.7.4" +summary = "comprehensive password hashing framework supporting over 30 schemes" +groups = ["default"] +files = [ + {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, + {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, +] + +[[package]] +name = "path" +version = "16.14.0" +requires_python = ">=3.8" +summary = "A module wrapper for os.path" +groups = ["default"] +files = [ + {file = "path-16.14.0-py3-none-any.whl", hash = "sha256:8ee37703cbdc7cc83835ed4ecc6b638226fb2b43b7b45f26b620589981a109a5"}, + {file = "path-16.14.0.tar.gz", hash = "sha256:dbaaa7efd4602fd6ba8d82890dc7823d69e5de740a6e842d9919b0faaf2b6a8e"}, +] + +[[package]] +name = "pillow" +version = "9.5.0" +requires_python = ">=3.7" +summary = "Python Imaging Library (Fork)" +groups = ["default"] +files = [ + {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, + {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, + {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, + {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, + {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, + {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, + {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, + {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, + {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, + {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.2" +requires_python = ">=3.8" +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +groups = ["default"] +files = [ + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, +] + +[[package]] +name = "pre-commit" +version = "3.7.1" +requires_python = ">=3.9" +summary = "A framework for managing and maintaining multi-language pre-commit hooks." +groups = ["default"] +dependencies = [ + "cfgv>=2.0.0", + "identify>=1.0.0", + "nodeenv>=0.11.1", + "pyyaml>=5.1", + "virtualenv>=20.10.0", +] +files = [ + {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, + {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, +] + +[[package]] +name = "pyasn1" +version = "0.6.0" +requires_python = ">=3.8" +summary = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +groups = ["default"] +files = [ + {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"}, + {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"}, +] + +[[package]] +name = "pycparser" +version = "2.22" +requires_python = ">=3.8" +summary = "C parser in Python" +groups = ["default"] +marker = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pydantic" +version = "2.7.1" +requires_python = ">=3.8" +summary = "Data validation using Python type hints" +groups = ["default"] +dependencies = [ + "annotated-types>=0.4.0", + "pydantic-core==2.18.2", + "typing-extensions>=4.6.1", +] +files = [ + {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, + {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, +] + +[[package]] +name = "pydantic-core" +version = "2.18.2" +requires_python = ">=3.8" +summary = "Core functionality for Pydantic validation and serialization" +groups = ["default"] +dependencies = [ + "typing-extensions!=4.7.0,>=4.6.0", +] +files = [ + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"}, + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"}, + {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"}, + {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, + {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, + {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, + {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, + {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, + {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, + {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, + {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.7.0" +requires_python = ">=3.8" +summary = "Extra Pydantic types." +groups = ["default"] +dependencies = [ + "pydantic>=2.5.2", +] +files = [ + {file = "pydantic_extra_types-2.7.0-py3-none-any.whl", hash = "sha256:ac01bbdaa4f85e4c4744a9792c5b0cfe61efa5028a0e670c3d8bfbf8b36c8543"}, + {file = "pydantic_extra_types-2.7.0.tar.gz", hash = "sha256:b9d9ddd755fa5960ec5a77cffcbd5d8796a0116e1dfc8f7c3a27fa0041693382"}, +] + +[[package]] +name = "pydantic-settings" +version = "2.2.1" +requires_python = ">=3.8" +summary = "Settings management using Pydantic" +groups = ["default"] +dependencies = [ + "pydantic>=2.3.0", + "python-dotenv>=0.21.0", +] +files = [ + {file = "pydantic_settings-2.2.1-py3-none-any.whl", hash = "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"}, + {file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"}, +] + +[[package]] +name = "pygments" +version = "2.18.0" +requires_python = ">=3.8" +summary = "Pygments is a syntax highlighting package written in Python." +groups = ["default"] +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[[package]] +name = "pypika-tortoise" +version = "0.1.6" +requires_python = ">=3.7,<4.0" +summary = "Forked from pypika and streamline just for tortoise-orm" +groups = ["default"] +files = [ + {file = "pypika-tortoise-0.1.6.tar.gz", hash = "sha256:d802868f479a708e3263724c7b5719a26ad79399b2a70cea065f4a4cadbebf36"}, + {file = "pypika_tortoise-0.1.6-py3-none-any.whl", hash = "sha256:2d68bbb7e377673743cff42aa1059f3a80228d411fbcae591e4465e173109fd8"}, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +requires_python = ">=3.8" +summary = "Read key-value pairs from a .env file and set them as environment variables" +groups = ["default"] +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[[package]] +name = "python-jose" +version = "3.3.0" +summary = "JOSE implementation in Python" +groups = ["default"] +dependencies = [ + "ecdsa!=0.15", + "pyasn1", + "rsa", +] +files = [ + {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, + {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, +] + +[[package]] +name = "python-multipart" +version = "0.0.9" +requires_python = ">=3.8" +summary = "A streaming multipart parser for Python" +groups = ["default"] +files = [ + {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, + {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, +] + +[[package]] +name = "pytz" +version = "2024.1" +summary = "World timezone definitions, modern and historical" +groups = ["default"] +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +requires_python = ">=3.6" +summary = "YAML parser and emitter for Python" +groups = ["default"] +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "redis" +version = "5.0.4" +requires_python = ">=3.7" +summary = "Python client for Redis database and key-value store" +groups = ["default"] +dependencies = [ + "async-timeout>=4.0.3; python_full_version < \"3.11.3\"", +] +files = [ + {file = "redis-5.0.4-py3-none-any.whl", hash = "sha256:7adc2835c7a9b5033b7ad8f8918d09b7344188228809c98df07af226d39dec91"}, + {file = "redis-5.0.4.tar.gz", hash = "sha256:ec31f2ed9675cc54c21ba854cfe0462e6faf1d83c8ce5944709db8a4700b9c61"}, +] + +[[package]] +name = "redis" +version = "5.0.4" +extras = ["hiredis"] +requires_python = ">=3.7" +summary = "Python client for Redis database and key-value store" +groups = ["default"] +dependencies = [ + "hiredis>=1.0.0", + "redis==5.0.4", +] +files = [ + {file = "redis-5.0.4-py3-none-any.whl", hash = "sha256:7adc2835c7a9b5033b7ad8f8918d09b7344188228809c98df07af226d39dec91"}, + {file = "redis-5.0.4.tar.gz", hash = "sha256:ec31f2ed9675cc54c21ba854cfe0462e6faf1d83c8ce5944709db8a4700b9c61"}, +] + +[[package]] +name = "rich" +version = "13.7.1" +requires_python = ">=3.7.0" +summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +groups = ["default"] +dependencies = [ + "markdown-it-py>=2.2.0", + "pygments<3.0.0,>=2.13.0", +] +files = [ + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, +] + +[[package]] +name = "rsa" +version = "4.9" +requires_python = ">=3.6,<4" +summary = "Pure-Python RSA implementation" +groups = ["default"] +dependencies = [ + "pyasn1>=0.1.3", +] +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[[package]] +name = "setuptools" +version = "70.0.0" +requires_python = ">=3.8" +summary = "Easily download, build, install, upgrade, and uninstall Python packages" +groups = ["default"] +files = [ + {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"}, + {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"}, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +requires_python = ">=3.7" +summary = "Tool to Detect Surrounding Shell" +groups = ["default"] +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "six" +version = "1.16.0" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +summary = "Python 2 and 3 compatibility utilities" +groups = ["default"] +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +requires_python = ">=3.7" +summary = "Sniff out which async library your code is running under" +groups = ["default"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "starlette" +version = "0.37.2" +requires_python = ">=3.8" +summary = "The little ASGI library that shines." +groups = ["default"] +dependencies = [ + "anyio<5,>=3.4.0", +] +files = [ + {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, + {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, +] + +[[package]] +name = "tomlkit" +version = "0.12.5" +requires_python = ">=3.7" +summary = "Style preserving TOML library" +groups = ["default"] +files = [ + {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, + {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, +] + +[[package]] +name = "tortoise-orm" +version = "0.21.0" +requires_python = "<4.0,>=3.8" +summary = "Easy async ORM for python, built with relations in mind" +groups = ["default"] +dependencies = [ + "aiosqlite<0.18.0,>=0.16.0", + "iso8601<2.0.0,>=1.0.2", + "pydantic!=2.7.0,<3.0,>=2.0", + "pypika-tortoise<0.2.0,>=0.1.6", + "pytz", +] +files = [ + {file = "tortoise_orm-0.21.0-py3-none-any.whl", hash = "sha256:f2252855989756193c18f145226852b74d867754c3b2e3b76e2aa3e010f4ccd0"}, + {file = "tortoise_orm-0.21.0.tar.gz", hash = "sha256:75150a6c802c3a2ca661f7c9d1928c05543e4b77f667eef9b97f2c0ac83ecae6"}, +] + +[[package]] +name = "tortoise-orm" +version = "0.21.0" +extras = ["asyncmy"] +requires_python = "<4.0,>=3.8" +summary = "Easy async ORM for python, built with relations in mind" +groups = ["default"] +dependencies = [ + "asyncmy<0.3.0,>=0.2.8", + "tortoise-orm==0.21.0", +] +files = [ + {file = "tortoise_orm-0.21.0-py3-none-any.whl", hash = "sha256:f2252855989756193c18f145226852b74d867754c3b2e3b76e2aa3e010f4ccd0"}, + {file = "tortoise_orm-0.21.0.tar.gz", hash = "sha256:75150a6c802c3a2ca661f7c9d1928c05543e4b77f667eef9b97f2c0ac83ecae6"}, +] + +[[package]] +name = "typer" +version = "0.12.3" +requires_python = ">=3.7" +summary = "Typer, build great CLIs. Easy to code. Based on Python type hints." +groups = ["default"] +dependencies = [ + "click>=8.0.0", + "rich>=10.11.0", + "shellingham>=1.3.0", + "typing-extensions>=3.7.4.3", +] +files = [ + {file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"}, + {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.0" +requires_python = ">=3.8" +summary = "Backported and Experimental Type Hints for Python 3.8+" +groups = ["default"] +files = [ + {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"}, + {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"}, +] + +[[package]] +name = "ujson" +version = "5.10.0" +requires_python = ">=3.8" +summary = "Ultra fast JSON encoder and decoder for Python" +groups = ["default"] +files = [ + {file = "ujson-5.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd"}, + {file = "ujson-5.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf"}, + {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22cffecf73391e8abd65ef5f4e4dd523162a3399d5e84faa6aebbf9583df86d6"}, + {file = "ujson-5.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26b0e2d2366543c1bb4fbd457446f00b0187a2bddf93148ac2da07a53fe51569"}, + {file = "ujson-5.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caf270c6dba1be7a41125cd1e4fc7ba384bf564650beef0df2dd21a00b7f5770"}, + {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a245d59f2ffe750446292b0094244df163c3dc96b3ce152a2c837a44e7cda9d1"}, + {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94a87f6e151c5f483d7d54ceef83b45d3a9cca7a9cb453dbdbb3f5a6f64033f5"}, + {file = "ujson-5.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29b443c4c0a113bcbb792c88bea67b675c7ca3ca80c3474784e08bba01c18d51"}, + {file = "ujson-5.10.0-cp310-cp310-win32.whl", hash = "sha256:c18610b9ccd2874950faf474692deee4223a994251bc0a083c114671b64e6518"}, + {file = "ujson-5.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:924f7318c31874d6bb44d9ee1900167ca32aa9b69389b98ecbde34c1698a250f"}, + {file = "ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00"}, + {file = "ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126"}, + {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8"}, + {file = "ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b"}, + {file = "ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9"}, + {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f"}, + {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4"}, + {file = "ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1"}, + {file = "ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f"}, + {file = "ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720"}, + {file = "ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5"}, + {file = "ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e"}, + {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043"}, + {file = "ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1"}, + {file = "ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3"}, + {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21"}, + {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2"}, + {file = "ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e"}, + {file = "ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e"}, + {file = "ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc"}, + {file = "ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287"}, + {file = "ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e"}, + {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557"}, + {file = "ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988"}, + {file = "ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816"}, + {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20"}, + {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0"}, + {file = "ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f"}, + {file = "ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165"}, + {file = "ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b6fee72fa77dc172a28f21693f64d93166534c263adb3f96c413ccc85ef6e64"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:61d0af13a9af01d9f26d2331ce49bb5ac1fb9c814964018ac8df605b5422dcb3"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecb24f0bdd899d368b715c9e6664166cf694d1e57be73f17759573a6986dd95a"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbd8fd427f57a03cff3ad6574b5e299131585d9727c8c366da4624a9069ed746"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beeaf1c48e32f07d8820c705ff8e645f8afa690cca1544adba4ebfa067efdc88"}, + {file = "ujson-5.10.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:baed37ea46d756aca2955e99525cc02d9181de67f25515c468856c38d52b5f3b"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7663960f08cd5a2bb152f5ee3992e1af7690a64c0e26d31ba7b3ff5b2ee66337"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8640fb4072d36b08e95a3a380ba65779d356b2fee8696afeb7794cf0902d0a1"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78778a3aa7aafb11e7ddca4e29f46bc5139131037ad628cc10936764282d6753"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0111b27f2d5c820e7f2dbad7d48e3338c824e7ac4d2a12da3dc6061cc39c8e6"}, + {file = "ujson-5.10.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:c66962ca7565605b355a9ed478292da628b8f18c0f2793021ca4425abf8b01e5"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ba43cc34cce49cf2d4bc76401a754a81202d8aa926d0e2b79f0ee258cb15d3a4"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac56eb983edce27e7f51d05bc8dd820586c6e6be1c5216a6809b0c668bb312b8"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44bd4b23a0e723bf8b10628288c2c7c335161d6840013d4d5de20e48551773b"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c10f4654e5326ec14a46bcdeb2b685d4ada6911050aa8baaf3501e57024b804"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0de4971a89a762398006e844ae394bd46991f7c385d7a6a3b93ba229e6dac17e"}, + {file = "ujson-5.10.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e1402f0564a97d2a52310ae10a64d25bcef94f8dd643fcf5d310219d915484f7"}, + {file = "ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1"}, +] + +[[package]] +name = "uvicorn" +version = "0.29.0" +requires_python = ">=3.8" +summary = "The lightning-fast ASGI server." +groups = ["default"] +dependencies = [ + "click>=7.0", + "h11>=0.8", + "typing-extensions>=4.0; python_version < \"3.11\"", +] +files = [ + {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, + {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, +] + +[[package]] +name = "uvicorn" +version = "0.29.0" +extras = ["standard"] +requires_python = ">=3.8" +summary = "The lightning-fast ASGI server." +groups = ["default"] +dependencies = [ + "colorama>=0.4; sys_platform == \"win32\"", + "httptools>=0.5.0", + "python-dotenv>=0.13", + "pyyaml>=5.1", + "uvicorn==0.29.0", + "uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"", + "watchfiles>=0.13", + "websockets>=10.4", +] +files = [ + {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, + {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, +] + +[[package]] +name = "uvloop" +version = "0.19.0" +requires_python = ">=3.8.0" +summary = "Fast implementation of asyncio event loop on top of libuv" +groups = ["default"] +marker = "(sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"" +files = [ + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, + {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, +] + +[[package]] +name = "virtualenv" +version = "20.26.2" +requires_python = ">=3.7" +summary = "Virtual Python Environment builder" +groups = ["default"] +dependencies = [ + "distlib<1,>=0.3.7", + "filelock<4,>=3.12.2", + "platformdirs<5,>=3.9.1", +] +files = [ + {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, + {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, +] + +[[package]] +name = "watchfiles" +version = "0.21.0" +requires_python = ">=3.8" +summary = "Simple, modern and high performance file watching and code reload in python." +groups = ["default"] +dependencies = [ + "anyio>=3.0.0", +] +files = [ + {file = "watchfiles-0.21.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa"}, + {file = "watchfiles-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c"}, + {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9"}, + {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9"}, + {file = "watchfiles-0.21.0-cp310-none-win32.whl", hash = "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293"}, + {file = "watchfiles-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235"}, + {file = "watchfiles-0.21.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7"}, + {file = "watchfiles-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7"}, + {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0"}, + {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365"}, + {file = "watchfiles-0.21.0-cp311-none-win32.whl", hash = "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400"}, + {file = "watchfiles-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe"}, + {file = "watchfiles-0.21.0-cp311-none-win_arm64.whl", hash = "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078"}, + {file = "watchfiles-0.21.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a"}, + {file = "watchfiles-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c"}, + {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235"}, + {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7"}, + {file = "watchfiles-0.21.0-cp312-none-win32.whl", hash = "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3"}, + {file = "watchfiles-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094"}, + {file = "watchfiles-0.21.0-cp312-none-win_arm64.whl", hash = "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097"}, + {file = "watchfiles-0.21.0.tar.gz", hash = "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3"}, +] + +[[package]] +name = "websockets" +version = "12.0" +requires_python = ">=3.8" +summary = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +groups = ["default"] +files = [ + {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, + {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, + {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, + {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, + {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, + {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, + {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, + {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, + {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, + {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, + {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, + {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, + {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, + {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, + {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, + {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, + {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, + {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, + {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, +] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +requires_python = ">=3.5" +summary = "A small Python utility to set file creation time on Windows" +groups = ["default"] +marker = "sys_platform == \"win32\"" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..aed8807 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,48 @@ +[project] +name = "fastapi_tortoise_mysql" +version = "0.0.1" +description = "fastapi 基础脚手架, fastapi + pydantic-v2 + tortoise-orm + aerich + mysql + redis" +authors = [ + { name = "Wu Clan", email = "jianhengwu0407@gmail.com" }, +] +dependencies = [ + "aerich==0.7.2", + "aiofiles==23.2.1", + "aiosmtplib==3.0.1", + "asgiref==3.8.1", + "bcrypt==4.1.3", + "cryptography==42.0.7", + "email_validator==2.1.1", + "fast-captcha==0.2.1", + "fastapi[all]==0.111.0", + "fastapi-limiter==0.1.6", + "fastapi-pagination==0.12.24", + "gunicorn==22.0.0", + "loguru==0.7.2", + "passlib==1.7.4", + "path==16.14.0", + "pre-commit==3.7.1", + "python-jose==3.3.0", + "python-multipart==0.0.9", + "redis[hiredis]==5.0.4", + "tortoise-orm[asyncmy]==0.21.0", + "uvicorn[standard]==0.29.0", +] +requires-python = ">=3.10" +readme = "README.md" +license = { text = "MIT" } + +[tool.pdm.dev-dependencies] +lint = [ + "ruff>=0.4.2", +] +deploy = [ + "supervisor>=4.2.5", + "wait-for-it>=2.2.2", +] + +[tool.pdm] +distribution = false + +[tool.pdm.scripts] +lint = "pre-commit run --all-files" diff --git a/requirements.txt b/requirements.txt index 6ba08e4..ac72710 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,25 +1,86 @@ +# This file is @generated by PDM. +# Please do not edit it manually. + aerich==0.7.2 aiofiles==23.2.1 -aiosmtplib==2.0.2 -asgiref==3.7.2 -bcrypt==4.0.1 -cryptography==41.0.6 -email-validator==2.0.0 -fast_captcha==0.2.1 -fastapi==0.108.0 +aiosmtplib==3.0.1 +aiosqlite==0.17.0 +annotated-types==0.7.0 +anyio==4.3.0 +asgiref==3.8.1 +async-timeout==4.0.3; python_full_version < "3.11.3" +asyncmy==0.2.9 +bcrypt==4.1.3 +certifi==2024.2.2 +cffi==1.16.0; platform_python_implementation != "PyPy" +cfgv==3.4.0 +click==8.1.7 +colorama==0.4.6; sys_platform == "win32" or platform_system == "Windows" +cryptography==42.0.7 +dictdiffer==0.9.0 +distlib==0.3.8 +dnspython==2.6.1 +ecdsa==0.19.0 +email-validator==2.1.1 +exceptiongroup==1.2.1; python_version < "3.11" +fast-captcha==0.2.1 +fastapi==0.111.0 +fastapi-cli==0.0.4 fastapi-limiter==0.1.6 -fastapi-pagination==0.12.8 -gunicorn==21.2.0 -loguru==0.7.0 +fastapi-pagination==0.12.24 +filelock==3.14.0 +gunicorn==22.0.0 +h11==0.14.0 +hiredis==2.3.2 +httpcore==1.0.5 +httptools==0.6.1 +httpx==0.27.0 +identify==2.5.36 +idna==3.7 +iso8601==1.1.0 +itsdangerous==2.2.0 +jinja2==3.1.4 +loguru==0.7.2 +markdown-it-py==3.0.0 +markupsafe==2.1.5 +mdurl==0.1.2 +nodeenv==1.8.0 +orjson==3.10.3 +packaging==24.0 passlib==1.7.4 -path==16.7.1 -pre-commit==3.2.2 -pydantic==2.2.1 -pydantic-settings==2.0.3 +path==16.14.0 +pillow==9.5.0 +platformdirs==4.2.2 +pre-commit==3.7.1 +pyasn1==0.6.0 +pycparser==2.22; platform_python_implementation != "PyPy" +pydantic==2.7.1 +pydantic-core==2.18.2 +pydantic-extra-types==2.7.0 +pydantic-settings==2.2.1 +pygments==2.18.0 +pypika-tortoise==0.1.6 +python-dotenv==1.0.1 python-jose==3.3.0 -python-multipart==0.0.6 -redis[hiredis]==5.0.1 -supervisor==4.2.5 -tortoise-orm[asyncmy]==0.24.0 -uvicorn[standard]==0.23.2 -wait-for-it==2.2.2 +python-multipart==0.0.9 +pytz==2024.1 +pyyaml==6.0.1 +redis==5.0.4 +rich==13.7.1 +rsa==4.9 +setuptools==70.0.0 +shellingham==1.5.4 +six==1.16.0 +sniffio==1.3.1 +starlette==0.37.2 +tomlkit==0.12.5 +tortoise-orm==0.21.0 +typer==0.12.3 +typing-extensions==4.12.0 +ujson==5.10.0 +uvicorn==0.29.0 +uvloop==0.19.0; (sys_platform != "cygwin" and sys_platform != "win32") and platform_python_implementation != "PyPy" +virtualenv==20.26.2 +watchfiles==0.21.0 +websockets==12.0 +win32-setctime==1.1.0; sys_platform == "win32" From b087664e7bb44464d89b22abfc9888f9055a9844 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 25 May 2024 09:27:49 +0000 Subject: [PATCH 9/9] Bump pillow from 9.5.0 to 10.3.0 Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.5.0 to 10.3.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/9.5.0...10.3.0) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ac72710..9560fb0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -49,7 +49,7 @@ orjson==3.10.3 packaging==24.0 passlib==1.7.4 path==16.14.0 -pillow==9.5.0 +pillow==10.3.0 platformdirs==4.2.2 pre-commit==3.7.1 pyasn1==0.6.0