Compare commits

...
14 Commits
Author SHA1 Message Date
Wu Clan dfc715aad8 Update the version number to 1.12.0 (#962) 2025-12-15 17:07:06 +08:00
Wu Clan 4bc5ba53e6 Add the Grafana observability suite (#961)
* Add the Grafana observability suite

* Update configs

* Update docker script directory structure

* Fix the otel trace id

* Add grafana ini

* Fix some configs and loguru integration

* Add the celery grafana

* Update Grafana dashboards

* Update configs

* Fix issues with the panel

* Update grafana configs

* Update grafana dashboards

* Optimized panel styles

* Add sqlalchemy traces

* Fix the CORS

* Update the grafana query and config

* Update grafana status is off by default
2025-12-15 17:06:43 +08:00
曾果 6b4fd93e5f Fix the IP address in the request log (#959) 2025-12-15 10:07:43 +08:00
Wu Clan 0c5956f828 Fix CLI command for code generation (#956) 2025-12-07 15:14:52 +08:00
Wu Clan 122d17ab57 Update the init project database CLI to subcommand (#954) 2025-12-05 17:23:44 +08:00
Wu Clan 4fb14e480a Add CLI init project database support (#952)
* Add CLI init project database support

* Update get sql script

* Update init CLI contexts
2025-12-05 17:03:01 +08:00
Wu Clan ea4ac6ff41 Optimize the coupling of code generation CLI (#951) 2025-12-05 14:52:58 +08:00
Wu Clan bb5bcdf430 Add the database primary key mode config (#953)
* Add the database primary key mode config

* Update auto to autoincrement
2025-12-05 14:52:12 +08:00
Wu Clan aad9afa9bb Optimize the coupling of user social plugin (#950) 2025-12-04 11:56:37 +08:00
Wu Clan a0e6e12d85 Update pre-commit to prek in pre-commit script (#949) 2025-12-04 11:56:00 +08:00
Wu Clan 5d680ff93f Optimize data permission logic and usage (#947)
* Optimize data permission rules and usage

* Update get data permission models

* Update date permission filter

* Optimize the target model logic

* Upgrade dependencies to use latest features

* Remove model warnings

* Fix the latest feature issues

* Fix the sqlalchemy Table class import

* Fix the sqlalchemy Table class compatibility
2025-12-03 18:44:18 +08:00
Wu Clan 866b0e6ba4 Fix super value in update user permissions (#948) 2025-12-03 18:41:26 +08:00
Wu Clan 5a49d20c9b Update celery related docker container independence (#943)
* Update celery related docker container independence

* Fix lint

* Fix nginx conf
2025-11-28 18:33:32 +08:00
Wu Clan 7641d5993a Update the changelog for v1.11.2 (#942) 2025-11-28 12:31:05 +08:00
55 changed files with 5853 additions and 326 deletions
+31
View File
@@ -1,3 +1,33 @@
<a id="v1.11.2"></a>
# [v1.11.2](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.11.2) - 2025-11-28
## What's Changed
* Update changelog for v1.11.1 by [@wu-clan](https://github.com/wu-clan) in [#923](https://github.com/fastapi-practices/fastapi_best_architecture/pull/923)
* Fix typos in data rule sql scripts by [@wu-clan](https://github.com/wu-clan) in [#926](https://github.com/fastapi-practices/fastapi_best_architecture/pull/926)
* Restore captcha uuid naming in the login params by [@wu-clan](https://github.com/wu-clan) in [#928](https://github.com/fastapi-practices/fastapi_best_architecture/pull/928)
* Add distributed deployment support for snowflake ID by [@downdawn](https://github.com/downdawn) in [#927](https://github.com/fastapi-practices/fastapi_best_architecture/pull/927)
* Add env reqs for plugin install and uninstall by [@wu-clan](https://github.com/wu-clan) in [#929](https://github.com/fastapi-practices/fastapi_best_architecture/pull/929)
* Optimize the use of some LRU caches by [@wu-clan](https://github.com/wu-clan) in [#932](https://github.com/fastapi-practices/fastapi_best_architecture/pull/932)
* Update the i18n language file init location by [@wu-clan](https://github.com/wu-clan) in [#934](https://github.com/fastapi-practices/fastapi_best_architecture/pull/934)
* Fix get column types in code generation by [@wu-clan](https://github.com/wu-clan) in [#935](https://github.com/fastapi-practices/fastapi_best_architecture/pull/935)
* Bump dependencies and pre-commits by [@wu-clan](https://github.com/wu-clan) in [#936](https://github.com/fastapi-practices/fastapi_best_architecture/pull/936)
* Update the files interface filename to file by [@wu-clan](https://github.com/wu-clan) in [#937](https://github.com/fastapi-practices/fastapi_best_architecture/pull/937)
* Update task application interface definitions by [@wu-clan](https://github.com/wu-clan) in [#938](https://github.com/fastapi-practices/fastapi_best_architecture/pull/938)
* Update code generation interface definitions by [@wu-clan](https://github.com/wu-clan) in [#939](https://github.com/fastapi-practices/fastapi_best_architecture/pull/939)
* Update the fba run CLI command output by [@wu-clan](https://github.com/wu-clan) in [#941](https://github.com/fastapi-practices/fastapi_best_architecture/pull/941)
* Update the version number to 1.11.2 by [@wu-clan](https://github.com/wu-clan) in [#940](https://github.com/fastapi-practices/fastapi_best_architecture/pull/940)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.11.1...v1.11.2
## Contributors
<a href="https://github.com/downdawn"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fdowndawn.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@downdawn"></a>
<a href="https://github.com/wu-clan"><img src="https://wsrv.nl/?url=https%3A%2F%2Fgithub.com%2Fwu-clan.png&w=128&h=128&fit=cover&mask=circle" width="64" height="64" alt="@wu-clan"></a>
[Changes][v1.11.2]
<a id="v1.11.1"></a>
# [v1.11.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.11.1) - 2025-11-16
@@ -1161,6 +1191,7 @@
[Changes][v1.0.0]
[v1.11.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.11.1...v1.11.2
[v1.11.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.11.0...v1.11.1
[v1.11.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.10.4...v1.11.0
[v1.10.4]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.10.3...v1.10.4
+24 -8
View File
@@ -36,33 +36,49 @@ COPY --from=builder /fba /fba
COPY --from=builder /usr/local /usr/local
COPY deploy/backend/supervisord.conf /etc/supervisor/supervisord.conf
COPY deploy/backend/supervisor/supervisord.conf /etc/supervisor/supervisord.conf
WORKDIR /fba/backend
# === FastAPI server image ===
FROM base_server AS fba_server
COPY deploy/backend/fba_server.conf /etc/supervisor/conf.d/
COPY deploy/backend/supervisor/fba_server.conf /etc/supervisor/conf.d/
RUN mkdir -p /var/log/fba
EXPOSE 8001
CMD ["/usr/local/bin/granian", "main:app", "--interface", "asgi", "--host", "0.0.0.0", "--port","8000"]
CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]
# === Celery server image ===
FROM base_server AS fba_celery
# === Celery Worker image ===
FROM base_server AS fba_celery_worker
COPY deploy/backend/fba_celery.conf /etc/supervisor/conf.d/
COPY deploy/backend/supervisor/fba_celery_worker.conf /etc/supervisor/conf.d/
RUN mkdir -p /var/log/fba
RUN chmod +x celery-start.sh
CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]
# === Celery Beat image ===
FROM base_server AS fba_celery_beat
COPY deploy/backend/supervisor/fba_celery_beat.conf /etc/supervisor/conf.d/
RUN mkdir -p /var/log/fba
CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]
# === Celery Flower image ===
FROM base_server AS fba_celery_flower
COPY deploy/backend/supervisor/fba_celery_flower.conf /etc/supervisor/conf.d/
RUN mkdir -p /var/log/fba
EXPOSE 8555
CMD ["./celery-start.sh"]
CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]
# Build image
FROM ${SERVER_TYPE}
-2
View File
@@ -74,8 +74,6 @@
- `pre_start.sh`: Perform automatic database migration
- `celery-start.sh`: For celery docker script, implementation is not recommended
- `scripts/format.sh`: Perform ruff format check
- `scripts/lint.sh`: Perform prek formatting
+17 -1
View File
@@ -1 +1,17 @@
__version__ = '1.11.2'
import sqlalchemy as sa
from backend.utils.import_parse import get_all_models
# import all models for auto create db tables
for cls in get_all_models():
if isinstance(cls, sa.Table):
table_name = cls.name
if table_name not in globals():
globals()[table_name] = cls
else:
class_name = cls.__name__
if class_name not in globals():
globals()[class_name] = cls
__version__ = '1.12.0'
-8
View File
@@ -8,17 +8,9 @@ from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from backend.app import get_app_models
from backend.common.model import MappedBase
from backend.core import path_conf
from backend.database.db import SQLALCHEMY_DATABASE_URL
from backend.plugin.tools import get_plugin_models
# import models
for cls in get_app_models() + get_plugin_models():
class_name = cls.__name__
if class_name not in globals():
globals()[class_name] = cls
if not os.path.exists(path_conf.ALEMBIC_VERSION_DIR):
os.makedirs(path_conf.ALEMBIC_VERSION_DIR)
-28
View File
@@ -1,28 +0,0 @@
import os.path
from backend.core.path_conf import BASE_PATH
from backend.utils.import_parse import get_model_objects
def get_app_models() -> list[type]:
"""获取 app 所有模型类"""
app_path = BASE_PATH / 'app'
list_dirs = os.listdir(app_path)
apps = [d for d in list_dirs if os.path.isdir(os.path.join(app_path, d)) and d != '__pycache__']
objs = []
for app in apps:
module_path = f'backend.app.{app}.model'
obj = get_model_objects(module_path)
if obj:
objs.extend(obj)
return objs
# import all app models for auto create db tables
for cls in get_app_models():
class_name = cls.__name__
if class_name not in globals():
globals()[class_name] = cls
+6 -4
View File
@@ -1,12 +1,14 @@
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query, Request
from fastapi import APIRouter, Depends, Path, Query
from sqlalchemy import ColumnElement
from backend.app.admin.model import Dept
from backend.app.admin.schema.dept import CreateDeptParam, GetDeptDetail, GetDeptTree, UpdateDeptParam
from backend.app.admin.service.dept_service import dept_service
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.permission import DataPermissionFilter, RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession, CurrentSessionTransaction
@@ -24,14 +26,14 @@ async def get_dept(
@router.get('', summary='获取部门树', dependencies=[DependsJwtAuth])
async def get_dept_tree(
db: CurrentSession,
request: Request,
data_filter: Annotated[ColumnElement[bool], Depends(DataPermissionFilter(Dept))],
name: Annotated[str | None, Query(description='部门名称')] = None,
leader: Annotated[str | None, Query(description='部门负责人')] = None,
phone: Annotated[str | None, Query(description='联系电话')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[list[GetDeptTree]]:
dept = await dept_service.get_tree(
db=db, request_user=request.user, name=name, leader=leader, phone=phone, status=status
db=db, data_filter=data_filter, name=name, leader=leader, phone=phone, status=status
)
return response_base.success(data=dept)
+3 -5
View File
@@ -1,13 +1,12 @@
from collections.abc import Sequence
from typing import Any
from sqlalchemy import ColumnElement
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus, JoinConfig
from backend.app.admin.model import Dept, User
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
from backend.app.admin.schema.user import GetUserInfoWithRelationDetail
from backend.common.security.permission import filter_data_permission
from backend.utils.serializers import select_join_serialize
@@ -37,7 +36,7 @@ class CRUDDept(CRUDPlus[Dept]):
async def get_all(
self,
db: AsyncSession,
request_user: GetUserInfoWithRelationDetail,
data_filter: ColumnElement[bool],
name: str | None,
leader: str | None,
phone: str | None,
@@ -47,7 +46,7 @@ class CRUDDept(CRUDPlus[Dept]):
获取所有部门
:param db: 数据库会话
:param request_user: 请求用户
:param data_filter: 请求用户
:param name: 部门名称
:param leader: 负责人
:param phone: 联系电话
@@ -65,7 +64,6 @@ class CRUDDept(CRUDPlus[Dept]):
if status is not None:
filters['status'] = status
data_filter = filter_data_permission(request_user)
return await self.select_models_order(db, 'sort', 'desc', data_filter, **filters)
async def create(self, db: AsyncSession, obj: CreateDeptParam) -> None:
+8 -2
View File
@@ -25,7 +25,7 @@ from backend.app.admin.schema.user import (
UpdateUserParam,
)
from backend.app.admin.utils.password_security import get_hash_password
from backend.plugin.oauth2.crud.crud_user_social import user_social_dao
from backend.utils.import_parse import import_module_cached
from backend.utils.serializers import select_join_serialize
from backend.utils.timezone import timezone
@@ -299,7 +299,13 @@ class CRUDUser(CRUDPlus[User]):
user_role_stmt = delete(user_role).where(user_role.c.user_id == user_id)
await db.execute(user_role_stmt)
await user_social_dao.delete_by_user_id(db, user_id)
try:
user_social = import_module_cached('backend.plugin.oauth2.crud.crud_user_social')
user_social_dao = user_social.user_social_dao
except (ImportError, AttributeError):
pass
else:
await user_social_dao.delete_by_user_id(db, user_id)
return await self.delete_model(db, user_id)
+1 -1
View File
@@ -45,4 +45,4 @@ class GetDataRuleColumnDetail(SchemaBase):
"""数据规则可用模型字段详情"""
key: str = Field(description='字段名')
comment: str = Field(description='字段评论')
comment: str | None = Field(description='字段评论')
@@ -1,6 +1,7 @@
from collections.abc import Sequence
from typing import Any
from sqlalchemy import Table
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.admin.crud.crud_data_rule import data_rule_dao
@@ -14,8 +15,8 @@ from backend.app.admin.schema.data_rule import (
from backend.app.admin.utils.cache import user_cache_manager
from backend.common.exception import errors
from backend.common.pagination import paging_data
from backend.common.security.permission import get_data_permission_models
from backend.core.conf import settings
from backend.utils.import_parse import dynamic_import_data_model
class DataRuleService:
@@ -39,7 +40,8 @@ class DataRuleService:
@staticmethod
async def get_models() -> list[str]:
"""获取所有数据规则可用模型"""
return list(settings.DATA_PERMISSION_MODELS.keys())
model_exclude = ['DataScope', 'DataRule', 'sys_role_data_scope', 'sys_data_scope_rule']
return [m for m in list(get_data_permission_models().keys()) if m not in model_exclude]
@staticmethod
async def get_columns(model: str) -> list[GetDataRuleColumnDetail]:
@@ -49,13 +51,15 @@ class DataRuleService:
:param model: 模型名称
:return:
"""
if model not in settings.DATA_PERMISSION_MODELS:
available_models = get_data_permission_models()
if model not in available_models:
raise errors.NotFoundError(msg='数据规则可用模型不存在')
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[model])
model_ins = available_models[model]
table = model_ins if isinstance(model_ins, Table) else model_ins.__table__
model_columns = [
GetDataRuleColumnDetail(key=column.key, comment=column.comment)
for column in model_ins.__table__.columns
for column in table.columns
if column.key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE
]
return model_columns
+4 -5
View File
@@ -1,11 +1,11 @@
from typing import Any
from sqlalchemy import ColumnElement
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.admin.crud.crud_dept import dept_dao
from backend.app.admin.model import Dept
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
from backend.app.admin.schema.user import GetUserInfoWithRelationDetail
from backend.common.exception import errors
from backend.core.conf import settings
from backend.database.redis import redis_client
@@ -34,7 +34,7 @@ class DeptService:
async def get_tree(
*,
db: AsyncSession,
request_user: GetUserInfoWithRelationDetail,
data_filter: ColumnElement[bool],
name: str | None,
leader: str | None,
phone: str | None,
@@ -44,15 +44,14 @@ class DeptService:
获取部门树形结构
:param db: 数据库会话
:param request_user: 请求用户
:param data_filter: 请求用户
:param name: 部门名称
:param leader: 部门负责人
:param phone: 联系电话
:param status: 状态
:return:
"""
dept_select = await dept_dao.get_all(db, request_user, name, leader, phone, status)
dept_select = await dept_dao.get_all(db, data_filter, name, leader, phone, status)
tree_data = get_tree_data(dept_select)
return tree_data
+1 -1
View File
@@ -144,7 +144,7 @@ class UserService:
raise errors.NotFoundError(msg='用户不存在')
if pk == request.user.id:
raise errors.ForbiddenError(msg='禁止修改自身权限')
count = await user_dao.set_super(db, pk, is_super=not user.status)
count = await user_dao.set_super(db, pk, is_super=not user.is_superuser)
case UserPermissionType.staff:
user = await user_dao.get(db, pk)
if not user:
+1 -2
View File
@@ -16,8 +16,7 @@
如果你想对任务进行目录层级划分,使任务结构更加清晰,你可以新建任意目录,但必须注意的是
1. 在 `backend/app/task/tasks` 目录下新建 python 包目录
2. 新建目录,务必更新 `conf.py` 配置中的 `CELERY_TASKS_PACKAGES`,将新建目录模块路径添加到此列表
3. 在新建目录下,务必添加 `tasks.py` 文件,并在此文件中编写相关任务代码
2. 新建目录,务必添加 `tasks.py` 文件,并在此文件中编写相关任务代码
## 消息代理
+4 -1
View File
@@ -4,6 +4,7 @@ import celery
import celery_aio_pool
from backend.app.task.tasks.beat import LOCAL_BEAT_SCHEDULE
from backend.common.enums import DataBaseType
from backend.core.conf import settings
from backend.core.path_conf import BASE_PATH
@@ -32,7 +33,7 @@ def init_celery() -> celery.Celery:
broker_url = f'redis://:{settings.REDIS_PASSWORD}@{settings.REDIS_HOST}:{settings.REDIS_PORT}/{settings.CELERY_BROKER_REDIS_DATABASE}'
result_backend = f'db+postgresql+psycopg://{settings.DATABASE_USER}:{settings.DATABASE_PASSWORD}@{settings.DATABASE_HOST}:{settings.DATABASE_PORT}/{settings.DATABASE_SCHEMA}'
if settings.DATABASE_TYPE == 'mysql':
if DataBaseType.mysql == settings.DATABASE_TYPE:
result_backend = result_backend.replace('postgresql+psycopg', 'mysql+pymysql')
# https://docs.celeryq.dev/en/stable/userguide/configuration.html
@@ -51,6 +52,8 @@ def init_celery() -> celery.Celery:
task_track_started=True,
enable_utc=False,
timezone=settings.DATETIME_TIMEZONE,
worker_send_task_events=True,
task_send_sent_event=True,
)
# 在 Celery 中设置此参数无效
-10
View File
@@ -1,10 +0,0 @@
#!/usr/bin/env bash
# work && beat
celery -A backend.app.task.celery worker -l info -P gevent -c 100 &
# beat
celery -A backend.app.task.celery beat -l info &
# flower
celery -A backend.app.task.celery flower --port=8555 --basic-auth=admin:123456
+133 -20
View File
@@ -5,12 +5,13 @@ import sys
from dataclasses import dataclass
from typing import Annotated, Literal
import anyio
import cappa
import granian
from cappa.output import error_format
from rich.panel import Panel
from rich.prompt import IntPrompt
from rich.prompt import IntPrompt, Prompt
from rich.table import Table
from rich.text import Text
from sqlalchemy import text
@@ -20,14 +21,13 @@ from backend import __version__
from backend.common.enums import DataBaseType, PrimaryKeyType
from backend.common.exception.errors import BaseExceptionError
from backend.core.conf import settings
from backend.database.db import async_db_session
from backend.plugin.code_generator.schema.code import ImportParam
from backend.plugin.code_generator.service.business_service import gen_business_service
from backend.plugin.code_generator.service.code_service import gen_service
from backend.core.path_conf import BASE_PATH
from backend.database.db import async_db_session, create_tables, drop_tables
from backend.database.redis import redis_client
from backend.plugin.tools import get_plugin_sql, get_plugins
from backend.utils._await import run_await
from backend.utils.console import console
from backend.utils.file_ops import install_git_plugin, install_zip_plugin, parse_sql_script
from backend.utils.import_parse import import_module_cached
output_help = '\n更多信息,尝试 "[cyan]--help[/]"'
@@ -39,6 +39,63 @@ class CustomReloadFilter(PythonFilter):
super().__init__(extra_extensions=['.json', '.yaml', '.yml'])
async def init() -> None:
panel_content = Text()
panel_content.append('【数据库配置】', style='bold green')
panel_content.append('\n\n • 类型: ')
panel_content.append(f'{settings.DATABASE_TYPE}', style='yellow')
panel_content.append('\n • 数据库:')
panel_content.append(f'{settings.DATABASE_SCHEMA}', style='yellow')
panel_content.append('\n • 主键模式:')
panel_content.append(
f'{settings.DATABASE_PK_MODE}',
style='yellow',
)
pk_details = panel_content.from_markup(
'[link=https://fastapi-practices.github.io/fastapi_best_architecture_docs/backend/reference/pk.html](了解详情)[/]'
)
panel_content.append(pk_details)
panel_content.append('\n\n【Redis 配置】', style='bold green')
panel_content.append('\n\n • 数据库:')
panel_content.append(f'{settings.REDIS_DATABASE}', style='yellow')
plugins = get_plugins()
panel_content.append('\n\n【已安装插件】', style='bold green')
panel_content.append('\n\n')
if plugins:
panel_content.append(f'{", ".join(plugins)}', style='yellow')
else:
panel_content.append('', style='dim')
console.print(Panel(panel_content, title=f'fba v{__version__} 初始化', border_style='cyan', padding=(1, 2)))
ok = Prompt.ask(
'即将[red]重建数据库表[/red]并[red]执行所有 SQL 脚本[/red],确认继续吗?', choices=['y', 'n'], default='n'
)
if ok.lower() == 'y':
console.print('开始初始化...', style='white')
try:
console.print('丢弃数据库表', style='white')
await drop_tables()
console.print('丢弃 Redis 缓存', style='white')
await redis_client.delete_prefix(settings.JWT_USER_REDIS_PREFIX)
await redis_client.delete_prefix(settings.TOKEN_EXTRA_INFO_REDIS_PREFIX)
await redis_client.delete_prefix(settings.TOKEN_REDIS_PREFIX)
await redis_client.delete_prefix(settings.TOKEN_REFRESH_REDIS_PREFIX)
console.print('创建数据库表', style='white')
await create_tables()
console.print('执行 SQL 脚本', style='white')
sql_scripts = await get_sql_scripts()
for sql_script in sql_scripts:
console.print(f'正在执行:{sql_script}', style='white')
await execute_sql_scripts(sql_script, is_init=True)
console.print('初始化成功', style='green')
console.print('\n快试试 [bold cyan]fba run[/bold cyan] 启动服务吧~')
except Exception as e:
raise cappa.Exit(f'初始化失败:{e}', code=1)
else:
console.print('已取消初始化', style='yellow')
def run(host: str, port: int, reload: bool, workers: int) -> None: # noqa: FBT001
url = f'http://{host}:{port}'
docs_url = url + settings.FASTAPI_DOCS_URL
@@ -124,7 +181,7 @@ async def install_plugin(
raise cappa.Exit('path 和 repo_url 不能同时指定', code=1)
plugin_name = None
console.print(Text('开始安装插件...', style='bold cyan'))
console.print('开始安装插件...', style='bold cyan')
try:
if path:
@@ -132,18 +189,44 @@ async def install_plugin(
if repo_url:
plugin_name = await install_git_plugin(repo_url=repo_url)
console.print(Text(f'插件 {plugin_name} 安装成功', style='bold green'))
console.print(f'插件 {plugin_name} 安装成功', style='bold green')
sql_file = await get_plugin_sql(plugin_name, db_type, pk_type)
if sql_file and not no_sql:
console.print(Text('开始自动执行插件 SQL 脚本...', style='bold cyan'))
console.print('开始自动执行插件 SQL 脚本...', style='bold cyan')
await execute_sql_scripts(sql_file)
except Exception as e:
raise cappa.Exit(e.msg if isinstance(e, BaseExceptionError) else str(e), code=1)
async def execute_sql_scripts(sql_scripts: str) -> None:
async def get_sql_scripts() -> list[str]:
sql_scripts = []
db_dir = (
BASE_PATH / 'sql' / 'mysql'
if DataBaseType.mysql == settings.DATABASE_TYPE
else BASE_PATH / 'sql' / 'postgresql'
)
main_sql_file = (
db_dir / 'init_test_data.sql'
if PrimaryKeyType.autoincrement == settings.DATABASE_PK_MODE
else db_dir / 'init_snowflake_test_data.sql'
)
main_sql_path = anyio.Path(main_sql_file)
if await main_sql_path.exists():
sql_scripts.append(str(main_sql_file))
plugins = get_plugins()
for plugin in plugins:
plugin_sql = await get_plugin_sql(plugin, settings.DATABASE_TYPE, settings.DATABASE_PK_MODE)
if plugin_sql:
sql_scripts.append(str(plugin_sql))
return sql_scripts
async def execute_sql_scripts(sql_scripts: str, *, is_init: bool = False) -> None:
async with async_db_session.begin() as db:
try:
stmts = await parse_sql_script(sql_scripts)
@@ -152,7 +235,8 @@ async def execute_sql_scripts(sql_scripts: str) -> None:
except Exception as e:
raise cappa.Exit(f'SQL 脚本执行失败:{e}', code=1)
console.print(Text('SQL 脚本已执行完成', style='bold green'))
if not is_init:
console.print('SQL 脚本已执行完成', style='bold green')
async def import_table(
@@ -160,18 +244,27 @@ async def import_table(
table_schema: str,
table_name: str,
) -> None:
from backend.plugin.code_generator.schema.code import ImportParam
from backend.plugin.code_generator.service.code_service import gen_service
try:
obj = ImportParam(app=app, table_schema=table_schema, table_name=table_name)
async with async_db_session.begin() as db:
await gen_service.import_business_and_model(db=db, obj=obj)
console.log('代码生成业务和模型列导入成功', style='bold green')
console.log('\n快试试 [bold cyan]fba codegen[/bold cyan] 生成代码吧~')
except Exception as e:
raise cappa.Exit(e.msg if isinstance(e, BaseExceptionError) else str(e), code=1)
def generate() -> None:
async def generate() -> None:
from backend.plugin.code_generator.service.business_service import gen_business_service
from backend.plugin.code_generator.service.code_service import gen_service
try:
ids = []
results = run_await(gen_business_service.get_all)()
async with async_db_session() as db:
results = await gen_business_service.get_all(db=db)
if not results:
raise cappa.Exit('[red]暂无可用的代码生成业务!请先通过 import 命令导入![/]')
@@ -194,12 +287,20 @@ def generate() -> None:
console.print(table)
business = IntPrompt.ask('请从中选择一个业务编号', choices=[str(_id) for _id in ids])
gen_path = run_await(gen_service.generate)(pk=business)
async with async_db_session.begin() as db:
gen_path = await gen_service.generate(db=db, pk=business)
except Exception as e:
raise cappa.Exit(e.msg if isinstance(e, BaseExceptionError) else str(e), code=1)
console.print(Text('\n代码已生成完', style='bold green'))
console.print(Text('\n详情请查看:'), Text(gen_path, style='bold magenta'))
console.print('\n代码已生成完', style='bold green')
console.print(Text('\n详情请查看:'), Text(str(gen_path), style='bold magenta'))
@cappa.command(help='初始化 fba 项目', default_long=True)
@dataclass
class Init:
async def __call__(self) -> None:
await init()
@cappa.command(help='运行 API 服务', default_long=True)
@@ -320,17 +421,29 @@ class Import:
cappa.Arg(short='tn', help='数据库表名'),
]
def __post_init__(self) -> None:
try:
import_module_cached('backend.plugin.code_generator')
except ImportError:
raise cappa.Exit('代码生成插件不存在,请先安装此插件')
async def __call__(self) -> None:
await import_table(self.app, self.table_schema, self.table_name)
@cappa.command(name='codegen', help='代码生成(体验完整功能,请自行部署 fba vben 前端工程)', default_long=True)
@dataclass
class CodeGenerate:
class CodeGenerator:
subcmd: cappa.Subcommands[Import | None] = None
def __call__(self) -> None:
generate()
def __post_init__(self) -> None:
try:
import_module_cached('backend.plugin.code_generator')
except ImportError:
raise cappa.Exit('代码生成插件不存在,请先安装此插件')
async def __call__(self) -> None:
await generate()
@cappa.command(help='一个高效的 fba 命令行界面', default_long=True)
@@ -340,7 +453,7 @@ class FbaCli:
str,
cappa.Arg(value_name='PATH', default='', show_default=False, help='在事务中执行 SQL 脚本'),
]
subcmd: cappa.Subcommands[Run | Celery | Add | CodeGenerate | None] = None
subcmd: cappa.Subcommands[Init | Run | Celery | Add | CodeGenerator | None] = None
async def __call__(self) -> None:
if self.sql:
+54 -1
View File
@@ -2,6 +2,7 @@ from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from pydantic import ValidationError
from starlette.exceptions import HTTPException
from starlette.middleware.cors import CORSMiddleware
from uvicorn.protocols.http.h11_impl import STATUS_PHRASES
from backend.common.context import ctx
@@ -75,7 +76,7 @@ async def _validation_exception_handler(exc: RequestValidationError | Validation
return MsgSpecJSONResponse(status_code=StandardResponseCode.HTTP_422, content=content)
def register_exception(app: FastAPI) -> None:
def register_exception(app: FastAPI) -> None: # noqa: C901
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
"""
@@ -194,3 +195,55 @@ def register_exception(app: FastAPI) -> None:
status_code=StandardResponseCode.HTTP_500,
content=content,
)
if settings.MIDDLEWARE_CORS:
@app.exception_handler(StandardResponseCode.HTTP_500)
async def cors_custom_code_500_exception_handler(request: Request, exc: BaseExceptionError | Exception):
"""
跨域自定义 500 异常处理
:param request: FastAPI 请求对象
:param exc: 自定义异常
:return:
"""
if isinstance(exc, BaseExceptionError):
content = {
'code': exc.code,
'msg': exc.msg,
'data': exc.data,
}
else:
if settings.ENVIRONMENT == 'dev':
content = {
'code': StandardResponseCode.HTTP_500,
'msg': str(exc),
'data': None,
}
else:
res = response_base.fail(res=CustomResponseCode.HTTP_500)
content = res.model_dump()
content.update(trace_id=get_request_trace_id())
response = MsgSpecJSONResponse(
status_code=exc.code if isinstance(exc, BaseExceptionError) else StandardResponseCode.HTTP_500,
content=content,
background=exc.background if isinstance(exc, BaseExceptionError) else None,
)
origin = request.headers.get('origin')
if origin:
cors = CORSMiddleware(
app=app,
allow_origins=settings.CORS_ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
expose_headers=settings.CORS_EXPOSE_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
+7 -6
View File
@@ -47,6 +47,13 @@ def default_formatter(record: logging.LogRecord) -> str:
return settings.LOG_FORMAT if settings.LOG_FORMAT.endswith('\n') else f'{settings.LOG_FORMAT}\n'
def request_id_filter(record: logging.LogRecord) -> logging.LogRecord:
"""请求 ID 过滤器"""
rid = get_request_trace_id()
record['request_id'] = rid[: settings.TRACE_ID_LOG_LENGTH]
return record
def setup_logging() -> None:
"""
设置日志处理器
@@ -75,12 +82,6 @@ def setup_logging() -> None:
# 移除 loguru 默认处理器
logger.remove()
# request_id 过滤器
def request_id_filter(record: logging.LogRecord) -> logging.LogRecord:
rid = get_request_trace_id()
record['request_id'] = rid[: settings.TRACE_ID_LOG_LENGTH]
return record
# 配置 loguru 处理器
logger.configure(
handlers=[
+7 -10
View File
@@ -6,6 +6,7 @@ from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, declared_attr, mapped_column
from backend.common.enums import DataBaseType, PrimaryKeyType
from backend.core.conf import settings
from backend.utils.snowflake import snowflake
from backend.utils.timezone import timezone
@@ -23,15 +24,11 @@ id_key = Annotated[
autoincrement=True,
sort_order=-999,
comment='主键 ID',
),
]
# 雪花算法 Mapped 类型主键,使用方法与 id_key 相同
# 详情:https://fastapi-practices.github.io/fastapi_best_architecture_docs/backend/reference/pk.html
snowflake_id_key = Annotated[
int,
mapped_column(
)
if PrimaryKeyType.autoincrement == settings.DATABASE_PK_MODE
# 雪花算法 Mapped 类型主键
# 详情:https://fastapi-practices.github.io/fastapi_best_architecture_docs/backend/reference/pk.html
else mapped_column(
BigInteger,
primary_key=True,
unique=True,
@@ -46,7 +43,7 @@ snowflake_id_key = Annotated[
class UniversalText(TypeDecorator[str]):
"""PostgreSQL、MySQL 兼容性(长)文本类型"""
impl = LONGTEXT if settings.DATABASE_TYPE == 'mysql' else Text
impl = LONGTEXT if DataBaseType.mysql == settings.DATABASE_TYPE else Text
cache_ok = True
def process_bind_param(self, value: str | None, dialect) -> str | None: # noqa: ANN001
+35
View File
@@ -0,0 +1,35 @@
from prometheus_client import Counter, Gauge, Histogram
from backend.core.conf import settings
PROMETHEUS_INFO_GAUGE = (
Gauge(name='fba_app_info', documentation='fba 应用信息', labelnames=['app_name'])
.labels(app_name=settings.GRAFANA_APP_NAME)
.inc()
)
PROMETHEUS_REQUEST_IN_PROGRESS_GAUGE = Gauge(
'fba_request_in_progress',
'按方法和路径统计请求的衡量',
['app_name', 'method', 'path'],
)
PROMETHEUS_REQUEST_COUNTER = Counter('fba_request_total', '按方法和路径统计请求总数', ['app_name', 'method', 'path'])
PROMETHEUS_RESPONSE_COUNTER = Counter(
'fba_response_total',
'按方法、路径和状态码统计响应总数',
['app_name', 'method', 'path', 'status_code'],
)
PROMETHEUS_EXCEPTION_COUNTER = Counter(
'fba_exception_total',
'按方法,路径和异常类型统计异常总数',
['app_name', 'method', 'path', 'exception_type'],
)
PROMETHEUS_REQUEST_COST_TIME_HISTOGRAM = Histogram(
'fba_request_cost_time',
'按方法和路径划分请求耗时的直方图(以 ms 为单位)',
['app_name', 'method', 'path'],
)
+10
View File
@@ -3,6 +3,8 @@ from typing import Annotated, Any
from pydantic import BaseModel, ConfigDict, EmailStr, Field, validate_email
from backend.common.enums import PrimaryKeyType
from backend.core.conf import settings
from backend.utils.timezone import timezone
CustomPhoneNumber = Annotated[str, Field(pattern=r'^1[3-9]\d{9}$')]
@@ -28,6 +30,14 @@ class SchemaBase(BaseModel):
},
)
if PrimaryKeyType.snowflake == settings.DATABASE_PK_MODE:
from pydantic import field_serializer
# 详情:https://fastapi-practices.github.io/fastapi_best_architecture_docs/backend/reference/pk.html#%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9
@field_serializer('id', check_fields=False)
def serialize_id(self, value: int) -> str:
return str(value)
def ser_string(value: Any) -> str | None:
if value:
+79 -35
View File
@@ -1,12 +1,15 @@
from typing import Any
from fastapi import Request
from sqlalchemy import ColumnElement, and_, or_
from sqlalchemy import Alias, ColumnElement, Table, and_, or_
from sqlalchemy.orm.util import AliasedClass
from sqlalchemy_crud_plus.types import Model
from backend.app.admin.schema.user import GetUserInfoWithRelationDetail
from backend.common.context import ctx
from backend.common.enums import RoleDataRuleExpressionType, RoleDataRuleOperatorType
from backend.common.exception import errors
from backend.core.conf import settings
from backend.utils.import_parse import dynamic_import_data_model
from backend.utils.import_parse import get_all_models
class RequestPermission:
@@ -41,75 +44,95 @@ class RequestPermission:
ctx.permission = self.value
def filter_data_permission(request_user: GetUserInfoWithRelationDetail) -> ColumnElement[bool]: # noqa: C901
def get_data_permission_models() -> dict[str, object]:
"""获取所有可用于数据权限的模型"""
return {getattr(model, '__name__', str(model)): model for model in get_all_models()}
def filter_data_permission( # noqa: C901
request: Request, *models: type[Model] | AliasedClass | Alias | Table
) -> ColumnElement[bool]:
"""
过滤数据权限控制用户可见数据范围
使用场景
- 控制用户能看到哪些数据
:param request_user: 请求用户
:param request: FastAPI 请求对象
:param models: 需要应用数据权限的模型类
:return:
"""
# 是否过滤数据权限
if request_user.is_superuser:
# 超级管理员不过滤
if request.user.is_superuser:
return or_(1 == 1)
for role in request_user.roles:
# 角色未启用数据权限过滤
for role in request.user.roles:
if not role.is_filter_scopes:
return or_(1 == 1)
# 获取数据规则
data_rules = set()
for role in request_user.roles:
for role in request.user.roles:
for scope in role.scopes:
if scope.status:
data_rules.update(scope.rules)
# 无规则用户不做过滤
if not list(data_rules):
if not data_rules:
return or_(1 == 1)
# 获取目标模型
model_map = (
{getattr(model, '__name__', str(model)): model for model in models} if models else get_data_permission_models()
)
where_and_list = []
where_or_list = []
for data_rule in list(data_rules):
# 验证规则模型
rule_model = data_rule.model
if rule_model not in settings.DATA_PERMISSION_MODELS:
raise errors.NotFoundError(msg='数据规则可用模型不存在')
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[rule_model])
for data_rule in data_rules:
target_model = model_map.get(data_rule.model)
if target_model is None:
continue
# 验证规则列
model_columns = [
key for key in model_ins.__table__.columns.keys() if key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE
]
column = data_rule.column
if column not in model_columns:
raise errors.NotFoundError(msg='数据规则可用模型列不存在')
table = target_model if isinstance(target_model, Table) else target_model.__table__
rule_column = data_rule.column
if rule_column not in table.columns.keys():
continue
if rule_column in settings.DATA_PERMISSION_COLUMN_EXCLUDE:
continue
# 构建过滤条件
column_obj = getattr(model_ins, column)
rule_expression = data_rule.expression
column_obj = (
getattr(target_model, rule_column) if not isinstance(target_model, Table) else table.columns[rule_column]
)
column_type = table.columns[rule_column].type.python_type
def cast_value(value: Any) -> Any:
"""类型转换"""
try:
return column_type(value) if column_type is not str else value
except (ValueError, TypeError):
return value
condition = None
match rule_expression:
match data_rule.expression:
case RoleDataRuleExpressionType.eq:
condition = column_obj == data_rule.value
condition = column_obj == cast_value(data_rule.value)
case RoleDataRuleExpressionType.ne:
condition = column_obj != data_rule.value
condition = column_obj != cast_value(data_rule.value)
case RoleDataRuleExpressionType.gt:
condition = column_obj > data_rule.value
condition = column_obj > cast_value(data_rule.value)
case RoleDataRuleExpressionType.ge:
condition = column_obj >= data_rule.value
condition = column_obj >= cast_value(data_rule.value)
case RoleDataRuleExpressionType.lt:
condition = column_obj < data_rule.value
condition = column_obj < cast_value(data_rule.value)
case RoleDataRuleExpressionType.le:
condition = column_obj <= data_rule.value
condition = column_obj <= cast_value(data_rule.value)
case RoleDataRuleExpressionType.in_:
values = data_rule.value.split(',') if isinstance(data_rule.value, str) else data_rule.value
values = [cast_value(v.strip()) for v in data_rule.value.split(',')]
condition = column_obj.in_(values)
case RoleDataRuleExpressionType.not_in:
values = data_rule.value.split(',') if isinstance(data_rule.value, str) else data_rule.value
values = [cast_value(v.strip()) for v in data_rule.value.split(',')]
condition = column_obj.not_in(values)
# 根据运算符添加到对应列表
@@ -128,3 +151,24 @@ def filter_data_permission(request_user: GetUserInfoWithRelationDetail) -> Colum
where_list.append(or_(*where_or_list))
return or_(*where_list) if where_list else or_(1 == 1)
# 此函数是为了简化调用方式,但目前无法正常工作: https://github.com/fastapi/fastapi/discussions/14438
# def DataPermissionFilter(*models: type[Model] | AliasedClass | Alias | Table) -> type[ColumnElement[bool]]:
# """
# 指定模型的数据权限过滤器
#
# :param models: 模型类(可选,支持多个)
# :return:
# """
# return Annotated[ColumnElement[bool], Depends(partial(filter_data_permission, *models))]
class DataPermissionFilter:
"""指定模型的数据权限过滤器"""
def __init__(self, *models: type[Model] | AliasedClass | Alias | Table) -> None:
self.models = models
async def __call__(self, request: Request) -> ColumnElement[bool]:
return filter_data_permission(request, *self.models)
+7 -4
View File
@@ -23,7 +23,7 @@ class Settings(BaseSettings):
# FastAPI
FASTAPI_API_V1_PATH: str = '/api/v1'
FASTAPI_TITLE: str = 'FastAPI'
FASTAPI_TITLE: str = 'fba'
FASTAPI_DESCRIPTION: str = 'FastAPI Best Architecture'
FASTAPI_DOCS_URL: str = '/docs'
FASTAPI_REDOC_URL: str = '/redoc'
@@ -42,6 +42,7 @@ class Settings(BaseSettings):
DATABASE_POOL_ECHO: bool | Literal['debug'] = False
DATABASE_SCHEMA: str = 'fba'
DATABASE_CHARSET: str = 'utf8mb4'
DATABASE_PK_MODE: Literal['autoincrement', 'snowflake'] = 'autoincrement'
# .env Redis
REDIS_HOST: str
@@ -111,9 +112,6 @@ class Settings(BaseSettings):
COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 # 7 天
# 数据权限
DATA_PERMISSION_MODELS: dict[str, str] = { # 允许进行数据过滤的 SQLA 模型,它必须以模块字符串的方式定义
'Dept': 'backend.app.admin.model.Dept',
}
DATA_PERMISSION_COLUMN_EXCLUDE: list[str] = [ # 排除允许进行数据过滤的 SQLA 模型列
'id',
'sort',
@@ -217,6 +215,11 @@ class Settings(BaseSettings):
# I18n 配置
I18N_DEFAULT_LANGUAGE: str = 'zh-CN'
# Grafana
GRAFANA_METRICS: bool = False
GRAFANA_APP_NAME: str = 'fba_server'
GRAFANA_OTLP_GRPC_ENDPOINT: str = 'fba_alloy:4317'
##################################################
# [ App ] task
##################################################
+35 -18
View File
@@ -9,10 +9,10 @@ import socketio
from fastapi import Depends, FastAPI
from fastapi_limiter import FastAPILimiter
from fastapi_pagination import add_pagination
from prometheus_client import make_asgi_app
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.middleware.cors import CORSMiddleware
from starlette.staticfiles import StaticFiles
from starlette.types import ASGIApp
from starlette_context.middleware import ContextMiddleware
from starlette_context.plugins import RequestIdPlugin
@@ -33,8 +33,10 @@ from backend.plugin.tools import build_final_router
from backend.utils.demo_site import demo_site
from backend.utils.health_check import ensure_unique_route_names, http_limit_callback
from backend.utils.openapi import simplify_operation_ids
from backend.utils.otel import init_otel
from backend.utils.serializers import MsgSpecJSONResponse
from backend.utils.snowflake import snowflake
from backend.utils.trace_id import OtelTraceIdPlugin
@asynccontextmanager
@@ -76,22 +78,7 @@ async def register_init(app: FastAPI) -> AsyncGenerator[None, None]:
def register_app() -> FastAPI:
"""注册 FastAPI 应用"""
class MyFastAPI(FastAPI):
if settings.MIDDLEWARE_CORS:
# Related issues
# https://github.com/fastapi/fastapi/discussions/7847
# https://github.com/fastapi/fastapi/discussions/8027
def build_middleware_stack(self) -> ASGIApp:
return CORSMiddleware(
super().build_middleware_stack(),
allow_origins=settings.CORS_ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
expose_headers=settings.CORS_EXPOSE_HEADERS,
)
app = MyFastAPI(
app = FastAPI(
title=settings.FASTAPI_TITLE,
version=__version__,
description=settings.FASTAPI_DESCRIPTION,
@@ -111,6 +98,9 @@ def register_app() -> FastAPI:
register_page(app)
register_exception(app)
if settings.GRAFANA_METRICS:
register_metrics(app)
return app
@@ -164,15 +154,29 @@ def register_middleware(app: FastAPI) -> None:
app.add_middleware(AccessMiddleware)
# ContextVar
plugins = [OtelTraceIdPlugin()] if settings.GRAFANA_METRICS else [RequestIdPlugin(validate=True)]
app.add_middleware(
ContextMiddleware,
plugins=[RequestIdPlugin(validate=True)],
plugins=plugins,
default_error_response=MsgSpecJSONResponse(
content={'code': StandardResponseCode.HTTP_400, 'msg': 'BAD_REQUEST', 'data': None},
status_code=StandardResponseCode.HTTP_400,
),
)
# CORS
# https://github.com/fastapi-practices/fastapi_best_architecture/pull/789/changes
# https://github.com/open-telemetry/opentelemetry-python-contrib/issues/4031
if settings.MIDDLEWARE_CORS:
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
expose_headers=settings.CORS_EXPOSE_HEADERS,
)
def register_router(app: FastAPI) -> None:
"""
@@ -218,3 +222,16 @@ def register_socket_app(app: FastAPI) -> None:
socketio_path='/ws/socket.io',
)
app.mount('/ws', socket_app)
def register_metrics(app: FastAPI) -> None:
"""
注册指标
:param app: FastAPI 应用实例
:return:
"""
metrics_app = make_asgi_app()
app.mount('/metrics', metrics_app)
init_otel(app)
+9 -2
View File
@@ -13,6 +13,7 @@ from sqlalchemy.ext.asyncio import (
create_async_engine,
)
from backend.common.enums import DataBaseType
from backend.common.log import log
from backend.common.model import MappedBase
from backend.core.conf import settings
@@ -26,14 +27,14 @@ def create_database_url(*, unittest: bool = False) -> URL:
:return:
"""
url = URL.create(
drivername='mysql+asyncmy' if settings.DATABASE_TYPE == 'mysql' else 'postgresql+asyncpg',
drivername='mysql+asyncmy' if DataBaseType.mysql == settings.DATABASE_TYPE else 'postgresql+asyncpg',
username=settings.DATABASE_USER,
password=settings.DATABASE_PASSWORD,
host=settings.DATABASE_HOST,
port=settings.DATABASE_PORT,
database=settings.DATABASE_SCHEMA if not unittest else f'{settings.DATABASE_SCHEMA}_test',
)
if settings.DATABASE_TYPE == 'mysql':
if DataBaseType.mysql == settings.DATABASE_TYPE:
url.update_query_dict({'charset': settings.DATABASE_CHARSET})
return url
@@ -91,6 +92,12 @@ async def create_tables() -> None:
await coon.run_sync(MappedBase.metadata.create_all)
async def drop_tables() -> None:
"""丢弃数据库表"""
async with async_engine.begin() as conn:
await conn.run_sync(MappedBase.metadata.drop_all)
def uuid4_str() -> str:
"""数据库引擎 UUID 类型兼容性解决方案"""
return str(uuid4())
+32 -1
View File
@@ -14,6 +14,13 @@ from backend.app.admin.service.opera_log_service import opera_log_service
from backend.common.context import ctx
from backend.common.enums import OperaLogCipherType, StatusType
from backend.common.log import log
from backend.common.prometheus.instruments import (
PROMETHEUS_EXCEPTION_COUNTER,
PROMETHEUS_REQUEST_COST_TIME_HISTOGRAM,
PROMETHEUS_REQUEST_COUNTER,
PROMETHEUS_REQUEST_IN_PROGRESS_GAUGE,
PROMETHEUS_RESPONSE_COUNTER,
)
from backend.common.queue import batch_dequeue
from backend.common.response.response_code import StandardResponseCode
from backend.core.conf import settings
@@ -43,6 +50,10 @@ class OperaLogMiddleware(BaseHTTPMiddleware):
else:
method = request.method
args = await self.get_request_args(request)
PROMETHEUS_REQUEST_IN_PROGRESS_GAUGE.labels(
app_name=settings.GRAFANA_APP_NAME, method=method, path=path
).inc()
PROMETHEUS_REQUEST_COUNTER.labels(app_name=settings.GRAFANA_APP_NAME, method=method, path=path).inc()
# 执行请求
code = 200
@@ -63,6 +74,12 @@ class OperaLogMiddleware(BaseHTTPMiddleware):
code = exception.get('code')
msg = exception.get('msg')
log.error(f'请求异常: {msg}')
PROMETHEUS_EXCEPTION_COUNTER.labels(
app_name=settings.GRAFANA_APP_NAME,
method=method,
path=path,
exception_type=type(e).__name__,
).inc()
break
except Exception as e:
elapsed = round((time.perf_counter() - ctx.perf_time) * 1000, 3)
@@ -71,6 +88,20 @@ class OperaLogMiddleware(BaseHTTPMiddleware):
status = StatusType.disable
error = e
log.error(f'请求异常: {e!s}')
PROMETHEUS_EXCEPTION_COUNTER.labels(
app_name=settings.GRAFANA_APP_NAME, method=method, path=path, exception_type=type(e).__name__
).inc()
else:
PROMETHEUS_REQUEST_COST_TIME_HISTOGRAM.labels(
app_name=settings.GRAFANA_APP_NAME, method=method, path=path
).observe(elapsed, exemplar={'TraceID': get_request_trace_id()})
finally:
PROMETHEUS_RESPONSE_COUNTER.labels(
app_name=settings.GRAFANA_APP_NAME, method=method, path=path, status_code=code
).inc()
PROMETHEUS_REQUEST_IN_PROGRESS_GAUGE.labels(
app_name=settings.GRAFANA_APP_NAME, method=method, path=path
).dec()
# 此信息只能在请求后获取
route = request.scope.get('route')
@@ -86,7 +117,7 @@ class OperaLogMiddleware(BaseHTTPMiddleware):
log.debug(f'接口摘要:[{summary}]')
log.debug(f'请求地址:[{ctx.ip}]')
log.debug(f'请求参数:{args}')
log.info(f'{request.client.host: <15} | {request.method: <8} | {code!s: <6} | {path} | {elapsed:.3f}ms')
log.info(f'{ctx.ip: <15} | {request.method: <8} | {code!s: <6} | {path} | {elapsed:.3f}ms')
if request.method != 'OPTIONS':
log.debug('<-- 请求结束')
@@ -3,6 +3,7 @@ from collections.abc import Sequence
from sqlalchemy import Row, RowMapping, text
from sqlalchemy.ext.asyncio import AsyncSession
from backend.common.enums import DataBaseType
from backend.core.conf import settings
@@ -18,7 +19,7 @@ class CRUDGen:
:param table_schema: 数据库 schema 名称
:return:
"""
if settings.DATABASE_TYPE == 'mysql':
if DataBaseType.mysql == settings.DATABASE_TYPE:
sql = """
SELECT table_name AS table_name, table_comment AS table_comment
FROM information_schema.tables
@@ -48,7 +49,7 @@ class CRUDGen:
:param table_name: 表名
:return:
"""
if settings.DATABASE_TYPE == 'mysql':
if DataBaseType.mysql == settings.DATABASE_TYPE:
sql = """
SELECT table_name AS table_name, table_comment AS table_comment
FROM information_schema.tables
@@ -79,7 +80,7 @@ class CRUDGen:
:param table_name: 表名
:return:
"""
if settings.DATABASE_TYPE == 'mysql':
if DataBaseType.mysql == settings.DATABASE_TYPE:
sql = """
SELECT column_name AS column_name,
CASE WHEN column_key = 'PRI' THEN 1 ELSE 0 END AS is_pk,
@@ -2,6 +2,7 @@ from collections.abc import Sequence
from sqlalchemy.ext.asyncio import AsyncSession
from backend.common.enums import DataBaseType
from backend.common.exception import errors
from backend.core.conf import settings
from backend.plugin.code_generator.crud.crud_column import gen_column_dao
@@ -32,7 +33,7 @@ class GenColumnService:
@staticmethod
async def get_types() -> list[str]:
"""获取所有列类型"""
if settings.DATABASE_TYPE == 'mysql':
if DataBaseType.mysql == settings.DATABASE_TYPE:
types = GenMySQLColumnType.get_member_keys()
else:
types = GenPostgreSQLColumnType.get_member_keys()
@@ -1,5 +1,6 @@
from functools import lru_cache
from backend.common.enums import DataBaseType
from backend.core.conf import settings
from backend.plugin.code_generator.enums import GenMySQLColumnType, GenPostgreSQLColumnType
@@ -12,7 +13,7 @@ def sql_type_to_sqlalchemy(typing: str) -> str:
:param typing: SQL 类型字符串
:return:
"""
if settings.DATABASE_TYPE == 'mysql':
if DataBaseType.mysql == settings.DATABASE_TYPE:
if typing in GenMySQLColumnType.get_member_keys():
return typing
else:
@@ -30,7 +31,7 @@ def sql_type_to_pydantic(typing: str) -> str:
:return:
"""
try:
if settings.DATABASE_TYPE == 'mysql':
if DataBaseType.mysql == settings.DATABASE_TYPE:
return GenMySQLColumnType[typing].value
if typing == 'CHARACTER VARYING': # postgresql 中 DDL VARCHAR 的别名
return 'str'
+12 -12
View File
@@ -55,15 +55,15 @@ def get_plugins() -> list[str]:
return plugin_packages
def get_plugin_models() -> list[type]:
def get_plugin_models() -> list[object]:
"""获取插件所有模型类"""
objs = []
for plugin in get_plugins():
module_path = f'backend.plugin.{plugin}.model'
obj = get_model_objects(module_path)
if obj:
objs.extend(obj)
model_objs = get_model_objects(module_path)
if model_objs:
objs.extend(model_objs)
return objs
@@ -79,16 +79,16 @@ async def get_plugin_sql(plugin: str, db_type: DataBaseType, pk_type: PrimaryKey
"""
if db_type == DataBaseType.mysql:
mysql_dir = PLUGIN_DIR / plugin / 'sql' / 'mysql'
if pk_type == PrimaryKeyType.autoincrement:
sql_file = mysql_dir / 'init.sql'
else:
sql_file = mysql_dir / 'init_snowflake.sql'
sql_file = (
mysql_dir / 'init.sql' if pk_type == PrimaryKeyType.autoincrement else mysql_dir / 'init_snowflake.sql'
)
else:
postgresql_dir = PLUGIN_DIR / plugin / 'sql' / 'postgresql'
if pk_type == PrimaryKeyType.autoincrement:
sql_file = postgresql_dir / 'init.sql'
else:
sql_file = postgresql_dir / 'init_snowflake.sql'
sql_file = (
postgresql_dir / 'init.sql'
if pk_type == PrimaryKeyType.autoincrement
else postgresql_dir / 'init_snowflake.sql'
)
path = anyio.Path(sql_file)
if not await path.exists():
+36 -5
View File
@@ -1,9 +1,12 @@
import importlib
import inspect
import os.path
from functools import lru_cache
from typing import Any, TypeVar
import sqlalchemy as sa
from backend.common.exception import errors
from backend.common.log import log
@@ -37,7 +40,7 @@ def dynamic_import_data_model(module_path: str) -> type[T]:
raise errors.ServerError(msg='数据模型列动态解析失败,请联系系统超级管理员')
def get_model_objects(module_path: str) -> list[type] | None:
def get_model_objects(module_path: str) -> list[object] | None:
"""
获取模型对象
@@ -47,15 +50,43 @@ def get_model_objects(module_path: str) -> list[type] | None:
try:
module = import_module_cached(module_path)
except ModuleNotFoundError:
log.warning(f'模块 {module_path} 中不包含模型对象')
return None
except Exception:
raise
except Exception as e:
raise e from None
classes = []
for _name, obj in inspect.getmembers(module):
if inspect.isclass(obj) and module_path in obj.__module__:
if (inspect.isclass(obj) and module_path in obj.__module__) or (
isinstance(obj, sa.Table) and obj.metadata is not None
):
classes.append(obj)
return classes
def get_app_models() -> list[object]:
"""获取 app 所有模型类"""
from backend.core.path_conf import BASE_PATH
app_path = BASE_PATH / 'app'
list_dirs = os.listdir(app_path)
apps = [d for d in list_dirs if os.path.isdir(os.path.join(app_path, d)) and d != '__pycache__']
objs = []
for app in apps:
module_path = f'backend.app.{app}.model'
model_objs = get_model_objects(module_path)
if model_objs:
objs.extend(model_objs)
return objs
@lru_cache
def get_all_models() -> list[object]:
"""获取所有模型类"""
from backend.plugin.tools import get_plugin_models
return get_app_models() + get_plugin_models()
+59
View File
@@ -0,0 +1,59 @@
from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry._logs import set_logger_provider
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.logging import LoggingInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs._internal.export import BatchLogRecordProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from backend.common.log import log, request_id_filter
from backend.core.conf import settings
from backend.database.db import async_engine
def init_otel(app: FastAPI) -> None:
"""
初始化 OpenTelemetry
:param app: FastAPI 应用实例
:return:
"""
from backend import __version__
resource = Resource(
attributes={
'service.name': settings.GRAFANA_APP_NAME,
'service.version': __version__,
'deployment.environment': settings.ENVIRONMENT,
},
)
tracer_provider = TracerProvider(resource=resource)
trace.set_tracer_provider(tracer_provider)
span_exporter = OTLPSpanExporter(endpoint=settings.GRAFANA_OTLP_GRPC_ENDPOINT, insecure=True)
tracer_provider.add_span_processor(BatchSpanProcessor(span_exporter))
logger_provider = LoggerProvider(resource=resource)
set_logger_provider(logger_provider)
log_exporter = OTLPLogExporter(endpoint=settings.GRAFANA_OTLP_GRPC_ENDPOINT, insecure=True)
logger_provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))
otel_logging_handler = LoggingHandler(logger_provider=logger_provider)
log.add( # type: ignore
otel_logging_handler,
level=settings.LOG_STD_LEVEL,
format=settings.LOG_FORMAT,
filter=lambda record: request_id_filter(record),
)
LoggingInstrumentor().instrument(set_logging_format=True)
SQLAlchemyInstrumentor().instrument(engine=async_engine.sync_engine)
FastAPIInstrumentor.instrument_app(app, tracer_provider=tracer_provider)
+23 -1
View File
@@ -1,9 +1,31 @@
from typing import Any
from opentelemetry import trace
from starlette.requests import Request
from starlette_context.plugins import Plugin
from backend.common.context import ctx
from backend.core.conf import settings
def get_request_trace_id() -> str:
"""请求头中获取追踪 ID"""
"""上下文中获取追踪 ID"""
if ctx.exists():
return ctx.get(settings.TRACE_ID_REQUEST_HEADER_KEY, settings.TRACE_ID_LOG_DEFAULT_VALUE)
return settings.TRACE_ID_LOG_DEFAULT_VALUE
class OtelTraceIdPlugin(Plugin):
"""OpenTelemetry Trace ID 插件"""
key = settings.TRACE_ID_REQUEST_HEADER_KEY
async def process_request(self, request: Request) -> Any:
"""从 OpenTelemetry span 中提取 trace_id"""
span = trace.get_current_span()
span_ctx = span.get_span_context()
if span_ctx.is_valid:
return trace.format_trace_id(span_ctx.trace_id)
return settings.TRACE_ID_LOG_DEFAULT_VALUE
-35
View File
@@ -1,35 +0,0 @@
[program:fba_celery_worker]
directory=/fba/backend
command=/usr/local/bin/celery -A app.task.celery worker -P gevent -c 1000 --loglevel=INFO
user=root
autostart=true
autorestart=true
startretries=5
redirect_stderr=true
stdout_logfile=/var/log/fba/fba_celery_worker.log
stdout_logfile_maxbytes=5MB
stdout_logfile_backups=5
[program:fba_celery_beat]
directory=/fba/backend
command=/usr/local/bin/celery -A app.task.celery beat --loglevel=INFO
user=root
autostart=true
autorestart=true
startretries=5
redirect_stderr=true
stdout_logfile=/var/log/fba/fba_celery_beat.log
stdout_logfile_maxbytes=5MB
stdout_logfile_backups=5
[program:fba_celery_flower]
directory=/fba/backend
command=/usr/local/bin/celery -A app.task.celery flower --port=8555 --url-prefix=flower --basic-auth=admin:123456
user=root
autostart=true
autorestart=true
startretries=5
redirect_stderr=true
stdout_logfile=/var/log/fba/fba_celery_flower.log
stdout_logfile_maxbytes=5MB
stdout_logfile_backups=5
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+158
View File
@@ -0,0 +1,158 @@
// ============================================================================
// Grafana Alloy 配置文件
// Alloy 是 Grafana 的可观测性数据收集器(原 Grafana Agent
// 官方文档: https://grafana.com/docs/alloy/latest/
//
// 数据流向:
// 应用程序 -> OTLP Receiver -> Batch Processor -> Exporters -> 后端存储
// - 日志 (Logs) -> Loki
// - 指标 (Metrics) -> Prometheus
// - 链路 (Traces) -> Tempo
// ============================================================================
// OTLP 接收器配置
// OpenTelemetry Protocol (OTLP) 是 OpenTelemetry 的标准数据传输协议
// 支持同时接收日志、指标和链路追踪数据
otelcol.receiver.otlp "default" {
// gRPC 协议配置
// gRPC 是高性能的 RPC 框架,适用于服务间通信
grpc {
endpoint = "0.0.0.0:4317"
// 0.0.0.0 表示监听所有网络接口
// 4317 是 OTLP gRPC 的标准端口
// 应用程序通过此端口发送遥测数据
}
// HTTP 协议配置
// HTTP 协议更通用,适用于无法使用 gRPC 的场景
http {
endpoint = "0.0.0.0:4318"
// 4318 是 OTLP HTTP 的标准端口
// 支持 JSON 和 Protobuf 格式的数据
}
// 输出配置
// 定义接收到的数据发送到哪些处理器
output {
logs = [otelcol.processor.batch.default.input]
metrics = [otelcol.processor.batch.default.input]
traces = [otelcol.processor.batch.default.input]
}
}
// 批处理器配置
// 将多个遥测数据项合并成批次,提高传输效率
otelcol.processor.batch "default" {
// 输出配置
// 定义处理后的数据发送到哪些导出器
output {
logs = [otelcol.exporter.loki.default.input]
metrics = [otelcol.exporter.prometheus.default.input]
traces = [otelcol.exporter.otlp.tempo.input]
}
}
// Loki 导出器配置
// 将日志数据发送到 Grafana Loki
otelcol.exporter.loki "default" {
forward_to = [loki.write.loki_server.receiver]
}
// Loki 写入组件
// 负责将日志数据推送到 Loki 服务器
loki.write "loki_server" {
endpoint {
url = "http://fba_loki:3100/loki/api/v1/push"
}
}
// Prometheus 导出器配置
// 将指标数据发送到 Prometheus
otelcol.exporter.prometheus "default" {
forward_to = [prometheus.remote_write.prom_server.receiver]
}
// Prometheus 远程写入组件
// 负责将指标数据推送到 Prometheus 服务器
prometheus.remote_write "prom_server" {
endpoint {
url = "http://fba_prometheus:9090/api/v1/write"
}
}
// Prometheus 自身指标抓取
// 监控 Prometheus 服务器本身的运行状态
prometheus.scrape "prometheus" {
targets = [{
__address__ = "fba_prometheus:9090",
}]
forward_to = [prometheus.remote_write.prom_server.receiver]
// 抓取的指标发送到远程写入组件
job_name = "fba_prometheus"
// 任务名称
// 将作为 job 标签添加到所有抓取的指标上
scrape_interval = "15s"
// 抓取间隔
}
// FastAPI 后端服务指标抓取
// 监控 FastAPI 应用的性能和业务指标
prometheus.scrape "fba_server" {
targets = [{
__address__ = "fba_server:8001",
}]
forward_to = [prometheus.remote_write.prom_server.receiver]
// 抓取的指标发送到远程写入组件
job_name = "fba_server"
// 任务名称
// 用于在 Prometheus 查询中区分不同服务
scrape_interval = "5s"
// 抓取间隔
scrape_timeout = "5s"
// 抓取超时时间
}
// Celery Exporter 指标抓取
// 监控 Celery 异步任务队列的运行状态
prometheus.scrape "fba_celery_exporter" {
targets = [{
__address__ = "fba_celery_exporter:9808",
}]
forward_to = [prometheus.remote_write.prom_server.receiver]
// 抓取的指标发送到远程写入组件
job_name = "fba_celery_exporter"
// 任务名称
// 用于标识 Celery 相关的指标
scrape_interval = "5s"
// 抓取间隔
scrape_timeout = "5s"
// 抓取超时时间
}
// Tempo 导出器配置
// 将链路追踪数据发送到 Grafana Tempo
otelcol.exporter.otlp "tempo" {
client {
endpoint = "fba_tempo:4317"
tls {
insecure = true
// 禁用 TLS 加密
// 在 Docker 内部网络中通信时可以禁用
// 生产环境建议启用 TLS
insecure_skip_verify = true
// 跳过 TLS 证书验证
// 仅在开发/测试环境使用
// 生产环境应配置正确的证书
}
}
}
+36
View File
@@ -0,0 +1,36 @@
# ============================================================================
# Grafana 仪表盘 Provisioning 配置文件
# 官方文档: https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards
# 用于自动加载和管理仪表盘,无需手动导入
# ============================================================================
# API 版本号
# 当前仅支持版本 1
apiVersion: 1
# providers: 仪表盘提供者列表
# 定义 Grafana 从哪里加载仪表盘以及如何管理它们
providers:
- name: 'fba_server'
# 提供者名称,用于标识和日志记录
orgId: 1
# 组织 ID
# 指定仪表盘属于哪个组织
# 默认值: 1(默认组织)
type: 'file'
# 提供者类型
# 可选值:
# - file: 从本地文件系统加载仪表盘 JSON 文件
disableDeletion: true
# 禁止通过 UI 删除仪表盘
editable: false
# 是否允许通过 UI 编辑仪表盘
options:
path: '/etc/grafana/dashboards'
# 仪表盘 JSON 文件所在目录(容器内路径)
# Grafana 会自动扫描此目录下的所有 .json 文件
+164
View File
@@ -0,0 +1,164 @@
# ============================================================================
# Grafana 数据源 Provisioning 配置文件
# 官方文档: https://grafana.com/docs/grafana/latest/administration/provisioning/#datasources
# 用于自动配置数据源,实现基础设施即代码
# ============================================================================
# API 版本号
# 当前仅支持版本 1
apiVersion: 1
# datasources: 数据源列表
# 定义 Grafana 连接的所有后端数据存储
datasources:
# ==========================================================================
# Loki 数据源配置
# Loki 是 Grafana 的日志聚合系统,类似于 Prometheus 但用于日志
# 官方文档: https://grafana.com/docs/loki/latest/
# ==========================================================================
- name: Loki
# 数据源显示名称,在 Grafana UI 中显示
uid: loki
# 数据源唯一标识符
# 用于在仪表盘和告警规则中引用此数据源
type: loki
# 数据源类型
# 必须与 Grafana 支持的数据源插件类型匹配
url: http://fba_loki:3100
# Loki 服务器地址
isDefault: false
# 是否设为默认数据源
jsonData:
# 数据源特定的 JSON 配置
derivedFields:
# 派生字段配置
# 用于从日志中提取字段并创建链接到其他数据源
# 这是实现日志到链路追踪关联的关键配置
- datasourceUid: tempo
# 目标数据源的 UID
# 点击链接时将跳转到此数据源
matcherRegex: '"traceid":"([a-f0-9]{32})"'
# 正则表达式,用于从日志内容中提取 Trace ID
matcherType: regex
# 匹配器类型
# 可选值: regex(正则表达式)
name: TraceID
# 派生字段的名称
# 将显示在日志详情中
url: $${__value.raw}
# 链接 URL 模板
# $${__value.raw} 表示提取的原始值
urlDisplayLabel: 查看 Trace
# 链接显示的文本标签
# ==========================================================================
# Prometheus 数据源配置
# Prometheus 是云原生监控系统,用于收集和存储时序指标数据
# 官方文档: https://prometheus.io/docs/
# ==========================================================================
- name: Prometheus
# 数据源显示名称
uid: prometheus
# 数据源唯一标识符
type: prometheus
# 数据源类型
url: http://fba_prometheus:9090
# Prometheus 服务器地址
isDefault: true
# 设为默认数据源
jsonData:
httpMethod: POST
# 查询使用的 HTTP 方法
exemplarTraceIdDestinations:
# Exemplar(范例)配置
# Exemplar 是指标数据点关联的 Trace ID
# 用于从指标跳转到对应的链路追踪
- name: TraceID
# Exemplar 中 Trace ID 的字段名称
datasourceUid: tempo
# 目标 Tempo 数据源的 UID
# 点击 Exemplar 时将跳转到此数据源查看 Trace
# ==========================================================================
# Tempo 数据源配置
# Tempo 是 Grafana 的分布式链路追踪后端
# 官方文档: https://grafana.com/docs/tempo/latest/
# ==========================================================================
- name: Tempo
# 数据源显示名称
uid: tempo
# 数据源唯一标识符
type: tempo
# 数据源类型
url: http://fba_tempo:3200
# Tempo 服务器地址
jsonData:
nodeGraph:
enabled: true
# 启用节点图功能
# 可视化展示服务之间的调用关系图
tracesToLogsV2:
# Trace 到日志的关联配置(V2 版本)
# 允许从 Trace 详情页面跳转到相关日志
datasourceUid: loki
# 日志数据源的 UID
filterByTraceID: true
# 是否按 Trace ID 过滤日志
spanStartTimeShift: "-1m"
# Span 开始时间偏移
# 用于捕获 Span 开始前的相关日志
spanEndTimeShift: "1m"
# Span 结束时间偏移
# 用于捕获 Span 结束后的相关日志
customQuery: true
# 启用自定义查询
# 允许使用下面的 query 字段自定义日志查询语句
query: '{service_name="fba_server"} | json | traceid="${__span.traceId}"'
# 自定义 LogQL 查询语句
search:
hide: false
# 是否隐藏搜索功能
serviceMap:
datasourceUid: prometheus
# 服务地图数据源
# 使用 Prometheus 中的指标数据生成服务依赖图
# 需要 Tempo 的 metrics_generator 功能配合
lokiSearch:
datasourceUid: loki
# Loki 搜索数据源
# 允许在 Tempo 中使用 Loki 进行日志搜索
# 实现基于日志内容查找 Trace 的功能
+40
View File
@@ -0,0 +1,40 @@
# ============================================================================
# Grafana 主配置文件
# 官方文档: https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/
# ============================================================================
# [server] 服务器配置
# 用于配置 Grafana HTTP 服务器的基本设置
[server]
# 协议类型
protocol = http
# HTTP 服务监听端口
http_port = 3000
# [security] 安全配置
# 用于配置 Grafana 安全相关的默认设置
[security]
# 默认 Grafana 管理员用户名
admin_user = admin
# 默认 Grafana 管理员密码
admin_password = 123456
# 如果您将 Grafana 托管在 HTTPS
;cookie_secure = true
# [users] 用户配置
# 用于配置用户相关的默认设置
[users]
# 默认 UI 主题
default_theme=system
# 默认界面语言
default_language = zh-Hans
# [dashboards] 仪表盘配置
# 用于配置仪表盘相关的默认设置
[dashboards]
# 默认首页仪表盘路径
default_home_dashboard_path = /etc/grafana/dashboards/fba_server.json
+13
View File
@@ -0,0 +1,13 @@
# ============================================================================
# Prometheus 配置文件
# Prometheus 是云原生监控和告警系统,专注于时序指标数据
# 官方文档: https://prometheus.io/docs/prometheus/latest/configuration/configuration/
# ============================================================================
# global: 全局配置
global:
scrape_interval: 15s
# 指标抓取间隔
evaluation_interval: 15s
# 规则评估间隔
+127
View File
@@ -0,0 +1,127 @@
# ============================================================================
# Tempo 配置文件
# Tempo 是 Grafana 的高性能、低成本分布式链路追踪后端
# 官方文档: https://grafana.com/docs/tempo/latest/configuration/
# ============================================================================
# server: HTTP 服务器配置
server:
http_listen_port: 3200
# HTTP API 监听端口
# 用于查询 Trace 数据和健康检查
# distributor: 分发器配置
distributor:
receivers:
# 接收器配置
otlp:
# OpenTelemetry Protocol (OTLP) 接收器
# OTLP 是 OpenTelemetry 的原生协议,推荐使用
protocols:
grpc:
endpoint: "0.0.0.0:4317"
# gRPC 协议端点
http:
endpoint: "0.0.0.0:4318"
# HTTP 协议端点
# storage: 存储配置
storage:
trace:
backend: local
# 存储后端类型
# 生产环境建议使用对象存储
wal:
path: /tmp/tempo/wal
# Write-Ahead Log (WAL) 目录
# WAL 用于在数据写入存储前的临时缓存
# 确保数据不会因意外重启而丢失
# 注意: 生产环境应使用持久化存储路径
local:
path: /tmp/tempo/blocks
# 本地存储的数据块目录
# 存储压缩后的 Trace 数据块
# 仅在 backend: local 时使用
# metrics_generator: 指标生成器配置
# 从 Trace 数据自动生成 Prometheus 指标
# 这是实现 Trace 到 Metrics 关联的关键功能
metrics_generator:
registry:
external_labels:
source: tempo
# 外部标签
# 添加到所有生成的指标上
storage:
path: /tmp/tempo/generator/wal
# 指标生成器的 WAL 目录
# 用于临时存储待发送的指标数据
remote_write:
# 远程写入配置
- url: http://fba_prometheus:9090/api/v1/write
# Prometheus 远程写入 API 地址
# 需要 Prometheus 启用 remote-write-receiver 功能
send_exemplars: true
# 发送 Exemplar 数据
# Exemplar 将指标数据点与 Trace ID 关联
# 实现从指标图表点击跳转到对应 Trace
traces_storage:
path: /tmp/tempo/generator/traces
# Trace 数据临时存储路径
# 用于指标生成器处理 Trace 数据
processor:
# 处理器配置
# 定义如何从 Trace 生成指标
span_metrics:
# Span 指标处理器
dimensions:
# 指标维度(标签)
# 这些属性将作为 Prometheus 指标的标签
- service.name
- http.method
- http.target
- http.status_code
service_graphs:
# 服务图处理器
# 生成服务间调用关系的指标
# 用于 Grafana 中的服务地图可视化
dimensions:
- service.name
local_blocks:
# 本地块处理器
# 处理本地存储的 Trace 数据块
flush_to_storage: true
# 是否将处理后的数据刷新到存储
# overrides: 覆盖配置
overrides:
defaults:
# 默认配置
# 适用于所有租户(单租户模式下为全局配置)
metrics_generator:
processors:
# 启用的指标处理器列表
# 只有在此列表中的处理器才会运行
- span-metrics
- service-graphs
- local-blocks
+1 -1
View File
@@ -31,7 +31,7 @@ server {
}
location /flower/ {
proxy_pass http://fba_server:8555;
proxy_pass http://fba_celery_flower:8555;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
@@ -0,0 +1,11 @@
[program:fba_celery_beat]
directory=/fba/backend
command=/usr/local/bin/celery -A app.task.celery beat --loglevel=INFO
user=root
autostart=true
autorestart=true
startretries=5
redirect_stderr=true
stdout_logfile=/var/log/fba/fba_celery_beat.log
stdout_logfile_maxbytes=5MB
stdout_logfile_backups=5
@@ -0,0 +1,11 @@
[program:fba_celery_flower]
directory=/fba/backend
command=/usr/local/bin/celery -A app.task.celery flower --port=8555 --url-prefix=flower --basic-auth=admin:123456
user=root
autostart=true
autorestart=true
startretries=5
redirect_stderr=true
stdout_logfile=/var/log/fba/fba_celery_flower.log
stdout_logfile_maxbytes=5MB
stdout_logfile_backups=5
@@ -0,0 +1,11 @@
[program:fba_celery_worker]
directory=/fba/backend
command=/usr/local/bin/celery -A app.task.celery worker -P gevent -c 1000 --loglevel=INFO
user=root
autostart=true
autorestart=true
startretries=5
redirect_stderr=true
stdout_logfile=/var/log/fba/fba_celery_worker.log
stdout_logfile_maxbytes=5MB
stdout_logfile_backups=5
+162 -7
View File
@@ -19,6 +19,8 @@ volumes:
name: fba_static_upload
fba_rabbitmq:
name: fba_rabbitmq
fba_prometheus_data:
name: fba_prometheus_data
services:
fba_server:
@@ -30,11 +32,12 @@ services:
- "8001:8001"
container_name: fba_server
restart: always
environment:
- TZ=Asia/Shanghai
# 如果你是 mysql 用户,应将 fba_postgres 修改为 fba_mysql
depends_on:
- fba_postgres
- fba_redis
- fba_celery
volumes:
- ./deploy/backend/docker-compose/.env.server:/fba/backend/.env
- fba_static:/fba/backend/app/static
@@ -167,17 +170,18 @@ services:
networks:
- fba_network
fba_celery:
fba_celery_worker:
build:
context: .
dockerfile: Dockerfile
args:
- SERVER_TYPE=fba_celery
image: fba_celery:latest
ports:
- "8555:8555"
container_name: fba_celery
- SERVER_TYPE=fba_celery_worker
image: fba_celery_worker:latest
# 如果你需要分布式部署 Worker,则必须移除此 container_name 配置
container_name: fba_celery_worker
restart: always
environment:
- TZ=Asia/Shanghai
depends_on:
- fba_rabbitmq
volumes:
@@ -191,3 +195,154 @@ services:
wait-for-it -s fba_rabbitmq:5672 -t 300
supervisord -c /etc/supervisor/supervisord.conf
supervisorctl restart
fba_celery_beat:
build:
context: .
dockerfile: Dockerfile
args:
- SERVER_TYPE=fba_celery_beat
image: fba_celery_beat:latest
container_name: fba_celery_beat
restart: always
environment:
- TZ=Asia/Shanghai
depends_on:
- fba_rabbitmq
- fba_celery_worker
volumes:
- ./deploy/backend/docker-compose/.env.server:/fba/backend/.env
networks:
- fba_network
command:
- bash
- -c
- |
wait-for-it -s fba_rabbitmq:5672 -t 300
supervisord -c /etc/supervisor/supervisord.conf
supervisorctl restart
fba_celery_flower:
build:
context: .
dockerfile: Dockerfile
args:
- SERVER_TYPE=fba_celery_flower
image: fba_celery_flower:latest
ports:
- "8555:8555"
container_name: fba_celery_flower
restart: always
environment:
- TZ=Asia/Shanghai
depends_on:
- fba_rabbitmq
- fba_celery_worker
volumes:
- ./deploy/backend/docker-compose/.env.server:/fba/backend/.env
networks:
- fba_network
command:
- bash
- -c
- |
wait-for-it -s fba_rabbitmq:5672 -t 300
supervisord -c /etc/supervisor/supervisord.conf
supervisorctl restart
fba_celery_exporter:
image: danihodovic/celery-exporter
restart: always
ports:
- "9808:9808"
container_name: fba_celery_exporter
environment:
- CE_BROKER_URL=amqp://guest:guest@fba_rabbitmq:5672//
depends_on:
- fba_rabbitmq
networks:
- fba_network
fba_loki:
image: grafana/loki
restart: always
ports:
- "3100:3100"
container_name: fba_loki
networks:
- fba_network
command: -config.file=/etc/loki/local-config.yaml
fba_prometheus:
image: prom/prometheus
restart: always
ports:
- "9090:9090"
container_name: fba_prometheus
depends_on:
- fba_loki
volumes:
- ./deploy/backend/grafana/fba_prometheus.yml:/workspace/fba_prometheus.yml
- fba_prometheus_data:/prometheus
networks:
- fba_network
command:
- --web.enable-remote-write-receiver
- --enable-feature=exemplar-storage
- --config.file=/workspace/fba_prometheus.yml
fba_alloy:
image: grafana/alloy
restart: always
ports:
- "4317:4317"
- "4318:4318"
- "12345:12345"
container_name: fba_alloy
depends_on:
- fba_loki
volumes:
- ./deploy/backend/grafana/fba_config.alloy:/etc/alloy/fba_config.alloy
networks:
- fba_network
command:
- run
- /etc/alloy/fba_config.alloy
- --server.http.listen-addr=0.0.0.0:12345
fba_tempo:
image: grafana/tempo:latest
restart: always
ports:
- "3200:3200"
expose:
- 4317
- 4318
container_name: fba_tempo
depends_on:
- fba_loki
volumes:
- ./deploy/backend/grafana/fba_tempo.yml:/etc/tempo/fba_tempo.yml
networks:
- fba_network
command:
- -config.file=/etc/tempo/fba_tempo.yml
- -auth.enabled=false
fba_grafana:
image: grafana/grafana
restart: always
ports:
- "3000:3000"
container_name: fba_grafana
depends_on:
- fba_loki
- fba_prometheus
volumes:
- ./deploy/backend/grafana/fba_grafana.ini:/etc/grafana/grafana.ini
- ./deploy/backend/grafana/dashboards/fba_celery.json:/etc/grafana/dashboards/fba_celery.json
- ./deploy/backend/grafana/dashboards/fba_server.json:/etc/grafana/dashboards/fba_server.json
- ./deploy/backend/grafana/fba_dashboards.yml:/etc/grafana/provisioning/dashboards/fba_dashboards.yml
- ./deploy/backend/grafana/fba_datasource.yml:/etc/grafana/provisioning/datasources/fba_datasource.yml
networks:
- fba_network
+1 -1
View File
@@ -1,3 +1,3 @@
#!/usr/bin/env bash
pre-commit run --all-files --verbose --show-diff-on-failure
prek run --all-files --verbose --show-diff-on-failure
+11 -5
View File
@@ -18,7 +18,7 @@ dependencies = [
"asyncpg>=0.31.0",
"bcrypt>=5.0.0",
"cappa>=0.30.4",
"celery>=5.5.3",
"celery>=5.6.0",
# When celery version < 6.0.0
# https://github.com/celery/celery/issues/7874
"celery-aio-pool>=0.1.0rc8",
@@ -27,7 +27,7 @@ dependencies = [
"fast-captcha>=0.3.2",
"fastapi-limiter>=0.1.6",
"fastapi-pagination>=0.15.0",
"fastapi[standard-no-fastapi-cloud-cli]>=0.122.0",
"fastapi[standard-no-fastapi-cloud-cli]>=0.123.5",
"flower>=2.0.1",
"gevent>=25.9.1",
"granian>=2.6.0",
@@ -36,11 +36,17 @@ dependencies = [
"jinja2>=3.1.6",
"loguru>=0.7.3",
"msgspec>=0.20.0",
"opentelemetry-exporter-otlp-proto-grpc>=1.39.0",
"opentelemetry-instrumentation-fastapi>=0.60b0",
"opentelemetry-instrumentation-logging>=0.60b0",
"opentelemetry-instrumentation-sqlalchemy>=0.60b0",
"opentelemetry-sdk>=1.39.0",
"prometheus-client>=0.23.1",
"psutil>=7.1.3",
# https://github.com/fastapi-practices/fastapi_best_architecture/issues/887
"psycopg[binary]==3.2.10",
"pwdlib>=0.3.0",
"pydantic>=2.12.4",
"pydantic>=2.12.5",
"pydantic-settings>=2.12.0",
"pymysql>=1.1.2",
"python-jose>=3.5.0",
@@ -49,7 +55,7 @@ dependencies = [
"rtoml>=0.13.0",
"sqlalchemy-crud-plus>=1.13.1",
"sqlalchemy[asyncio]>=2.0.44",
"sqlparse>=0.5.3",
"sqlparse>=0.5.4",
"starlette-context>=0.4.0",
"user-agents>=2.2.0",
]
@@ -60,7 +66,7 @@ dev = [
"pytest-sugar>=1.1.1",
]
lint = [
"prek>=0.2.18",
"prek>=0.2.19",
]
server = [
"aio-pika>=9.5.8",
+90 -14
View File
@@ -9,13 +9,15 @@ annotated-doc==0.0.4
# via fastapi
annotated-types==0.7.0
# via pydantic
anyio==4.11.0
anyio==4.12.0
# via
# httpx
# starlette
# watchfiles
asgiref==3.11.0
# via fastapi-best-architecture
# via
# fastapi-best-architecture
# opentelemetry-instrumentation-asgi
async-timeout==5.0.1 ; python_full_version < '3.11.3'
# via
# asyncpg
@@ -28,11 +30,11 @@ bcrypt==5.0.0
# via fastapi-best-architecture
bidict==0.23.1
# via python-socketio
billiard==4.2.3
billiard==4.2.4
# via celery
cappa==0.30.4
# via fastapi-best-architecture
celery==5.5.3
celery==5.6.0
# via
# celery-aio-pool
# fastapi-best-architecture
@@ -79,13 +81,14 @@ ecdsa==0.19.1
# via python-jose
email-validator==2.3.0
# via fastapi
exceptiongroup==1.3.1 ; python_full_version < '3.11'
exceptiongroup==1.3.1
# via
# anyio
# celery
# pytest
fast-captcha==0.3.2
# via fastapi-best-architecture
fastapi==0.122.0
fastapi==0.123.5
# via
# fastapi-best-architecture
# fastapi-limiter
@@ -100,12 +103,16 @@ flower==2.0.1
# via fastapi-best-architecture
gevent==25.9.1
# via fastapi-best-architecture
googleapis-common-protos==1.72.0
# via opentelemetry-exporter-otlp-proto-grpc
granian==2.6.0
# via fastapi-best-architecture
greenlet==3.2.4
# via
# gevent
# sqlalchemy
grpcio==1.76.0
# via opentelemetry-exporter-otlp-proto-grpc
h11==0.16.0
# via
# httpcore
@@ -126,6 +133,8 @@ idna==3.11
# anyio
# email-validator
# httpx
importlib-metadata==8.7.0
# via opentelemetry-api
iniconfig==2.3.0
# via pytest
ip2loc==1.0.0
@@ -136,7 +145,7 @@ jinja2==3.1.6
# via
# fastapi
# fastapi-best-architecture
kombu==5.5.4
kombu==5.6.1
# via celery
loguru==0.7.3
# via fastapi-best-architecture
@@ -152,19 +161,74 @@ mdurl==0.1.2
# via markdown-it-py
msgspec==0.20.0
# via fastapi-best-architecture
opentelemetry-api==1.39.0
# via
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-instrumentation
# opentelemetry-instrumentation-asgi
# opentelemetry-instrumentation-fastapi
# opentelemetry-instrumentation-logging
# opentelemetry-instrumentation-sqlalchemy
# opentelemetry-sdk
# opentelemetry-semantic-conventions
opentelemetry-exporter-otlp-proto-common==1.39.0
# via opentelemetry-exporter-otlp-proto-grpc
opentelemetry-exporter-otlp-proto-grpc==1.39.0
# via fastapi-best-architecture
opentelemetry-instrumentation==0.60b0
# via
# opentelemetry-instrumentation-asgi
# opentelemetry-instrumentation-fastapi
# opentelemetry-instrumentation-logging
# opentelemetry-instrumentation-sqlalchemy
opentelemetry-instrumentation-asgi==0.60b0
# via opentelemetry-instrumentation-fastapi
opentelemetry-instrumentation-fastapi==0.60b0
# via fastapi-best-architecture
opentelemetry-instrumentation-logging==0.60b0
# via fastapi-best-architecture
opentelemetry-instrumentation-sqlalchemy==0.60b0
# via fastapi-best-architecture
opentelemetry-proto==1.39.0
# via
# opentelemetry-exporter-otlp-proto-common
# opentelemetry-exporter-otlp-proto-grpc
opentelemetry-sdk==1.39.0
# via
# fastapi-best-architecture
# opentelemetry-exporter-otlp-proto-grpc
opentelemetry-semantic-conventions==0.60b0
# via
# opentelemetry-instrumentation
# opentelemetry-instrumentation-asgi
# opentelemetry-instrumentation-fastapi
# opentelemetry-instrumentation-sqlalchemy
# opentelemetry-sdk
opentelemetry-util-http==0.60b0
# via
# opentelemetry-instrumentation-asgi
# opentelemetry-instrumentation-fastapi
packaging==25.0
# via
# kombu
# opentelemetry-instrumentation
# opentelemetry-instrumentation-sqlalchemy
# pytest
pillow==12.0.0
# via fast-captcha
pluggy==1.6.0
# via pytest
prek==0.2.18
prek==0.2.19
prometheus-client==0.23.1
# via flower
# via
# fastapi-best-architecture
# flower
prompt-toolkit==3.0.52
# via click-repl
protobuf==6.33.2
# via
# googleapis-common-protos
# opentelemetry-proto
psutil==7.1.3
# via fastapi-best-architecture
psycopg==3.2.10
@@ -179,7 +243,7 @@ pyasn1==0.6.1
# rsa
pycparser==2.23 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy'
# via cffi
pydantic==2.12.4
pydantic==2.12.5
# via
# fastapi
# fastapi-best-architecture
@@ -226,7 +290,7 @@ rich==14.2.0
# cappa
# rich-toolkit
# typer
rich-toolkit==0.16.0
rich-toolkit==0.17.0
# via fastapi-cli
rsa==4.9.1
# via python-jose
@@ -240,8 +304,6 @@ six==1.17.0
# via
# ecdsa
# python-dateutil
sniffio==1.3.1
# via anyio
sqlalchemy==2.0.44
# via
# alembic
@@ -249,7 +311,7 @@ sqlalchemy==2.0.44
# sqlalchemy-crud-plus
sqlalchemy-crud-plus==1.13.1
# via fastapi-best-architecture
sqlparse==0.5.3
sqlparse==0.5.4
# via fastapi-best-architecture
starlette==0.50.0
# via
@@ -281,6 +343,11 @@ typing-extensions==4.15.0
# exceptiongroup
# fastapi
# fastapi-pagination
# grpcio
# opentelemetry-api
# opentelemetry-exporter-otlp-proto-grpc
# opentelemetry-sdk
# opentelemetry-semantic-conventions
# psycopg
# pydantic
# pydantic-core
@@ -299,6 +366,9 @@ tzdata==2025.2
# via
# kombu
# psycopg
# tzlocal
tzlocal==5.3.1
# via celery
ua-parser==1.0.1
# via user-agents
ua-parser-builtins==0.18.0.post1
@@ -326,8 +396,14 @@ websockets==15.0.1
# via uvicorn
win32-setctime==1.2.0 ; sys_platform == 'win32'
# via loguru
wrapt==1.17.3
# via
# opentelemetry-instrumentation
# opentelemetry-instrumentation-sqlalchemy
wsproto==1.3.2
# via simple-websocket
zipp==3.23.0
# via importlib-metadata
zope-event==6.1
# via gevent
zope-interface==8.1.1
Generated
+420 -59
View File
@@ -80,17 +80,16 @@ wheels = [
[[package]]
name = "anyio"
version = "4.11.0"
version = "4.12.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc" },
{ url = "https://mirrors.aliyun.com/pypi/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb" },
]
[[package]]
@@ -291,11 +290,11 @@ wheels = [
[[package]]
name = "billiard"
version = "4.2.3"
version = "4.2.4"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/6a/50/cc2b8b6e6433918a6b9a3566483b743dcd229da1e974be9b5f259db3aad7/billiard-4.2.3.tar.gz", hash = "sha256:96486f0885afc38219d02d5f0ccd5bec8226a414b834ab244008cbb0025b8dcb" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/b3/cc/38b6f87170908bd8aaf9e412b021d17e85f690abe00edf50192f1a4566b9/billiard-4.2.3-py3-none-any.whl", hash = "sha256:989e9b688e3abf153f307b68a1328dfacfb954e30a4f920005654e276c69236b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5" },
]
[[package]]
@@ -314,7 +313,7 @@ wheels = [
[[package]]
name = "celery"
version = "5.5.3"
version = "5.6.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "billiard" },
@@ -322,13 +321,15 @@ dependencies = [
{ name = "click-didyoumean" },
{ name = "click-plugins" },
{ name = "click-repl" },
{ name = "exceptiongroup" },
{ name = "kombu" },
{ name = "python-dateutil" },
{ name = "tzlocal" },
{ name = "vine" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/bb/7d/6c289f407d219ba36d8b384b42489ebdd0c84ce9c413875a8aae0c85f35b/celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ad/5f/b681ae3c89290d2ea6562ea96b40f5af6f6fc5f7743e2cd1a19e47721548/celery-5.6.0.tar.gz", hash = "sha256:641405206042d52ae460e4e9751a2e31b06cf80ab836fcf92e0b9311d7ea8113" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525" },
{ url = "https://mirrors.aliyun.com/pypi/packages/01/4e/53a125038d6a814491a0ae3457435c13cf8821eb602292cf9db37ce35f62/celery-5.6.0-py3-none-any.whl", hash = "sha256:33cf01477b175017fc8f22c5ee8a65157591043ba8ca78a443fe703aa910f581" },
]
[[package]]
@@ -654,7 +655,7 @@ wheels = [
[[package]]
name = "fastapi"
version = "0.122.0"
version = "0.123.5"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "annotated-doc" },
@@ -662,9 +663,9 @@ dependencies = [
{ name = "starlette" },
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b2/de/3ee97a4f6ffef1fb70bf20561e4f88531633bb5045dc6cebc0f8471f764d/fastapi-0.122.0.tar.gz", hash = "sha256:cd9b5352031f93773228af8b4c443eedc2ac2aa74b27780387b853c3726fb94b" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/78/b8/c6e916565af8a8e1c8f5a4736b31a6995adb51dbd4cbefc8b022e753ecb9/fastapi-0.123.5.tar.gz", hash = "sha256:54bbb660ca231d3985474498b51c621ddcf8888d9a4c1ecb10aa40ec217e4965" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/7a/93/aa8072af4ff37b795f6bbf43dcaf61115f40f49935c7dbb180c9afc3f421/fastapi-0.122.0-py3-none-any.whl", hash = "sha256:a456e8915dfc6c8914a50d9651133bd47ec96d331c5b44600baa635538a30d67" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c2/dc/faa52fe784892bb057934248ded02705d26ca3aca562876e61c239947036/fastapi-0.123.5-py3-none-any.whl", hash = "sha256:a9c708e47c0fa424139cddb8601d0f92d3111b77843c22e9c8d0164d65fe3c97" },
]
[package.optional-dependencies]
@@ -703,6 +704,12 @@ dependencies = [
{ name = "jinja2" },
{ name = "loguru" },
{ name = "msgspec" },
{ name = "opentelemetry-exporter-otlp-proto-grpc" },
{ name = "opentelemetry-instrumentation-fastapi" },
{ name = "opentelemetry-instrumentation-logging" },
{ name = "opentelemetry-instrumentation-sqlalchemy" },
{ name = "opentelemetry-sdk" },
{ name = "prometheus-client" },
{ name = "psutil" },
{ name = "psycopg", extra = ["binary"] },
{ name = "pwdlib" },
@@ -741,12 +748,12 @@ requires-dist = [
{ name = "asyncpg", specifier = ">=0.31.0" },
{ name = "bcrypt", specifier = ">=5.0.0" },
{ name = "cappa", specifier = ">=0.30.4" },
{ name = "celery", specifier = ">=5.5.3" },
{ name = "celery", specifier = ">=5.6.0" },
{ name = "celery-aio-pool", specifier = ">=0.1.0rc8" },
{ name = "cryptography", specifier = ">=46.0.3" },
{ name = "dulwich", specifier = ">=0.24.10" },
{ name = "fast-captcha", specifier = ">=0.3.2" },
{ name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier = ">=0.122.0" },
{ name = "fastapi", extras = ["standard-no-fastapi-cloud-cli"], specifier = ">=0.123.5" },
{ name = "fastapi-limiter", specifier = ">=0.1.6" },
{ name = "fastapi-pagination", specifier = ">=0.15.0" },
{ name = "flower", specifier = ">=2.0.1" },
@@ -757,10 +764,16 @@ requires-dist = [
{ name = "jinja2", specifier = ">=3.1.6" },
{ name = "loguru", specifier = ">=0.7.3" },
{ name = "msgspec", specifier = ">=0.20.0" },
{ name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.39.0" },
{ name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.60b0" },
{ name = "opentelemetry-instrumentation-logging", specifier = ">=0.60b0" },
{ name = "opentelemetry-instrumentation-sqlalchemy", specifier = ">=0.60b0" },
{ name = "opentelemetry-sdk", specifier = ">=1.39.0" },
{ name = "prometheus-client", specifier = ">=0.23.1" },
{ name = "psutil", specifier = ">=7.1.3" },
{ name = "psycopg", extras = ["binary"], specifier = "==3.2.10" },
{ name = "pwdlib", specifier = ">=0.3.0" },
{ name = "pydantic", specifier = ">=2.12.4" },
{ name = "pydantic", specifier = ">=2.12.5" },
{ name = "pydantic-settings", specifier = ">=2.12.0" },
{ name = "pymysql", specifier = ">=1.1.2" },
{ name = "python-jose", specifier = ">=3.5.0" },
@@ -769,7 +782,7 @@ requires-dist = [
{ name = "rtoml", specifier = ">=0.13.0" },
{ name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.44" },
{ name = "sqlalchemy-crud-plus", specifier = ">=1.13.1" },
{ name = "sqlparse", specifier = ">=0.5.3" },
{ name = "sqlparse", specifier = ">=0.5.4" },
{ name = "starlette-context", specifier = ">=0.4.0" },
{ name = "user-agents", specifier = ">=2.2.0" },
]
@@ -779,7 +792,7 @@ dev = [
{ name = "pytest", specifier = ">=9.0.0" },
{ name = "pytest-sugar", specifier = ">=1.1.1" },
]
lint = [{ name = "prek", specifier = ">=0.2.18" }]
lint = [{ name = "prek", specifier = ">=0.2.19" }]
server = [
{ name = "aio-pika", specifier = ">=9.5.8" },
{ name = "wait-for-it", specifier = ">=2.3.0" },
@@ -900,6 +913,18 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/c2/69/a7c4ba2ffbc7c7dbf6d8b4f5d0f0a421f7815d229f4909854266c445a3d4/gevent-25.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:bb63c0d6cb9950cc94036a4995b9cc4667b8915366613449236970f4394f94d7" },
]
[[package]]
name = "googleapis-common-protos"
version = "1.72.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038" },
]
[[package]]
name = "granian"
version = "2.6.0"
@@ -1050,6 +1075,67 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01" },
]
[[package]]
name = "grpcio"
version = "1.76.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/88/17/ff4795dc9a34b6aee6ec379f1b66438a3789cd1315aac0cbab60d92f74b3/grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4e/ff/35f9b96e3fa2f12e1dcd58a4513a2e2294a001d64dec81677361b7040c9a/grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde" },
{ url = "https://mirrors.aliyun.com/pypi/packages/3e/1c/8374990f9545e99462caacea5413ed783014b3b66ace49e35c533f07507b/grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/1e/77/36fd7d7c75a6c12542c90a6d647a27935a1ecaad03e0ffdb7c42db6b04d2/grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990" },
{ url = "https://mirrors.aliyun.com/pypi/packages/38/f7/e3cdb252492278e004722306c5a8935eae91e64ea11f0af3437a7de2e2b7/grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af" },
{ url = "https://mirrors.aliyun.com/pypi/packages/7e/20/340db7af162ccd20a0893b5f3c4a5d676af7b71105517e62279b5b61d95a/grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2" },
{ url = "https://mirrors.aliyun.com/pypi/packages/10/f0/b2160addc1487bd8fa4810857a27132fb4ce35c1b330c2f3ac45d697b106/grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2c/2c/ac6f98aa113c6ef111b3f347854e99ebb7fb9d8f7bb3af1491d438f62af4/grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/90/84/7852f7e087285e3ac17a2703bc4129fafee52d77c6c82af97d905566857e/grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/10/30/d3d2adcbb6dd3ff59d6ac3df6ef830e02b437fb5c90990429fd180e52f30/grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48" },
{ url = "https://mirrors.aliyun.com/pypi/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397" },
{ url = "https://mirrors.aliyun.com/pypi/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00" },
{ url = "https://mirrors.aliyun.com/pypi/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054" },
{ url = "https://mirrors.aliyun.com/pypi/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280" },
{ url = "https://mirrors.aliyun.com/pypi/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4" },
{ url = "https://mirrors.aliyun.com/pypi/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980" },
{ url = "https://mirrors.aliyun.com/pypi/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347" },
{ url = "https://mirrors.aliyun.com/pypi/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae" },
{ url = "https://mirrors.aliyun.com/pypi/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03" },
{ url = "https://mirrors.aliyun.com/pypi/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42" },
{ url = "https://mirrors.aliyun.com/pypi/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc" },
{ url = "https://mirrors.aliyun.com/pypi/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e" },
]
[[package]]
name = "h11"
version = "0.16.0"
@@ -1234,6 +1320,18 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea" },
]
[[package]]
name = "importlib-metadata"
version = "8.7.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "zipp" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd" },
]
[[package]]
name = "iniconfig"
version = "2.3.0"
@@ -1275,7 +1373,7 @@ wheels = [
[[package]]
name = "kombu"
version = "5.5.4"
version = "5.6.1"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "amqp" },
@@ -1283,9 +1381,9 @@ dependencies = [
{ name = "tzdata" },
{ name = "vine" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/0f/d3/5ff936d8319ac86b9c409f1501b07c426e6ad41966fedace9ef1b966e23f/kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ac/05/749ada8e51718445d915af13f1d18bc4333848e8faa0cb234028a3328ec8/kombu-5.6.1.tar.gz", hash = "sha256:90f1febb57ad4f53ca327a87598191b2520e0c793c75ea3b88d98e3b111282e4" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/ef/70/a07dcf4f62598c8ad579df241af55ced65bed76e42e45d3c368a6d82dbc1/kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/14/d6/943cf84117cd9ddecf6e1707a3f712a49fc64abdb8ac31b19132871af1dd/kombu-5.6.1-py3-none-any.whl", hash = "sha256:b69e3f5527ec32fc5196028a36376501682973e9620d6175d1c3d4eaf7e95409" },
]
[[package]]
@@ -1613,6 +1711,173 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3" },
]
[[package]]
name = "opentelemetry-api"
version = "1.39.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "importlib-metadata" },
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/c0/0b/e5428c009d4d9af0515b0a8371a8aaae695371af291f45e702f7969dce6b/opentelemetry_api-1.39.0.tar.gz", hash = "sha256:6130644268c5ac6bdffaf660ce878f10906b3e789f7e2daa5e169b047a2933b9" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/05/85/d831a9bc0a9e0e1a304ff3d12c1489a5fbc9bf6690a15dcbdae372bbca45/opentelemetry_api-1.39.0-py3-none-any.whl", hash = "sha256:3c3b3ca5c5687b1b5b37e5c5027ff68eacea8675241b29f13110a8ffbb8f0459" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-common"
version = "1.39.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "opentelemetry-proto" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/11/cb/3a29ce606b10c76d413d6edd42d25a654af03e73e50696611e757d2602f3/opentelemetry_exporter_otlp_proto_common-1.39.0.tar.gz", hash = "sha256:a135fceed1a6d767f75be65bd2845da344dd8b9258eeed6bc48509d02b184409" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/ef/c6/215edba62d13a3948c718b289539f70e40965bc37fc82ecd55bb0b749c1a/opentelemetry_exporter_otlp_proto_common-1.39.0-py3-none-any.whl", hash = "sha256:3d77be7c4bdf90f1a76666c934368b8abed730b5c6f0547a2ec57feb115849ac" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-grpc"
version = "1.39.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
{ name = "grpcio" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/7e/62/4db083ee9620da3065eeb559e9fc128f41a1d15e7c48d7c83aafbccd354c/opentelemetry_exporter_otlp_proto_grpc-1.39.0.tar.gz", hash = "sha256:7e7bb3f436006836c0e0a42ac619097746ad5553ad7128a5bd4d3e727f37fc06" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/56/e8/d420b94ffddfd8cff85bb4aa5d98da26ce7935dc3cf3eca6b83cd39ab436/opentelemetry_exporter_otlp_proto_grpc-1.39.0-py3-none-any.whl", hash = "sha256:758641278050de9bb895738f35ff8840e4a47685b7e6ef4a201fe83196ba7a05" },
]
[[package]]
name = "opentelemetry-instrumentation"
version = "0.60b0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "packaging" },
{ name = "wrapt" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/55/3c/bd53dbb42eff93d18e3047c7be11224aa9966ce98ac4cc5bfb860a32c95a/opentelemetry_instrumentation-0.60b0.tar.gz", hash = "sha256:4e9fec930f283a2677a2217754b40aaf9ef76edae40499c165bc7f1d15366a74" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/5c/7b/5b5b9f8cfe727a28553acf9cd287b1d7f706f5c0a00d6e482df55b169483/opentelemetry_instrumentation-0.60b0-py3-none-any.whl", hash = "sha256:aaafa1483543a402819f1bdfb06af721c87d60dd109501f9997332862a35c76a" },
]
[[package]]
name = "opentelemetry-instrumentation-asgi"
version = "0.60b0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b0/0a/715ea7044708d3c215385fb2a1c6ffe429aacb3cd23a348060aaeda52834/opentelemetry_instrumentation_asgi-0.60b0.tar.gz", hash = "sha256:928731218050089dca69f0fe980b8bfe109f384be8b89802d7337372ddb67b91" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/9b/8c/c6c59127fd996107243ca45669355665a7daff578ddafb86d6d2d3b01428/opentelemetry_instrumentation_asgi-0.60b0-py3-none-any.whl", hash = "sha256:9d76a541269452c718a0384478f3291feb650c5a3f29e578fdc6613ea3729cf3" },
]
[[package]]
name = "opentelemetry-instrumentation-fastapi"
version = "0.60b0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-instrumentation-asgi" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/fe/51/a021a7c929b5103fcb6bfdfa5a99abcaeb3b505faf9e3ee3ec14612c1ef9/opentelemetry_instrumentation_fastapi-0.60b0.tar.gz", hash = "sha256:5d34d67eb634a08bfe9e530680d6177521cd9da79285144e6d5a8f42683ed1b3" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/b1/5a/e238c108eb65a726d75184439377a87d532050036b54e718e4c789b26d1a/opentelemetry_instrumentation_fastapi-0.60b0-py3-none-any.whl", hash = "sha256:415c6602db01ee339276ea4cabe3e80177c9e955631c087f2ef60a75e31bfaee" },
]
[[package]]
name = "opentelemetry-instrumentation-logging"
version = "0.60b0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/5b/8b/a1aed0b695a58a1f0bdcf90fae5e468cc0fd7de2b74b9816b17e45c38e43/opentelemetry_instrumentation_logging-0.60b0.tar.gz", hash = "sha256:6d87840666669cbbcd53d2230c7a33476862d0bf7f1adc67a95519c6ccfc8281" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/00/89/6d4f5d8d03376637bff5b8d9e91356104a6f1ce9a34a2099bb2f4bf5e2b5/opentelemetry_instrumentation_logging-0.60b0-py3-none-any.whl", hash = "sha256:af75b3020911b9b6a1b4b19819a165eb131ce9bfdd313062d578fc2dc9a5cd0f" },
]
[[package]]
name = "opentelemetry-instrumentation-sqlalchemy"
version = "0.60b0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "packaging" },
{ name = "wrapt" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/8f/52/6a6e8ef8d0430487f1c65fdb81f2001d0b3b68de72f7437b54ff2330de44/opentelemetry_instrumentation_sqlalchemy-0.60b0.tar.gz", hash = "sha256:df9fdc6f48709138343971cef95a055f52d0262331115231bd5d678c1a452891" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/e0/08/943c1c650e7826b0462f0a2528d37b9cf717957955df6259b9c25dfcaa51/opentelemetry_instrumentation_sqlalchemy-0.60b0-py3-none-any.whl", hash = "sha256:ff66ac39364bfb38fe127f823b36693c6f2a44f929926863640ac1575583a73f" },
]
[[package]]
name = "opentelemetry-proto"
version = "1.39.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/48/b5/64d2f8c3393cd13ea2092106118f7b98461ba09333d40179a31444c6f176/opentelemetry_proto-1.39.0.tar.gz", hash = "sha256:c1fa48678ad1a1624258698e59be73f990b7fc1f39e73e16a9d08eef65dd838c" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/e3/4d/d500e1862beed68318705732d1976c390f4a72ca8009c4983ff627acff20/opentelemetry_proto-1.39.0-py3-none-any.whl", hash = "sha256:1e086552ac79acb501485ff0ce75533f70f3382d43d0a30728eeee594f7bf818" },
]
[[package]]
name = "opentelemetry-sdk"
version = "1.39.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/51/e3/7cd989003e7cde72e0becfe830abff0df55c69d237ee7961a541e0167833/opentelemetry_sdk-1.39.0.tar.gz", hash = "sha256:c22204f12a0529e07aa4d985f1bca9d6b0e7b29fe7f03e923548ae52e0e15dde" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/a4/b4/2adc8bc83eb1055ecb592708efb6f0c520cc2eb68970b02b0f6ecda149cf/opentelemetry_sdk-1.39.0-py3-none-any.whl", hash = "sha256:90cfb07600dfc0d2de26120cebc0c8f27e69bf77cd80ef96645232372709a514" },
]
[[package]]
name = "opentelemetry-semantic-conventions"
version = "0.60b0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/71/0e/176a7844fe4e3cb5de604212094dffaed4e18b32f1c56b5258bcbcba85c2/opentelemetry_semantic_conventions-0.60b0.tar.gz", hash = "sha256:227d7aa73cbb8a2e418029d6b6465553aa01cf7e78ec9d0bc3255c7b3ac5bf8f" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/d0/56/af0306666f91bae47db14d620775604688361f0f76a872e0005277311131/opentelemetry_semantic_conventions-0.60b0-py3-none-any.whl", hash = "sha256:069530852691136018087b52688857d97bba61cd641d0f8628d2d92788c4f78a" },
]
[[package]]
name = "opentelemetry-util-http"
version = "0.60b0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/38/0d/786a713445cf338131fef3a84fab1378e4b2ef3c3ea348eeb0c915eb804a/opentelemetry_util_http-0.60b0.tar.gz", hash = "sha256:e42b7bb49bba43b6f34390327d97e5016eb1c47949ceaf37c4795472a4e3a82d" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/53/5d/a448862f6d10c95685ed0e703596b6bd1784074e7ad90bffdc550abb7b68/opentelemetry_util_http-0.60b0-py3-none-any.whl", hash = "sha256:4f366f1a48adb74ffa6f80aee26f96882e767e01b03cd1cfb948b6e1020341fe" },
]
[[package]]
name = "packaging"
version = "25.0"
@@ -1740,28 +2005,28 @@ wheels = [
[[package]]
name = "prek"
version = "0.2.18"
version = "0.2.19"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/04/78/4bfb14e48197a90962d1d6c62194621057b6227212ef1de18965f80f2af6/prek-0.2.18.tar.gz", hash = "sha256:88d6ff5343d3613d88799ddc47c9ab22d98b4b8e2c389b535310562e11eac452" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/0f/f5/d18c5981540da731b0dbb3499718256764d4e3c939011e78b7bb3637b626/prek-0.2.19.tar.gz", hash = "sha256:2995be837a97ea9b8d2f27894b1fc763594701bafbc8d26c46f80ea9e2cb28a9" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/a9/38/fb45e4575394199c035ed03209f93e0a5b7ce7babf15ba6de1cc88aac44e/prek-0.2.18-py3-none-linux_armv6l.whl", hash = "sha256:9edd3f1e1dd1426b987b90aaf2fdebfd70061cbf2111973d22626700593fa26b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d1/17/da6f33ddd91540958af01ffb5c4d2c086766ba68026fa535720eaf3c36f2/prek-0.2.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cc723ad71b8f2345e9ce2622a7a863e63dcdbf157600de6f0b40b5e5cece04e0" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0b/78/8a7c6d8c30abd27c6043f65a692021de4e5ac3d59a21dbf295e8b079a9f5/prek-0.2.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b4833e771aab7732fcbabd41f4a613c437e1db622f3de6a389efb83cd1859cb7" },
{ url = "https://mirrors.aliyun.com/pypi/packages/97/8e/5e640218bdbc221984527859a489620b768b8481280feaf760c3a1511017/prek-0.2.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:1c532d08ebe3d5585927b40d5625780848357f06a8995d80a4c16e12c79fc1ed" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b4/eb/801f19208e77ff56dabe58c44ff4e2045c870e3af10ad66b1e03624ec674/prek-0.2.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:845f47e3d8fcc8f6b68eb90784856371b7ec681cd555cdee602d4aa0a1d98858" },
{ url = "https://mirrors.aliyun.com/pypi/packages/bd/59/f3099b7fc927192f82793ec51cf40b8bb237f07735bb7143efc3cfb431ca/prek-0.2.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af94f0e576295db9df6ba8755f49e19ed8d58f3a3b9255f5e6a5a2e1949ddb5e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a8/82/66b709652aae6ec4e949cc9ea926b02f892346be882b291d7d37ae3b034f/prek-0.2.18-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e10a8d5180b72de83ee66164dc58999651eded88274f3fb0fc51a8df90debd41" },
{ url = "https://mirrors.aliyun.com/pypi/packages/65/80/0782c0e3441a302b023e9072769ed3bf930e20cca44b5d892210735bf46b/prek-0.2.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49bd9f1ef0e2f091b401bc3207e55ecfc0a051529ca1113badf5b55fa15b8cee" },
{ url = "https://mirrors.aliyun.com/pypi/packages/06/ca/ecc58fd6ae7a7d51127cb3f2310c7c299b76702f06a5f8ae8d091e49464e/prek-0.2.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa9d62fcfcb0076813f46787a8418f92d5ed0047c16b002f4219191f3c7828b8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/30/7a/a5a1514bf5c16eeb439e0dd933ae4516604e828bacced4214e72f4bdbbf4/prek-0.2.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3255bc6ebb850ca66892b95ecf6e33cd0009baa9fa0de5aed591efb79c035a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/bb/b1/437bf874904cf550d6e0cf3c2cf6a7822e3479dc57e7edeb5c44caf16b9b/prek-0.2.18-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:2cb65df949aa974af151ffaf2543a02d00d120d3e47d0073b1173ff9f5c76242" },
{ url = "https://mirrors.aliyun.com/pypi/packages/43/d9/49e18569ec9cbe1c642ec0cfb4666dfe43039c4b622d32ed19ab28699773/prek-0.2.18-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e073cc9e09d5131b7ad04f0a4684be09bcc9cb382792c287df13b222aeeb4abf" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0e/46/6cbd9a18218bbf90680ee105f019e9d5912b66c1ff9b59bfc20a0dc2ef4c/prek-0.2.18-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:f01f92557e8bcc8f76f0354758c8903c19e4f95a49f16085f914e94c06414403" },
{ url = "https://mirrors.aliyun.com/pypi/packages/07/e0/2cc64269e7dbac0748a8995e5ee9d443d06c82735dd0a11ba266d33fc302/prek-0.2.18-py3-none-musllinux_1_1_i686.whl", hash = "sha256:4035b637b4d738da05cf08bb110376fc5f9f8d88c30ac251440c52d3242ea906" },
{ url = "https://mirrors.aliyun.com/pypi/packages/37/12/465d2b96529c45d9ce6ae281a99c5c482b8ef60f735cd0302002849ab9c7/prek-0.2.18-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:818571df61182491e3f08b5928f37d0325cad1f343aad9c276a482a3b9c4563a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/31/75/520db7fc842df383fee043f72d67fe38042495ab1ec6f644d751945ddbad/prek-0.2.18-py3-none-win32.whl", hash = "sha256:1988c5957087275008a5fa9b8d0f4e2461d64d8f54a6c59bd0a14dfee310fefc" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ca/0c/16e91a5547b78f006ab0b9b4ef1508ac43344d21db9e92a41bbd08d48208/prek-0.2.18-py3-none-win_amd64.whl", hash = "sha256:64eddbb515723785c129c2ebe6a778358b8db27650879bf82fbd683eac08f504" },
{ url = "https://mirrors.aliyun.com/pypi/packages/70/4f/76662876895476fd0bfc8a64d265226c24615c0d973d602d416e99cb20c4/prek-0.2.18-py3-none-win_arm64.whl", hash = "sha256:cb541a8a87b50ea9562e909f51f86b2fb77a6d21df6985237ecfe50743bd9f6f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ef/ab/b23d83992e76738c3aeb254739e554f32d11be58121488b124918e461200/prek-0.2.19-py3-none-linux_armv6l.whl", hash = "sha256:8879bd9df431bfa689cd38633e324b76d6579cf1978f186849c75c9d7c0dc6b4" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9f/4b/d2289899e3d05e825018e9e6b6d2b5e95e1b877f9b673cda352555b2b778/prek-0.2.19-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b57000eb18f5621c55ef255da8953aaaa316b013714e05709c76accc24487bf" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e4/9a/198336704e8faf86293626d39657136cb42bed7cd046a6fd183c7497e5ec/prek-0.2.19-py3-none-macosx_11_0_arm64.whl", hash = "sha256:598cad01b68fae2732ff552c93e8286071e58796c7f3c461c298ad8618d984ab" },
{ url = "https://mirrors.aliyun.com/pypi/packages/12/e6/d0f87337413a79eff029d1eb6a2d2b66d07f8bd59edbd30220d195654ad5/prek-0.2.19-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:dd8e5f36022fcbefbc9750e3f5067a01102b1c0808325489f5614bc012be1136" },
{ url = "https://mirrors.aliyun.com/pypi/packages/3d/b5/e4f4f0acc8ad1fd967494b962bfabc821807b52a7c68d2bae30c65b588c4/prek-0.2.19-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:721271a31f51c8c7994d24e5e3816a249c482b56fe287ec31e96f47479432201" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c8/24/faa519d2b8b9ceba6dd43c3e993ebacfef33ddc4be2b781beef24b9ad79e/prek-0.2.19-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:116bd9152c68be9a77b40af15763ddba73d4744dd606fe6686c3d2e9eb7b52b1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d4/19/2b92c8fbfe9872a233fc326cb969348dd2fc8db0d01323cf822b5c2990ff/prek-0.2.19-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:96aef4dfcf8c3c28d93ee93f78289e07a3ce60e1f2f91d3eb9f023dc75b62216" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ed/8e/934b5dff72aaf6df41ae59bf5324f2c3db8a123cb976387e85c16cc639b2/prek-0.2.19-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a1156a1f525d3f6b69bc1c07a0dea930cb9027f95bcb4d390fe5fe7fe88687e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/89/ba/990decf00161eed117fb7c71a9f2cba71879e77bf7a6e17d94659f72cd4e/prek-0.2.19-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:485b152f4a637aa761ab1634a6b9a38cd07cc6d23e3651f2c8eb4d7a507e132c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/bf/57/b7b87ee878c45046cb220ecdd257ea0398984b104f983b7eb25a92fe86e2/prek-0.2.19-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7613493f39918247f2d97ebce8b3692fc738aa2ab8a9f76cfa800947756cfa42" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4b/50/33b8cd6fe3517be2a4303a72b20341a54b728e9fc0027e0cc65843210ee9/prek-0.2.19-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a1546907f28b56c940f7d3cd9452052bcd97be31de92bf0a4bd07c4746929a00" },
{ url = "https://mirrors.aliyun.com/pypi/packages/db/8d/06b49a443a43a0fbf576e0d068ce4cce2a76367b19f42552f6182c3d06a0/prek-0.2.19-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7db50ebce756e6d67c4ed1935c6e1bcacf29662c9bfee4a0f401d3fb0385d922" },
{ url = "https://mirrors.aliyun.com/pypi/packages/18/d9/1e317d0697a98b2c57e2b4c1f8d3efeb6db86b81ce2df5211fdbb17f8b7c/prek-0.2.19-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:37f03e0796d753a9bd82ed09a7e901474e6a26b9a6ceffa5003a3fa703497a8e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0c/f0/0e32a9a7fc7876d08f5b118ecab6e81c5b433ddaa4daaa6c930c46c7d916/prek-0.2.19-py3-none-musllinux_1_1_i686.whl", hash = "sha256:3b0da9f14191e0adfb55e626ce3562d802800bc9aea96bbbdc1374290ef848c9" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e0/61/70204d48dd86a472d56dc75d2d9caf8236507a563428cb22fe5c90933978/prek-0.2.19-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:9cef90e8f8d5de9a165f2a81d9ce728bd3b99b65620a8427928e832a43fe4c4b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/14/5c/13361ca9aa8ce3bce50430b5f2afb2ea9e44639841a638f998c762c27708/prek-0.2.19-py3-none-win32.whl", hash = "sha256:a6d19d8b10bc4aba18cf1a7902de7bede05671f484adabeac2d96a02ac470ef1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c9/d5/bf010982c063ed630bf75c058e2313ba546daa11343fa2ba3116a19f5b69/prek-0.2.19-py3-none-win_amd64.whl", hash = "sha256:34a638a2cb9808b3db8b7020c9cf03d445e9c957a36e09a443a19e1e71a2fb6c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/06/e3/42a5fb0f0dc2d0ae117e533e9da8a0fb41850526b27dd5dd5c0f37e878fa/prek-0.2.19-py3-none-win_arm64.whl", hash = "sha256:1a865880cc2362eb0698d938b811a796dfd5bcb49ef7abd02568f0f5e3b7ec88" },
]
[[package]]
@@ -1899,6 +2164,21 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237" },
]
[[package]]
name = "protobuf"
version = "6.33.2"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/34/44/e49ecff446afeec9d1a66d6bbf9adc21e3c7cea7803a920ca3773379d4f6/protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/bc/91/1e3a34881a88697a7354ffd177e8746e97a722e5e8db101544b47e84afb1/protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/64/20/4d50191997e917ae13ad0a235c8b42d8c1ab9c3e6fd455ca16d416944355/protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43" },
{ url = "https://mirrors.aliyun.com/pypi/packages/7d/4f/f743761e41d3b2b2566748eb76bbff2b43e14d5fcab694f494a16458b05f/protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b1/fa/26468d00a92824020f6f2090d827078c09c9c587e34cbfd2d0c7911221f8/protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872" },
{ url = "https://mirrors.aliyun.com/pypi/packages/56/13/333b8f421738f149d4fe5e49553bc2a2ab75235486259f689b4b91f96cec/protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c" },
]
[[package]]
name = "psutil"
version = "7.1.3"
@@ -2024,7 +2304,7 @@ wheels = [
[[package]]
name = "pydantic"
version = "2.12.4"
version = "2.12.5"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "annotated-types" },
@@ -2032,9 +2312,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d" },
]
[[package]]
@@ -2392,16 +2672,16 @@ wheels = [
[[package]]
name = "rich-toolkit"
version = "0.16.0"
version = "0.17.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "click" },
{ name = "rich" },
{ name = "typing-extensions" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/83/8e/ab512afd71d4e67bb611a57db92a0e967304c97ec61963e99103f5a88069/rich_toolkit-0.16.0.tar.gz", hash = "sha256:2f554b00b194776639f4d80f2706980756b659ceed9f345ebbd9de77d1bdd0f4" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ad/d0/8f8de36e1abf8339b497ce700dd7251ca465ffca4a1976969b0eaeb596fb/rich_toolkit-0.17.0.tar.gz", hash = "sha256:17ca7a32e613001aa0945ddea27a246f6de01dfc4c12403254c057a8ee542977" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/3a/04/f4bfb5d8a258d395d7fb6fbaa0e3fe7bafae17a2a3e2387e6dea9d6474df/rich_toolkit-0.16.0-py3-none-any.whl", hash = "sha256:3f4307f678c5c1e22c36d89ac05f1cd145ed7174f19c1ce5a4d3664ba77c0f9e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b2/42/ef2ed40699567661d03b0b511ac46cf6cee736de8f3666819c12d6d20696/rich_toolkit-0.17.0-py3-none-any.whl", hash = "sha256:06fb47a5c5259d6b480287cd38aff5f551b6e1a307f90ed592453dd360e4e71e" },
]
[[package]]
@@ -2518,15 +2798,6 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2" },
]
[[package]]
name = "sqlalchemy"
version = "2.0.44"
@@ -2592,11 +2863,11 @@ wheels = [
[[package]]
name = "sqlparse"
version = "0.5.3"
version = "0.5.4"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/18/67/701f86b28d63b2086de47c942eccf8ca2208b3be69715a1119a4e384415a/sqlparse-0.5.4.tar.gz", hash = "sha256:4396a7d3cf1cd679c1be976cf3dc6e0a51d0111e87787e7a8d780e7d5a998f9e" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca" },
{ url = "https://mirrors.aliyun.com/pypi/packages/25/70/001ee337f7aa888fb2e3f5fd7592a6afc5283adb1ed44ce8df5764070f22/sqlparse-0.5.4-py3-none-any.whl", hash = "sha256:99a9f0314977b76d776a0fcb8554de91b9bb8a18560631d6bc48721d07023dcb" },
]
[[package]]
@@ -2758,6 +3029,18 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8" },
]
[[package]]
name = "tzlocal"
version = "5.3.1"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d" },
]
[[package]]
name = "ua-parser"
version = "1.0.1"
@@ -3069,6 +3352,75 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390" },
]
[[package]]
name = "wrapt"
version = "1.17.3"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04" },
{ url = "https://mirrors.aliyun.com/pypi/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05" },
{ url = "https://mirrors.aliyun.com/pypi/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18" },
{ url = "https://mirrors.aliyun.com/pypi/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85" },
{ url = "https://mirrors.aliyun.com/pypi/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311" },
{ url = "https://mirrors.aliyun.com/pypi/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5" },
{ url = "https://mirrors.aliyun.com/pypi/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2" },
{ url = "https://mirrors.aliyun.com/pypi/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89" },
{ url = "https://mirrors.aliyun.com/pypi/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0" },
{ url = "https://mirrors.aliyun.com/pypi/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba" },
{ url = "https://mirrors.aliyun.com/pypi/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396" },
{ url = "https://mirrors.aliyun.com/pypi/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe" },
{ url = "https://mirrors.aliyun.com/pypi/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77" },
{ url = "https://mirrors.aliyun.com/pypi/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277" },
{ url = "https://mirrors.aliyun.com/pypi/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa" },
{ url = "https://mirrors.aliyun.com/pypi/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb" },
{ url = "https://mirrors.aliyun.com/pypi/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16" },
{ url = "https://mirrors.aliyun.com/pypi/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa" },
{ url = "https://mirrors.aliyun.com/pypi/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10" },
{ url = "https://mirrors.aliyun.com/pypi/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58" },
{ url = "https://mirrors.aliyun.com/pypi/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454" },
{ url = "https://mirrors.aliyun.com/pypi/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056" },
{ url = "https://mirrors.aliyun.com/pypi/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804" },
{ url = "https://mirrors.aliyun.com/pypi/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977" },
{ url = "https://mirrors.aliyun.com/pypi/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116" },
{ url = "https://mirrors.aliyun.com/pypi/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22" },
]
[[package]]
name = "wsproto"
version = "1.3.2"
@@ -3207,6 +3559,15 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff" },
]
[[package]]
name = "zipp"
version = "3.23.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e" },
]
[[package]]
name = "zope-event"
version = "6.1"