Compare commits

..
11 Commits
Author SHA1 Message Date
Wu Clan 5f8ae74ea5 Fix list query in the dict plugin (#582) 2025-04-18 23:16:03 +08:00
Wu Clan da8e7c1b01 Fix the plugin system route injection (#581) 2025-04-18 23:09:25 +08:00
Wu Clan f513b98f15 Update the changelog for v1.1.0 (#580) 2025-04-17 17:54:52 +08:00
Wu Clan 80cef6dde7 Update code generator file and table naming (#579)
* Update code generator file and table naming

* Update column table comment
2025-04-17 17:47:20 +08:00
Huyuwei 2d504f281e Fix avatar url type of update avatar (#575) 2025-04-17 15:02:19 +08:00
Wu Clan fc34c98f0a Update the code generator to plugin (#578)
* Update the code generator to plugin

* Fix get all tables return type
2025-04-17 11:06:43 +08:00
Wu Clan 1cc020efdd Update unique columns in dict models (#577) 2025-04-17 11:06:29 +08:00
Wu Clan bb33e99e33 Update casbin RBAC verify to dynamic import (#576) 2025-04-17 11:06:15 +08:00
Wu Clan 6c70b34ae6 Optimize the file structure of code generator (#574) 2025-04-15 19:54:18 +08:00
Wu Clan 960d9f6215 Update the default value for some functions (#573)
* Update the default value for some functions

* Fix lint
2025-04-14 14:22:05 +08:00
Wu Clan 085f08ec3b Update the changelog for v1.0.5 (#572) 2025-04-09 11:55:49 +08:00
72 changed files with 652 additions and 577 deletions
+45
View File
@@ -1,3 +1,46 @@
<a id="v1.1.0"></a>
# [v1.1.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.1.0) - 2025-04-17
## What's Changed
* Update the changelog for v1.0.5 by [@wu-clan](https://github.com/wu-clan) in [#572](https://github.com/fastapi-practices/fastapi_best_architecture/pull/572)
* Update the default value for some functions by [@wu-clan](https://github.com/wu-clan) in [#573](https://github.com/fastapi-practices/fastapi_best_architecture/pull/573)
* Optimize the file structure of code generator by [@wu-clan](https://github.com/wu-clan) in [#574](https://github.com/fastapi-practices/fastapi_best_architecture/pull/574)
* Update casbin RBAC verify to dynamic import by [@wu-clan](https://github.com/wu-clan) in [#576](https://github.com/fastapi-practices/fastapi_best_architecture/pull/576)
* Update unique columns in dict models by [@wu-clan](https://github.com/wu-clan) in [#577](https://github.com/fastapi-practices/fastapi_best_architecture/pull/577)
* Update the code generator to plugin by [@wu-clan](https://github.com/wu-clan) in [#578](https://github.com/fastapi-practices/fastapi_best_architecture/pull/578)
* Fix avatar url type of update avatar by [@huyuwei1996](https://github.com/huyuwei1996) in [#575](https://github.com/fastapi-practices/fastapi_best_architecture/pull/575)
* Update code generator file and table naming by [@wu-clan](https://github.com/wu-clan) in [#579](https://github.com/fastapi-practices/fastapi_best_architecture/pull/579)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.5...v1.1.0
[Changes][v1.1.0]
<a id="v1.0.5"></a>
# [v1.0.5](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.5) - 2025-04-09
## What's Changed
* Update the changelog for v1.0.4 by [@wu-clan](https://github.com/wu-clan) in [#558](https://github.com/fastapi-practices/fastapi_best_architecture/pull/558)
* Bump dependencies and pre-commits by [@wu-clan](https://github.com/wu-clan) in [#559](https://github.com/fastapi-practices/fastapi_best_architecture/pull/559)
* Add python 3.13 to GitHub ci by [@wu-clan](https://github.com/wu-clan) in [#560](https://github.com/fastapi-practices/fastapi_best_architecture/pull/560)
* Update the system config to plugin by [@wu-clan](https://github.com/wu-clan) in [#561](https://github.com/fastapi-practices/fastapi_best_architecture/pull/561)
* Update dict data and type to plugin by [@wu-clan](https://github.com/wu-clan) in [#562](https://github.com/fastapi-practices/fastapi_best_architecture/pull/562)
* Update menu and add vben5 compatibility by [@wu-clan](https://github.com/wu-clan) in [#563](https://github.com/fastapi-practices/fastapi_best_architecture/pull/563)
* Update the vben5 tree data structure by [@wu-clan](https://github.com/wu-clan) in [#564](https://github.com/fastapi-practices/fastapi_best_architecture/pull/564)
* Update custom validation error messages by [@wu-clan](https://github.com/wu-clan) in [#566](https://github.com/fastapi-practices/fastapi_best_architecture/pull/566)
* Update the number of pagination le by [@wu-clan](https://github.com/wu-clan) in [#565](https://github.com/fastapi-practices/fastapi_best_architecture/pull/565)
* Fix the login password verification by [@wu-clan](https://github.com/wu-clan) in [#568](https://github.com/fastapi-practices/fastapi_best_architecture/pull/568)
* Fix the failure hook of celery task by [@wu-clan](https://github.com/wu-clan) in [#569](https://github.com/fastapi-practices/fastapi_best_architecture/pull/569)
* Bump fastapi oauth2 from 0.0.1a2 to 0.0.1 by [@wu-clan](https://github.com/wu-clan) in [#570](https://github.com/fastapi-practices/fastapi_best_architecture/pull/570)
* Fix the log rule in gitignore by [@wu-clan](https://github.com/wu-clan) in [#571](https://github.com/fastapi-practices/fastapi_best_architecture/pull/571)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.4...v1.0.5
[Changes][v1.0.5]
<a id="v1.0.4"></a>
# [v1.0.4](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.4) - 2025-03-28
@@ -403,6 +446,8 @@
[Changes][v1.0.0]
[v1.1.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.5...v1.1.0
[v1.0.5]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.4...v1.0.5
[v1.0.4]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.3...v1.0.4
[v1.0.3]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.2...v1.0.3
[v1.0.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.1...v1.0.2
+1 -1
View File
@@ -20,7 +20,7 @@ from backend.plugin.tools import get_plugin_models
# import your new model here
from backend.app.admin.model import * # noqa: F401
from backend.app.generator.model import * # noqa: F401
from backend.plugin.code_generator.model import * # noqa: F401
# import plugin model
for cls in get_plugin_models():
+1 -1
View File
@@ -24,7 +24,7 @@ class CRUDDataRule(CRUDPlus[DataRule]):
"""
return await self.select_model(db, pk)
async def get_list(self, name: str | None = None) -> Select:
async def get_list(self, name: str | None) -> Select:
"""
获取规则列表
+1 -6
View File
@@ -35,12 +35,7 @@ class CRUDDept(CRUDPlus[Dept]):
return await self.select_model_by_column(db, name=name, del_flag=0)
async def get_all(
self,
db: AsyncSession,
name: str | None = None,
leader: str | None = None,
phone: str | None = None,
status: int | None = None,
self, db: AsyncSession, name: str | None, leader: str | None, phone: str | None, status: int | None
) -> Sequence[Dept]:
"""
获取所有部门
+1 -1
View File
@@ -11,7 +11,7 @@ from backend.app.admin.schema.login_log import CreateLoginLogParam
class CRUDLoginLog(CRUDPlus[LoginLog]):
"""登录日志数据库操作类"""
async def get_list(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
async def get_list(self, username: str | None, status: int | None, ip: str | None) -> Select:
"""
获取登录日志列表
+1 -1
View File
@@ -34,7 +34,7 @@ class CRUDMenu(CRUDPlus[Menu]):
"""
return await self.select_model_by_column(db, title=title, menu_type__ne=2)
async def get_all(self, db: AsyncSession, title: str | None = None, status: int | None = None) -> Sequence[Menu]:
async def get_all(self, db: AsyncSession, title: str | None, status: int | None) -> Sequence[Menu]:
"""
获取菜单列表
+1 -1
View File
@@ -11,7 +11,7 @@ from backend.app.admin.schema.opera_log import CreateOperaLogParam
class CRUDOperaLogDao(CRUDPlus[OperaLog]):
"""操作日志数据库操作类"""
async def get_list(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
async def get_list(self, username: str | None, status: int | None, ip: str | None) -> Select:
"""
获取操作日志列表
+1 -1
View File
@@ -66,7 +66,7 @@ class CRUDRole(CRUDPlus[Role]):
roles = await db.execute(stmt)
return roles.scalars().all()
async def get_list(self, name: str | None = None, status: int | None = None) -> Select:
async def get_list(self, name: str | None, status: int | None) -> Select:
"""
获取角色列表
+2 -4
View File
@@ -142,7 +142,7 @@ class CRUDUser(CRUDPlus[User]):
:param avatar: 头像地址
:return:
"""
return await self.update_model(db, input_user, {'avatar': avatar.url})
return await self.update_model(db, input_user, {'avatar': str(avatar.url)})
async def delete(self, db: AsyncSession, user_id: int) -> int:
"""
@@ -175,9 +175,7 @@ class CRUDUser(CRUDPlus[User]):
"""
return await self.update_model(db, pk, {'password': new_pwd})
async def get_list(
self, dept: int | None = None, username: str | None = None, phone: str | None = None, status: int | None = None
) -> Select:
async def get_list(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select:
"""
获取用户列表
@@ -70,7 +70,7 @@ class DataRuleService:
return model_columns
@staticmethod
async def get_select(*, name: str | None = None) -> Select:
async def get_select(*, name: str | None) -> Select:
"""
获取数据规则列表查询条件
+1 -1
View File
@@ -33,7 +33,7 @@ class DeptService:
@staticmethod
async def get_dept_tree(
*, name: str | None = None, leader: str | None = None, phone: str | None = None, status: int | None = None
*, name: str | None, leader: str | None, phone: str | None, status: int | None
) -> list[dict[str, Any]]:
"""
获取部门树形结构
@@ -16,7 +16,7 @@ class LoginLogService:
"""登录日志服务类"""
@staticmethod
async def get_select(*, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
async def get_select(*, username: str | None, status: int | None, ip: str | None) -> Select:
"""
获取登录日志列表查询条件
+1 -1
View File
@@ -33,7 +33,7 @@ class MenuService:
return menu
@staticmethod
async def get_menu_tree(*, title: str | None = None, status: int | None = None) -> list[dict[str, Any]]:
async def get_menu_tree(*, title: str | None, status: int | None) -> list[dict[str, Any]]:
"""
获取菜单树形结构
@@ -11,7 +11,7 @@ class OperaLogService:
"""操作日志服务类"""
@staticmethod
async def get_select(*, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
async def get_select(*, username: str | None, status: int | None, ip: str | None) -> Select:
"""
获取操作日志列表查询条件
+1 -1
View File
@@ -58,7 +58,7 @@ class RoleService:
return roles
@staticmethod
async def get_select(*, name: str | None = None, status: int | None = None) -> Select:
async def get_select(*, name: str | None, status: int | None) -> Select:
"""
获取角色列表查询条件
+1 -1
View File
@@ -200,7 +200,7 @@ class UserService:
return count
@staticmethod
async def get_select(*, dept: int, username: str = None, phone: str = None, status: int = None) -> Select:
async def get_select(*, dept: int, username: str, phone: str, status: int) -> Select:
"""
获取用户列表查询条件
-14
View File
@@ -1,14 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.generator.api.v1.gen import router as gen_router
from backend.app.generator.api.v1.gen_business import router as gen_business_router
from backend.app.generator.api.v1.gen_model import router as gen_model_router
from backend.core.conf import settings
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/gen', tags=['代码生成'])
v1.include_router(gen_router)
v1.include_router(gen_business_router, prefix='/businesses')
v1.include_router(gen_model_router, prefix='/models')
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.app.generator.model.gen_business import GenBusiness
from backend.app.generator.model.gen_model import GenModel
-2
View File
@@ -3,11 +3,9 @@
from fastapi import APIRouter
from backend.app.admin.api.router import v1 as admin_v1
from backend.app.generator.api.router import v1 as generator_v1
from backend.app.task.api.router import v1 as task_v1
router = APIRouter()
router.include_router(admin_v1)
router.include_router(generator_v1)
router.include_router(task_v1)
-150
View File
@@ -119,153 +119,3 @@ class FileType(StrEnum):
image = 'image'
video = 'video'
class GenModelMySQLColumnType(StrEnum):
"""代码生成模型列类型(MySQL"""
# Python 类型映射
BIGINT = 'int'
BigInteger = 'int' # BIGINT
BINARY = 'bytes'
BLOB = 'bytes'
BOOLEAN = 'bool' # BOOL
Boolean = 'bool' # BOOL
CHAR = 'str'
CLOB = 'str'
DATE = 'date'
Date = 'date' # DATE
DATETIME = 'datetime'
DateTime = 'datetime' # DATETIME
DECIMAL = 'Decimal'
DOUBLE = 'float'
Double = 'float' # DOUBLE
DOUBLE_PRECISION = 'float'
Enum = 'Enum' # Enum()
FLOAT = 'float'
Float = 'float' # FLOAT
INT = 'int' # INTEGER
INTEGER = 'int'
Integer = 'int' # INTEGER
Interval = 'timedelta' # DATETIME
JSON = 'dict'
LargeBinary = 'bytes' # BLOB
NCHAR = 'str'
NUMERIC = 'Decimal'
Numeric = 'Decimal' # NUMERIC
NVARCHAR = 'str' # String
PickleType = 'bytes' # BLOB
REAL = 'float'
SMALLINT = 'int'
SmallInteger = 'int' # SMALLINT
String = 'str' # String
TEXT = 'str'
Text = 'str' # TEXT
TIME = 'time'
Time = 'time' # TIME
TIMESTAMP = 'datetime'
Unicode = 'str' # String
UnicodeText = 'str' # TEXT
UUID = 'str | UUID'
Uuid = 'str' # CHAR(32)
VARBINARY = 'bytes'
VARCHAR = 'str' # String
# sa.dialects.mysql 导入
BIT = 'bool'
ENUM = 'Enum'
LONGBLOB = 'bytes'
LONGTEXT = 'str'
MEDIUMBLOB = 'bytes'
MEDIUMINT = 'int'
MEDIUMTEXT = 'str'
SET = 'list[str]'
TINYBLOB = 'bytes'
TINYINT = 'int'
TINYTEXT = 'str'
YEAR = 'int'
class GenModelPostgreSQLColumnType(StrEnum):
"""代码生成模型列类型(PostgreSQL)"""
# Python 类型映射
BIGINT = 'int'
BigInteger = 'int' # BIGINT
BINARY = 'bytes'
BLOB = 'bytes'
BOOLEAN = 'bool'
Boolean = 'bool' # BOOLEAN
CHAR = 'str'
CLOB = 'str'
DATE = 'date'
Date = 'date' # DATE
DATETIME = 'datetime'
DateTime = 'datetime' # TIMESTAMP WITHOUT TIME ZONE
DECIMAL = 'Decimal'
DOUBLE = 'float'
Double = 'float' # DOUBLE PRECISION
DOUBLE_PRECISION = 'float' # DOUBLE PRECISION
Enum = 'Enum' # Enum(name='enum')
FLOAT = 'float'
Float = 'float' # FLOAT
INT = 'int' # INTEGER
INTEGER = 'int'
Integer = 'int' # INTEGER
Interval = 'timedelta' # INTERVAL
JSON = 'dict'
LargeBinary = 'bytes' # BYTEA
NCHAR = 'str'
NUMERIC = 'Decimal'
Numeric = 'Decimal' # NUMERIC
NVARCHAR = 'str' # String
PickleType = 'bytes' # BYTEA
REAL = 'float'
SMALLINT = 'int'
SmallInteger = 'int' # SMALLINT
String = 'str' # String
TEXT = 'str'
Text = 'str' # TEXT
TIME = 'time' # TIME WITHOUT TIME ZONE
Time = 'time' # TIME WITHOUT TIME ZONE
TIMESTAMP = 'datetime' # TIMESTAMP WITHOUT TIME ZONE
Unicode = 'str' # String
UnicodeText = 'str' # TEXT
UUID = 'str | UUID'
Uuid = 'str'
VARBINARY = 'bytes'
VARCHAR = 'str' # String
# sa.dialects.postgresql 导入
ARRAY = 'list'
BIT = 'bool'
BYTEA = 'bytes'
CIDR = 'str'
CITEXT = 'str'
DATEMULTIRANGE = 'list[date]'
DATERANGE = 'tuple[date, date]'
DOMAIN = 'str'
ENUM = 'Enum'
HSTORE = 'dict'
INET = 'str'
INT4MULTIRANGE = 'list[int]'
INT4RANGE = 'tuple[int, int]'
INT8MULTIRANGE = 'list[int]'
INT8RANGE = 'tuple[int, int]'
INTERVAL = 'timedelta'
JSONB = 'dict'
JSONPATH = 'str'
MACADDR = 'str'
MACADDR8 = 'str'
MONEY = 'Decimal'
NUMMULTIRANGE = 'list[Decimal]'
NUMRANGE = 'tuple[Decimal, Decimal]'
OID = 'int'
REGCLASS = 'str'
REGCONFIG = 'str'
TSMULTIRANGE = 'list[datetime]'
TSQUERY = 'str'
TSRANGE = 'tuple[datetime, datetime]'
TSTZMULTIRANGE = 'list[datetime]'
TSTZRANGE = 'tuple[datetime, datetime]'
TSVECTOR = 'str'
+3 -1
View File
@@ -68,7 +68,9 @@ class ResponseBase:
@staticmethod
def __response(
*, res: CustomResponseCode | CustomResponse = None, data: Any | None = None
*,
res: CustomResponseCode | CustomResponse,
data: Any | None,
) -> ResponseModel | ResponseSchemaModel:
"""
请求返回通用方法
+10 -1
View File
@@ -3,10 +3,12 @@
from fastapi import Depends, Request
from backend.common.enums import MethodType, StatusType
from backend.common.exception import errors
from backend.common.exception.errors import AuthorizationError, TokenError
from backend.common.log import log
from backend.common.security.jwt import DependsJwtAuth
from backend.core.conf import settings
from backend.plugin.casbin.utils.rbac import casbin_verify
from backend.utils.import_parse import import_module_cached
async def rbac_verify(request: Request, _token: str = DependsJwtAuth) -> None:
@@ -67,6 +69,13 @@ async def rbac_verify(request: Request, _token: str = DependsJwtAuth) -> None:
if path_auth_perm not in allow_perms:
raise AuthorizationError
else:
try:
casbin_rbac = import_module_cached('backend.plugin.casbin.utils.rbac')
casbin_verify = getattr(casbin_rbac, 'casbin_verify')
except (ImportError, AttributeError) as e:
log.error(f'正在通过 casbin 执行 RBAC 权限校验,但此插件不存在: {e}')
raise errors.ServerError(msg='权限校验失败,请联系系统管理员')
await casbin_verify(request)
-3
View File
@@ -17,9 +17,6 @@ STATIC_DIR = BASE_PATH / 'static'
# 上传文件目录
UPLOAD_DIR = STATIC_DIR / 'upload'
# jinja2 模版文件路径
JINJA2_TEMPLATE_DIR = BASE_PATH / 'templates'
# 插件目录
PLUGIN_DIR = BASE_PATH / 'plugin'
+3 -7
View File
@@ -23,7 +23,7 @@ from backend.database.redis import redis_client
from backend.middleware.jwt_auth_middleware import JwtAuthMiddleware
from backend.middleware.opera_log_middleware import OperaLogMiddleware
from backend.middleware.state_middleware import StateMiddleware
from backend.plugin.tools import plugin_router_inject
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
@@ -157,12 +157,8 @@ def register_router(app: FastAPI) -> None:
"""
dependencies = [Depends(demo_site)] if settings.DEMO_MODE else None
# 插件路由
plugin_router_inject()
# 系统路由(必须在插件路由注入后导入)
from backend.app.router import router
# API
router = build_final_router()
app.include_router(router, dependencies=dependencies)
# Extra
@@ -34,7 +34,7 @@ class CasbinService:
return await casbin_dao.get_list(ptype, sub)
@staticmethod
async def get_policy_list(*, role: int | None = None) -> list:
async def get_policy_list(*, role: int | None) -> list:
"""
获取 P 策略列表
@@ -0,0 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.core.conf import settings
from backend.plugin.code_generator.api.v1.business import router as business_router
from backend.plugin.code_generator.api.v1.column import router as model_router
from backend.plugin.code_generator.api.v1.gen import router as gen_router
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/gen', tags=['代码生成'])
v1.include_router(gen_router)
v1.include_router(business_router, prefix='/businesses')
v1.include_router(model_router, prefix='/models')
@@ -4,18 +4,18 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path
from backend.app.generator.schema.gen_business import (
CreateGenBusinessParam,
GetGenBusinessDetail,
UpdateGenBusinessParam,
)
from backend.app.generator.schema.gen_model import GetGenModelDetail
from backend.app.generator.service.gen_business_service import gen_business_service
from backend.app.generator.service.gen_model_service import gen_model_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.rbac import DependsRBAC
from backend.plugin.code_generator.schema.business import (
CreateGenBusinessParam,
GetGenBusinessDetail,
UpdateGenBusinessParam,
)
from backend.plugin.code_generator.schema.column import GetGenModelDetail
from backend.plugin.code_generator.service.business_service import gen_business_service
from backend.plugin.code_generator.service.column_service import gen_model_service
router = APIRouter()
@@ -4,12 +4,12 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path
from backend.app.generator.schema.gen_model import CreateGenModelParam, GetGenModelDetail, UpdateGenModelParam
from backend.app.generator.service.gen_model_service import gen_model_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.rbac import DependsRBAC
from backend.plugin.code_generator.schema.column import CreateGenModelParam, GetGenModelDetail, UpdateGenModelParam
from backend.plugin.code_generator.service.column_service import gen_model_service
router = APIRouter()
@@ -5,13 +5,13 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
from fastapi.responses import StreamingResponse
from backend.app.generator.conf import generator_settings
from backend.app.generator.schema.gen import ImportParam
from backend.app.generator.service.gen_service import gen_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.rbac import DependsRBAC
from backend.plugin.code_generator.conf import generator_settings
from backend.plugin.code_generator.schema.gen import ImportParam
from backend.plugin.code_generator.service.gen_service import gen_service
router = APIRouter()
@@ -8,9 +8,6 @@ from pydantic_settings import BaseSettings
class GeneratorSettings(BaseSettings):
"""代码生成配置"""
# 模版
TEMPLATE_BACKEND_DIR_NAME: str = 'py'
# 代码下载
DOWNLOAD_ZIP_FILENAME: str = 'fba_generator'
@@ -5,8 +5,8 @@ from typing import Sequence
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.generator.model import GenBusiness
from backend.app.generator.schema.gen_business import CreateGenBusinessParam, UpdateGenBusinessParam
from backend.plugin.code_generator.model import GenBusiness
from backend.plugin.code_generator.schema.business import CreateGenBusinessParam, UpdateGenBusinessParam
class CRUDGenBusiness(CRUDPlus[GenBusiness]):
@@ -5,14 +5,14 @@ from typing import Sequence
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.generator.model import GenModel
from backend.app.generator.schema.gen_model import CreateGenModelParam, UpdateGenModelParam
from backend.plugin.code_generator.model import GenColumn
from backend.plugin.code_generator.schema.column import CreateGenModelParam, UpdateGenModelParam
class CRUDGenModel(CRUDPlus[GenModel]):
class CRUDGenModel(CRUDPlus[GenColumn]):
"""代码生成模型 CRUD 类"""
async def get(self, db: AsyncSession, pk: int) -> GenModel | None:
async def get(self, db: AsyncSession, pk: int) -> GenColumn | None:
"""
获取代码生成模型列
@@ -22,7 +22,7 @@ class CRUDGenModel(CRUDPlus[GenModel]):
"""
return await self.select_model(db, pk)
async def get_all_by_business(self, db: AsyncSession, business_id: int) -> Sequence[GenModel]:
async def get_all_by_business(self, db: AsyncSession, business_id: int) -> Sequence[GenColumn]:
"""
获取所有代码生成模型列
@@ -32,7 +32,7 @@ class CRUDGenModel(CRUDPlus[GenModel]):
"""
return await self.select_models_order(db, sort_columns='sort', gen_business_id=business_id)
async def create(self, db: AsyncSession, obj: CreateGenModelParam, pd_type: str | None = None) -> None:
async def create(self, db: AsyncSession, obj: CreateGenModelParam, pd_type: str | None) -> None:
"""
创建代码生成模型
@@ -43,7 +43,7 @@ class CRUDGenModel(CRUDPlus[GenModel]):
"""
await self.create_model(db, obj, pd_type=pd_type)
async def update(self, db: AsyncSession, pk: int, obj: UpdateGenModelParam, pd_type: str | None = None) -> int:
async def update(self, db: AsyncSession, pk: int, obj: UpdateGenModelParam, pd_type: str | None) -> int:
"""
更新代码生成模型
@@ -66,4 +66,4 @@ class CRUDGenModel(CRUDPlus[GenModel]):
return await self.delete_model(db, pk)
gen_model_dao: CRUDGenModel = CRUDGenModel(GenModel)
gen_model_dao: CRUDGenModel = CRUDGenModel(GenColumn)
@@ -12,7 +12,7 @@ class CRUDGen:
"""代码生成 CRUD 类"""
@staticmethod
async def get_all_tables(db: AsyncSession, table_schema: str) -> list[str]:
async def get_all_tables(db: AsyncSession, table_schema: str) -> Sequence[str]:
"""
获取所有表名
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.common.enums import StrEnum
class GenModelMySQLColumnType(StrEnum):
"""代码生成模型列类型(MySQL"""
# Python 类型映射
BIGINT = 'int'
BigInteger = 'int' # BIGINT
BINARY = 'bytes'
BLOB = 'bytes'
BOOLEAN = 'bool' # BOOL
Boolean = 'bool' # BOOL
CHAR = 'str'
CLOB = 'str'
DATE = 'date'
Date = 'date' # DATE
DATETIME = 'datetime'
DateTime = 'datetime' # DATETIME
DECIMAL = 'Decimal'
DOUBLE = 'float'
Double = 'float' # DOUBLE
DOUBLE_PRECISION = 'float'
Enum = 'Enum' # Enum()
FLOAT = 'float'
Float = 'float' # FLOAT
INT = 'int' # INTEGER
INTEGER = 'int'
Integer = 'int' # INTEGER
Interval = 'timedelta' # DATETIME
JSON = 'dict'
LargeBinary = 'bytes' # BLOB
NCHAR = 'str'
NUMERIC = 'Decimal'
Numeric = 'Decimal' # NUMERIC
NVARCHAR = 'str' # String
PickleType = 'bytes' # BLOB
REAL = 'float'
SMALLINT = 'int'
SmallInteger = 'int' # SMALLINT
String = 'str' # String
TEXT = 'str'
Text = 'str' # TEXT
TIME = 'time'
Time = 'time' # TIME
TIMESTAMP = 'datetime'
Unicode = 'str' # String
UnicodeText = 'str' # TEXT
UUID = 'str | UUID'
Uuid = 'str' # CHAR(32)
VARBINARY = 'bytes'
VARCHAR = 'str' # String
# sa.dialects.mysql 导入
BIT = 'bool'
ENUM = 'Enum'
LONGBLOB = 'bytes'
LONGTEXT = 'str'
MEDIUMBLOB = 'bytes'
MEDIUMINT = 'int'
MEDIUMTEXT = 'str'
SET = 'list[str]'
TINYBLOB = 'bytes'
TINYINT = 'int'
TINYTEXT = 'str'
YEAR = 'int'
class GenModelPostgreSQLColumnType(StrEnum):
"""代码生成模型列类型(PostgreSQL)"""
# Python 类型映射
BIGINT = 'int'
BigInteger = 'int' # BIGINT
BINARY = 'bytes'
BLOB = 'bytes'
BOOLEAN = 'bool'
Boolean = 'bool' # BOOLEAN
CHAR = 'str'
CLOB = 'str'
DATE = 'date'
Date = 'date' # DATE
DATETIME = 'datetime'
DateTime = 'datetime' # TIMESTAMP WITHOUT TIME ZONE
DECIMAL = 'Decimal'
DOUBLE = 'float'
Double = 'float' # DOUBLE PRECISION
DOUBLE_PRECISION = 'float' # DOUBLE PRECISION
Enum = 'Enum' # Enum(name='enum')
FLOAT = 'float'
Float = 'float' # FLOAT
INT = 'int' # INTEGER
INTEGER = 'int'
Integer = 'int' # INTEGER
Interval = 'timedelta' # INTERVAL
JSON = 'dict'
LargeBinary = 'bytes' # BYTEA
NCHAR = 'str'
NUMERIC = 'Decimal'
Numeric = 'Decimal' # NUMERIC
NVARCHAR = 'str' # String
PickleType = 'bytes' # BYTEA
REAL = 'float'
SMALLINT = 'int'
SmallInteger = 'int' # SMALLINT
String = 'str' # String
TEXT = 'str'
Text = 'str' # TEXT
TIME = 'time' # TIME WITHOUT TIME ZONE
Time = 'time' # TIME WITHOUT TIME ZONE
TIMESTAMP = 'datetime' # TIMESTAMP WITHOUT TIME ZONE
Unicode = 'str' # String
UnicodeText = 'str' # TEXT
UUID = 'str | UUID'
Uuid = 'str'
VARBINARY = 'bytes'
VARCHAR = 'str' # String
# sa.dialects.postgresql 导入
ARRAY = 'list'
BIT = 'bool'
BYTEA = 'bytes'
CIDR = 'str'
CITEXT = 'str'
DATEMULTIRANGE = 'list[date]'
DATERANGE = 'tuple[date, date]'
DOMAIN = 'str'
ENUM = 'Enum'
HSTORE = 'dict'
INET = 'str'
INT4MULTIRANGE = 'list[int]'
INT4RANGE = 'tuple[int, int]'
INT8MULTIRANGE = 'list[int]'
INT8RANGE = 'tuple[int, int]'
INTERVAL = 'timedelta'
JSONB = 'dict'
JSONPATH = 'str'
MACADDR = 'str'
MACADDR8 = 'str'
MONEY = 'Decimal'
NUMMULTIRANGE = 'list[Decimal]'
NUMRANGE = 'tuple[Decimal, Decimal]'
OID = 'int'
REGCLASS = 'str'
REGCONFIG = 'str'
TSMULTIRANGE = 'list[datetime]'
TSQUERY = 'str'
TSRANGE = 'tuple[datetime, datetime]'
TSTZMULTIRANGE = 'list[datetime]'
TSTZRANGE = 'tuple[datetime, datetime]'
TSVECTOR = 'str'
@@ -0,0 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.plugin.code_generator.model.business import GenBusiness
from backend.plugin.code_generator.model.column import GenColumn
@@ -10,13 +10,13 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import Base, id_key
if TYPE_CHECKING:
from backend.app.generator.model import GenModel
from backend.plugin.code_generator.model import GenColumn
class GenBusiness(Base):
"""代码生成业务表"""
__tablename__ = 'sys_gen_business'
__tablename__ = 'gen_business'
id: Mapped[id_key] = mapped_column(init=False)
app_name: Mapped[str] = mapped_column(String(50), comment='应用名称(英文)')
@@ -26,6 +26,7 @@ class GenBusiness(Base):
table_comment: Mapped[str | None] = mapped_column(String(255), default=None, comment='表描述')
# relate_model_fk: Mapped[int | None] = mapped_column(default=None, comment='关联表外键')
schema_name: Mapped[str | None] = mapped_column(String(255), default=None, comment='Schema 名称 (默认为英文表名称)')
filename: Mapped[str | None] = mapped_column(String(20), default=None, comment='基础文件名(默认为英文表名称)')
default_datetime_column: Mapped[bool] = mapped_column(default=True, comment='是否存在默认时间列')
api_version: Mapped[str] = mapped_column(String(20), default='v1', comment='代码生成 api 版本,默认为 v1')
gen_path: Mapped[str | None] = mapped_column(String(255), default=None, comment='代码生成路径(默认为 app 根路径)')
@@ -33,4 +34,4 @@ class GenBusiness(Base):
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
)
# 代码生成业务模型一对多
gen_model: Mapped[list['GenModel']] = relationship(init=False, back_populates='gen_business')
gen_column: Mapped[list['GenColumn']] = relationship(init=False, back_populates='gen_business')
@@ -10,18 +10,18 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import DataClassBase, id_key
if TYPE_CHECKING:
from backend.app.generator.model import GenBusiness
from backend.plugin.code_generator.model import GenBusiness
class GenModel(DataClassBase):
"""代码生成模型表"""
class GenColumn(DataClassBase):
"""代码生成模型"""
__tablename__ = 'sys_gen_model'
__tablename__ = 'gen_column'
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(50), comment='列名称')
comment: Mapped[str | None] = mapped_column(String(255), default=None, comment='列描述')
type: Mapped[str] = mapped_column(String(20), default='str', comment='SQLA 模型列类型')
type: Mapped[str] = mapped_column(String(20), default='String', comment='SQLA 模型列类型')
pd_type: Mapped[str] = mapped_column(String(20), default='str', comment='列类型对应的 pydantic 类型')
default: Mapped[str | None] = mapped_column(
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='列默认值'
@@ -33,6 +33,6 @@ class GenModel(DataClassBase):
# 代码生成业务模型一对多
gen_business_id: Mapped[int] = mapped_column(
ForeignKey('sys_gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID'
ForeignKey('gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID'
)
gen_business: Mapped[Union['GenBusiness', None]] = relationship(init=False, back_populates='gen_model')
gen_business: Mapped[Union['GenBusiness', None]] = relationship(init=False, back_populates='gen_column')
@@ -0,0 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.core.path_conf import PLUGIN_DIR
# jinja2 模版文件路径
JINJA2_TEMPLATE_DIR = PLUGIN_DIR / 'code_generator' / 'templates'
@@ -0,0 +1,2 @@
[app]
router = ['v1']
@@ -2,8 +2,7 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict, Field, model_validator
from typing_extensions import Self
from pydantic import ConfigDict, Field
from backend.common.schema import SchemaBase
@@ -17,18 +16,12 @@ class GenBusinessSchemaBase(SchemaBase):
table_simple_name_zh: str = Field(description='表名称(中文简称)')
table_comment: str | None = Field(None, description='表描述')
schema_name: str | None = Field(None, description='Schema 名称 (默认为英文表名称)')
filename: str | None = Field(None, description='基础文件名(默认为英文表名称)')
default_datetime_column: bool = Field(True, description='是否存在默认时间列')
api_version: str = Field('v1', description='代码生成 api 版本')
gen_path: str | None = Field(None, description='代码生成路径(默认为 app 根路径)')
remark: str | None = Field(None, description='备注')
@model_validator(mode='after')
def check_schema_name(self) -> Self:
"""检查并设置 schema 名称"""
if self.schema_name is None:
self.schema_name = self.table_name_en
return self
class CreateGenBusinessParam(GenBusinessSchemaBase):
"""创建代码生成业务参数"""
@@ -3,7 +3,7 @@
from pydantic import ConfigDict, Field, field_validator
from backend.common.schema import SchemaBase
from backend.utils.type_conversion import sql_type_to_sqlalchemy
from backend.plugin.code_generator.utils.type_conversion import sql_type_to_sqlalchemy
class GenModelSchemaBase(SchemaBase):
@@ -9,5 +9,5 @@ class ImportParam(SchemaBase):
"""导入参数"""
app: str = Field(description='应用名称,用于代码生成到指定 app')
table_name: str = Field(description='数据库表名')
table_schema: str = Field(description='数据库名')
table_name: str = Field(description='数据库表名')
@@ -2,11 +2,11 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from backend.app.generator.crud.crud_gen_business import gen_business_dao
from backend.app.generator.model import GenBusiness
from backend.app.generator.schema.gen_business import CreateGenBusinessParam, UpdateGenBusinessParam
from backend.common.exception import errors
from backend.database.db import async_db_session
from backend.plugin.code_generator.crud.crud_business import gen_business_dao
from backend.plugin.code_generator.model import GenBusiness
from backend.plugin.code_generator.schema.business import CreateGenBusinessParam, UpdateGenBusinessParam
class GenBusinessService:
@@ -2,20 +2,20 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from backend.app.generator.crud.crud_gen_model import gen_model_dao
from backend.app.generator.model import GenModel
from backend.app.generator.schema.gen_model import CreateGenModelParam, UpdateGenModelParam
from backend.common.enums import GenModelMySQLColumnType
from backend.common.exception import errors
from backend.database.db import async_db_session
from backend.utils.type_conversion import sql_type_to_pydantic
from backend.plugin.code_generator.crud.crud_column import gen_model_dao
from backend.plugin.code_generator.enums import GenModelMySQLColumnType
from backend.plugin.code_generator.model import GenColumn
from backend.plugin.code_generator.schema.column import CreateGenModelParam, UpdateGenModelParam
from backend.plugin.code_generator.utils.type_conversion import sql_type_to_pydantic
class GenModelService:
"""代码生成模型服务类"""
@staticmethod
async def get(*, pk: int) -> GenModel:
async def get(*, pk: int) -> GenColumn:
"""
获取指定 ID 的模型
@@ -36,7 +36,7 @@ class GenModelService:
return types
@staticmethod
async def get_by_business(*, business_id: int) -> Sequence[GenModel]:
async def get_by_business(*, business_id: int) -> Sequence[GenColumn]:
"""
获取指定业务的所有模型
@@ -5,31 +5,32 @@ import os.path
import zipfile
from pathlib import Path
from typing import Sequence
import aiofiles
from pydantic.alias_generators import to_pascal
from backend.app.generator.crud.crud_gen import gen_dao
from backend.app.generator.crud.crud_gen_business import gen_business_dao
from backend.app.generator.crud.crud_gen_model import gen_model_dao
from backend.app.generator.model import GenBusiness
from backend.app.generator.schema.gen import ImportParam
from backend.app.generator.schema.gen_business import CreateGenBusinessParam
from backend.app.generator.schema.gen_model import CreateGenModelParam
from backend.app.generator.service.gen_model_service import gen_model_service
from backend.common.exception import errors
from backend.core.path_conf import BASE_PATH
from backend.database.db import async_db_session
from backend.utils.gen_template import gen_template
from backend.utils.type_conversion import sql_type_to_pydantic
from backend.plugin.code_generator.crud.crud_business import gen_business_dao
from backend.plugin.code_generator.crud.crud_column import gen_model_dao
from backend.plugin.code_generator.crud.crud_gen import gen_dao
from backend.plugin.code_generator.model import GenBusiness
from backend.plugin.code_generator.schema.business import CreateGenBusinessParam
from backend.plugin.code_generator.schema.column import CreateGenModelParam
from backend.plugin.code_generator.schema.gen import ImportParam
from backend.plugin.code_generator.service.column_service import gen_model_service
from backend.plugin.code_generator.utils.gen_template import gen_template
from backend.plugin.code_generator.utils.type_conversion import sql_type_to_pydantic
class GenService:
"""代码生成服务类"""
@staticmethod
async def get_tables(*, table_schema: str) -> list[str]:
async def get_tables(*, table_schema: str) -> Sequence[str]:
"""
获取指定 schema 下的所有表名
@@ -57,14 +58,17 @@ class GenService:
raise errors.ForbiddenError(msg='已存在相同数据库表业务')
table_name = table_info[0]
business_data = {
'app_name': obj.app,
'table_name_en': table_name,
'table_name_zh': table_info[1] or ' '.join(table_name.split('_')),
'table_simple_name_zh': table_info[1] or table_name.split('_')[-1],
'table_comment': table_info[1],
}
new_business = GenBusiness(**CreateGenBusinessParam(**business_data).model_dump())
new_business = GenBusiness(
**CreateGenBusinessParam(
app_name=obj.app,
table_name_en=table_name,
table_name_zh=table_info[1] or ' '.join(table_name.split('_')),
table_simple_name_zh=table_info[1] or table_name.split('_')[-1],
table_comment=table_info[1],
schema_name=table_name,
filename=table_name,
).model_dump()
)
db.add(new_business)
await db.flush()
@@ -72,17 +76,20 @@ class GenService:
for column in column_info:
column_type = column[-1].split('(')[0].upper()
pd_type = sql_type_to_pydantic(column_type)
model_data = {
'name': column[0],
'comment': column[-2],
'type': column_type,
'sort': column[-3],
'length': column[-1].split('(')[1][:-1] if pd_type == 'str' and '(' in column[-1] else 0,
'is_pk': column[1],
'is_nullable': column[2],
'gen_business_id': new_business.id,
}
await gen_model_dao.create(db, CreateGenModelParam(**model_data), pd_type=pd_type)
await gen_model_dao.create(
db,
CreateGenModelParam(
name=column[0],
comment=column[-2],
type=column_type,
sort=column[-3],
length=column[-1].split('(')[1][:-1] if pd_type == 'str' and '(' in column[-1] else 0,
is_pk=column[1],
is_nullable=column[2],
gen_business_id=new_business.id,
),
pd_type=pd_type,
)
@staticmethod
async def render_tpl_code(*, business: GenBusiness) -> dict[str, str]:
@@ -99,7 +106,7 @@ class GenService:
gen_vars = gen_template.get_vars(business, gen_models)
return {
tpl_path: await gen_template.get_template(tpl_path).render_async(**gen_vars)
for tpl_path in gen_template.get_template_paths()
for tpl_path in gen_template.get_template_files()
}
async def preview(self, *, pk: int) -> dict[str, bytes]:
@@ -115,10 +122,13 @@ class GenService:
raise errors.NotFoundError(msg='业务不存在')
tpl_code_map = await self.render_tpl_code(business=business)
return {
tpl.replace('.jinja', '.py') if tpl.startswith('py') else ...: code.encode('utf-8')
for tpl, code in tpl_code_map.items()
}
codes = {}
for tpl, code in tpl_code_map.items():
if tpl.startswith('python'):
codes[tpl.replace('.jinja', '.py').split('/')[-1]] = code.encode('utf-8')
return codes
@staticmethod
async def get_generate_path(*, pk: int) -> list[str]:
@@ -133,9 +143,10 @@ class GenService:
if not business:
raise errors.NotFoundError(msg='业务不存在')
gen_path = business.gen_path or 'fba-backend-app-path'
gen_path = business.gen_path or 'fba-backend-app-dir'
target_files = gen_template.get_code_gen_paths(business)
return [os.path.join(gen_path, *target_file.split('/')[1:]) for target_file in target_files]
return [os.path.join(gen_path, *target_file.split('/')) for target_file in target_files]
async def generate(self, *, pk: int) -> None:
"""
@@ -155,32 +166,29 @@ class GenService:
for tpl_path, code in tpl_code_map.items():
code_filepath = os.path.join(
gen_path,
*gen_template.get_code_gen_path(tpl_path, business).split('/')[1:],
*gen_template.get_code_gen_path(tpl_path, business).split('/'),
)
code_folder = Path(str(code_filepath)).parent
code_folder.mkdir(parents=True, exist_ok=True)
# 写入 init 文件
str_code_filepath = str(code_filepath)
code_folder = Path(str_code_filepath).parent
code_folder.mkdir(parents=True, exist_ok=True)
init_filepath = code_folder.joinpath('__init__.py')
if not init_filepath.exists():
async with aiofiles.open(init_filepath, 'w', encoding='utf-8') as f:
async with aiofiles.open(init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content)
# api __init__.py
if 'api' in str_code_filepath:
api_init_filepath = code_folder.parent.joinpath('__init__.py')
async with aiofiles.open(api_init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content)
if 'api' in str(code_folder):
# api __init__.py
api_init_filepath = code_folder.parent.joinpath('__init__.py')
if not api_init_filepath.exists():
async with aiofiles.open(api_init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content)
# app __init__.py
app_init_filepath = api_init_filepath.parent.joinpath('__init__.py')
if not app_init_filepath.exists():
async with aiofiles.open(app_init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content)
# 写入代码文件
async with aiofiles.open(code_filepath, 'w', encoding='utf-8') as f:
await f.write(code)
# app __init__.py
if 'service' in str_code_filepath:
app_init_filepath = code_folder.parent.joinpath('__init__.py')
async with aiofiles.open(app_init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content)
# model init 文件补充
if code_folder.name == 'model':
@@ -190,6 +198,10 @@ class GenService:
f'import {to_pascal(business.table_name_en)}\n',
)
# 写入代码文件
async with aiofiles.open(code_filepath, 'w', encoding='utf-8') as f:
await f.write(code)
async def download(self, *, pk: int) -> io.BytesIO:
"""
下载生成的代码
@@ -206,13 +218,12 @@ class GenService:
with zipfile.ZipFile(bio, 'w') as zf:
tpl_code_map = await self.render_tpl_code(business=business)
for tpl_path, code in tpl_code_map.items():
# 写入代码文件
new_code_path = gen_template.get_code_gen_path(tpl_path, business)
zf.writestr(new_code_path, code)
code_filepath = gen_template.get_code_gen_path(tpl_path, business)
# 写入 init 文件
init_filepath = os.path.join(*new_code_path.split('/')[:-1], '__init__.py')
if 'model' not in new_code_path.split('/'):
code_dir = os.path.dirname(code_filepath)
init_filepath = os.path.join(code_dir, '__init__.py')
if 'model' not in code_filepath.split('/'):
zf.writestr(init_filepath, gen_template.init_content)
else:
zf.writestr(
@@ -222,11 +233,19 @@ class GenService:
f'import {to_pascal(business.table_name_en)}\n',
)
if 'api' in new_code_path:
# api __init__.py
api_init_filepath = os.path.join(*new_code_path.split('/')[:-2], '__init__.py')
# api __init__.py
if 'api' in code_dir:
api_init_filepath = os.path.join(os.path.dirname(code_dir), '__init__.py')
zf.writestr(api_init_filepath, gen_template.init_content)
# app __init__.py
if 'service' in code_dir:
app_init_filepath = os.path.join(os.path.dirname(code_dir), '__init__.py')
zf.writestr(app_init_filepath, gen_template.init_content)
# 写入代码文件
zf.writestr(code_filepath, code)
bio.seek(0)
return bio
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -5,10 +5,9 @@ from typing import Sequence
from jinja2 import Environment, FileSystemLoader, Template, select_autoescape
from pydantic.alias_generators import to_pascal, to_snake
from backend.app.generator.conf import generator_settings
from backend.app.generator.model import GenBusiness, GenModel
from backend.core.conf import settings
from backend.core.path_conf import JINJA2_TEMPLATE_DIR
from backend.plugin.code_generator.model import GenBusiness, GenColumn
from backend.plugin.code_generator.path_conf import JINJA2_TEMPLATE_DIR
class GenTemplate:
@@ -34,19 +33,19 @@ class GenTemplate:
return self.env.get_template(jinja_file)
@staticmethod
def get_template_paths() -> list[str]:
def get_template_files() -> list[str]:
"""
获取模板文件路径列表
获取模板文件列表
:return:
"""
return [
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/api.jinja',
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/crud.jinja',
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/model.jinja',
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/schema.jinja',
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/service.jinja',
]
files = []
# python
python_template_path = JINJA2_TEMPLATE_DIR / 'python'
files.extend([f'python/{file.name}' for file in python_template_path.iterdir() if file.is_file()])
return files
@staticmethod
def get_code_gen_paths(business: GenBusiness) -> list[str]:
@@ -57,13 +56,13 @@ class GenTemplate:
:return:
"""
app_name = business.app_name
module_name = business.table_name_en
filename = business.filename
return [
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/{app_name}/api/{business.api_version}/{module_name}.py',
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/{app_name}/crud/crud_{module_name}.py',
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/{app_name}/model/{module_name}.py',
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/{app_name}/schema/{module_name}.py',
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/{app_name}/service/{module_name}_service.py',
f'{app_name}/api/{business.api_version}/{filename}.py',
f'{app_name}/crud/crud_{filename}.py',
f'{app_name}/model/{filename}.py',
f'{app_name}/schema/{filename}.py',
f'{app_name}/service/{filename}_service.py',
]
def get_code_gen_path(self, tpl_path: str, business: GenBusiness) -> str:
@@ -75,11 +74,11 @@ class GenTemplate:
:return:
"""
target_files = self.get_code_gen_paths(business)
code_gen_path_mapping = dict(zip(self.get_template_paths(), target_files))
code_gen_path_mapping = dict(zip(self.get_template_files(), target_files))
return code_gen_path_mapping[tpl_path]
@staticmethod
def get_vars(business: GenBusiness, models: Sequence[GenModel]) -> dict[str, str | Sequence[GenModel]]:
def get_vars(business: GenBusiness, models: Sequence[GenColumn]) -> dict[str, str | Sequence[GenColumn]]:
"""
获取模板变量
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.common.enums import GenModelMySQLColumnType, GenModelPostgreSQLColumnType
from backend.core.conf import settings
from backend.plugin.code_generator.enums import GenModelMySQLColumnType, GenModelPostgreSQLColumnType
def sql_type_to_sqlalchemy(typing: str) -> str:
+1 -1
View File
@@ -55,7 +55,7 @@ class CRUDConfig(CRUDPlus[Config]):
"""
return await self.select_model_by_column(db, key=key)
async def get_list(self, name: str | None = None, type: str | None = None) -> Select:
async def get_list(self, name: str | None, type: str | None) -> Select:
"""
获取参数配置列表
@@ -64,7 +64,7 @@ class ConfigService:
return config
@staticmethod
async def get_select(*, name: str | None = None, type: str | None = None) -> Select:
async def get_select(*, name: str | None, type: str | None) -> Select:
"""
获取参数配置列表查询条件
+1 -1
View File
@@ -22,7 +22,7 @@ class CRUDDictData(CRUDPlus[DictData]):
"""
return await self.select_model(db, pk)
async def get_list(self, label: str | None = None, value: str | None = None, status: int | None = None) -> Select:
async def get_list(self, label: str | None, value: str | None, status: int | None) -> Select:
"""
获取字典数据列表
+14 -7
View File
@@ -1,7 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import Select
from sqlalchemy import Select, and_, desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import noload
from sqlalchemy_crud_plus import CRUDPlus
from backend.plugin.dict.model import DictType
@@ -21,7 +22,7 @@ class CRUDDictType(CRUDPlus[DictType]):
"""
return await self.select_model(db, pk)
async def get_list(self, *, name: str | None = None, code: str | None = None, status: int | None = None) -> Select:
async def get_list(self, *, name: str | None, code: str | None, status: int | None) -> Select:
"""
获取字典类型列表
@@ -30,14 +31,20 @@ class CRUDDictType(CRUDPlus[DictType]):
:param status: 字典状态
:return:
"""
filters = {}
stmt = select(self.model).options(noload(self.model.datas)).order_by(desc(self.model.created_time))
filters = []
if name is not None:
filters.update(name__like=f'%{name}%')
filters.append(self.model.name.like(f'%{name}%'))
if code is not None:
filters.update(code__like=f'%{code}%')
filters.append(self.model.code.like(f'%{code}%'))
if status is not None:
filters.update(status=status)
return await self.select_order('created_time', 'desc', **filters)
filters.append(self.model.status == status)
if filters:
stmt = stmt.where(and_(*filters))
return stmt
async def get_by_code(self, db: AsyncSession, code: str) -> DictType | None:
"""
+1 -1
View File
@@ -22,7 +22,7 @@ class DictData(Base):
id: Mapped[id_key] = mapped_column(init=False)
label: Mapped[str] = mapped_column(String(32), unique=True, comment='字典标签')
value: Mapped[str] = mapped_column(String(32), unique=True, comment='字典值')
value: Mapped[str] = mapped_column(String(32), comment='字典值')
sort: Mapped[int] = mapped_column(default=0, comment='排序')
status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)')
remark: Mapped[str | None] = mapped_column(
+1 -1
View File
@@ -21,7 +21,7 @@ class DictType(Base):
__tablename__ = 'sys_dict_type'
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(32), unique=True, comment='字典类型名称')
name: Mapped[str] = mapped_column(String(32), comment='字典类型名称')
code: Mapped[str] = mapped_column(String(32), unique=True, comment='字典类型编码')
status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)')
remark: Mapped[str | None] = mapped_column(
@@ -28,7 +28,7 @@ class DictDataService:
return dict_data
@staticmethod
async def get_select(*, label: str | None = None, value: str | None = None, status: int | None = None) -> Select:
async def get_select(*, label: str | None, value: str | None, status: int | None) -> Select:
"""
获取字典数据列表查询条件
@@ -12,7 +12,7 @@ class DictTypeService:
"""字典类型服务类"""
@staticmethod
async def get_select(*, name: str | None = None, code: str | None = None, status: int | None = None) -> Select:
async def get_select(*, name: str | None, code: str | None, status: int | None) -> Select:
"""
获取字典类型列表查询条件
+20 -12
View File
@@ -134,12 +134,13 @@ def inject_extra_router(plugin: str, data: dict[str, Any]) -> None:
raise PluginInjectError(f'扩展级插件 {plugin} 路由注入失败:{str(e)}') from e
def inject_app_router(plugin: str, data: dict[str, Any]) -> None:
def inject_app_router(plugin: str, data: dict[str, Any], target_router: APIRouter) -> None:
"""
应用级插件路由注入
:param plugin: 插件名称
:param data: 插件配置数据
:param target_router: FastAPI 路由器
:return:
"""
module_path = f'backend.plugin.{plugin}.api.router'
@@ -149,10 +150,6 @@ def inject_app_router(plugin: str, data: dict[str, Any]) -> None:
if not routers or not isinstance(routers, list):
raise PluginInjectError(f'应用级插件 {plugin} 配置文件存在错误,请检查')
# 获取目标路由
target_module = import_module_cached('backend.app.router')
target_router = getattr(target_module, 'router')
for router in routers:
plugin_router = getattr(module, router, None)
if not plugin_router or not isinstance(plugin_router, APIRouter):
@@ -166,15 +163,26 @@ def inject_app_router(plugin: str, data: dict[str, Any]) -> None:
raise PluginInjectError(f'应用级插件 {plugin} 路由注入失败:{str(e)}') from e
def plugin_router_inject() -> None:
"""插件路由注入"""
def build_final_router() -> APIRouter:
"""构建最终路由"""
extra_plugins = []
app_plugins = []
for plugin in get_plugins():
data = load_plugin_config(plugin)
# 基于插件 plugin.toml 配置文件,判断插件类型
if data.get('api'):
inject_extra_router(plugin, data)
else:
inject_app_router(plugin, data)
(extra_plugins if data.get('api') else app_plugins).append((plugin, data))
for plugin, data in extra_plugins:
inject_extra_router(plugin, data)
# 主路由,必须在插件路由注入后导入
from backend.app.router import router as main_router
for plugin, data in app_plugins:
inject_app_router(plugin, data, main_router)
return main_router
def _install_plugin_requirements(plugin: str, requirements_file: str) -> None:
+52 -55
View File
@@ -1,3 +1,54 @@
create table gen_business
(
id int auto_increment comment '主键 ID'
primary key,
app_name varchar(50) not null comment '应用名称(英文)',
table_name_en varchar(255) not null comment '表名称(英文)',
table_name_zh varchar(255) not null comment '表名称(中文)',
table_simple_name_zh varchar(255) not null comment '表名称(中文简称)',
table_comment varchar(255) null comment '表描述',
schema_name varchar(255) null comment 'Schema 名称 (默认为英文表名称)',
filename varchar(20) null comment '基础文件名(默认为英文表名称)',
default_datetime_column tinyint(1) not null comment '是否存在默认时间列',
api_version varchar(20) not null comment '代码生成 api 版本,默认为 v1',
gen_path varchar(255) null comment '代码生成路径(默认为 app 根路径)',
remark longtext null comment '备注',
created_time datetime not null comment '创建时间',
updated_time datetime null comment '更新时间',
constraint table_name_en
unique (table_name_en)
)
comment '代码生成业务表';
create index ix_gen_business_id
on gen_business (id);
create table gen_column
(
id int auto_increment comment '主键 ID'
primary key,
name varchar(50) not null comment '列名称',
comment varchar(255) null comment '列描述',
type varchar(20) not null comment 'SQLA 模型列类型',
pd_type varchar(20) not null comment '列类型对应的 pydantic 类型',
`default` longtext null comment '列默认值',
sort int null comment '列排序',
length int not null comment '列长度',
is_pk tinyint(1) not null comment '是否主键',
is_nullable tinyint(1) not null comment '是否可为空',
gen_business_id int not null comment '代码生成业务ID',
constraint gen_column_ibfk_1
foreign key (gen_business_id) references gen_business (id)
on delete cascade
)
comment '代码生成模型列表';
create index gen_business_id
on gen_column (gen_business_id);
create index ix_gen_column_id
on gen_column (id);
create table sys_api
(
id int auto_increment comment '主键 ID'
@@ -110,9 +161,7 @@ create table sys_dict_type
created_time datetime not null comment '创建时间',
updated_time datetime null comment '更新时间',
constraint code
unique (code),
constraint name
unique (name)
unique (code)
)
comment '字典类型表';
@@ -130,8 +179,6 @@ create table sys_dict_data
updated_time datetime null comment '更新时间',
constraint label
unique (label),
constraint value
unique (value),
constraint sys_dict_data_ibfk_1
foreign key (type_id) references sys_dict_type (id)
on delete cascade
@@ -147,56 +194,6 @@ create index type_id
create index ix_sys_dict_type_id
on sys_dict_type (id);
create table sys_gen_business
(
id int auto_increment comment '主键 ID'
primary key,
app_name varchar(50) not null comment '应用名称(英文)',
table_name_en varchar(255) not null comment '表名称(英文)',
table_name_zh varchar(255) not null comment '表名称(中文)',
table_simple_name_zh varchar(255) not null comment '表名称(中文简称)',
table_comment varchar(255) null comment '表描述',
schema_name varchar(255) null comment 'Schema 名称 (默认为英文表名称)',
default_datetime_column tinyint(1) not null comment '是否存在默认时间列',
api_version varchar(20) not null comment '代码生成 api 版本,默认为 v1',
gen_path varchar(255) null comment '代码生成路径(默认为 app 根路径)',
remark longtext null comment '备注',
created_time datetime not null comment '创建时间',
updated_time datetime null comment '更新时间',
constraint table_name_en
unique (table_name_en)
)
comment '代码生成业务表';
create index ix_sys_gen_business_id
on sys_gen_business (id);
create table sys_gen_model
(
id int auto_increment comment '主键 ID'
primary key,
name varchar(50) not null comment '列名称',
comment varchar(255) null comment '列描述',
type varchar(20) not null comment 'SQLA 模型列类型',
pd_type varchar(20) not null comment '列类型对应的 pydantic 类型',
`default` longtext null comment '列默认值',
sort int null comment '列排序',
length int not null comment '列长度',
is_pk tinyint(1) not null comment '是否主键',
is_nullable tinyint(1) not null comment '是否可为空',
gen_business_id int not null comment '代码生成业务ID',
constraint sys_gen_model_ibfk_1
foreign key (gen_business_id) references sys_gen_business (id)
on delete cascade
)
comment '代码生成模型表';
create index gen_business_id
on sys_gen_model (gen_business_id);
create index ix_sys_gen_model_id
on sys_gen_model (id);
create table sys_login_log
(
id int auto_increment comment '主键 ID'
+144 -143
View File
@@ -397,6 +397,62 @@ alter table sys_casbin_rule
create index ix_sys_casbin_rule_id
on sys_casbin_rule (id);
create table gen_business
(
id serial
primary key,
app_name varchar(50) not null,
table_name_en varchar(255) not null
unique,
table_name_zh varchar(255) not null,
table_simple_name_zh varchar(255) not null,
table_comment varchar(255),
schema_name varchar(255),
filename varchar(20),
default_datetime_column boolean not null,
api_version varchar(20) not null,
gen_path varchar(255),
remark text,
created_time timestamp with time zone not null,
updated_time timestamp with time zone
);
comment on table gen_business is '代码生成业务表';
comment on column gen_business.id is '主键 ID';
comment on column gen_business.app_name is '应用名称(英文)';
comment on column gen_business.table_name_en is '表名称(英文)';
comment on column gen_business.table_name_zh is '表名称(中文)';
comment on column gen_business.table_simple_name_zh is '表名称(中文简称)';
comment on column gen_business.table_comment is '表描述';
comment on column gen_business.schema_name is 'Schema 名称 (默认为英文表名称)';
comment on column gen_business.filename is '基础文件名(默认为英文表名称)';
comment on column gen_business.default_datetime_column is '是否存在默认时间列';
comment on column gen_business.api_version is '代码生成 api 版本,默认为 v1';
comment on column gen_business.gen_path is '代码生成路径(默认为 app 根路径)';
comment on column gen_business.remark is '备注';
comment on column gen_business.created_time is '创建时间';
comment on column gen_business.updated_time is '更新时间';
alter table gen_business
owner to postgres;
create index ix_gen_business_id
on gen_business (id);
create table sys_config
(
id serial
@@ -438,6 +494,41 @@ alter table sys_config
create index ix_sys_config_id
on sys_config (id);
create table sys_dict_type
(
id serial
primary key,
name varchar(32) not null,
code varchar(32) not null
unique,
status integer not null,
remark text,
created_time timestamp with time zone not null,
updated_time timestamp with time zone
);
comment on table sys_dict_type is '字典类型表';
comment on column sys_dict_type.id is '主键 ID';
comment on column sys_dict_type.name is '字典类型名称';
comment on column sys_dict_type.code is '字典类型编码';
comment on column sys_dict_type.status is '状态(0停用 1正常)';
comment on column sys_dict_type.remark is '备注';
comment on column sys_dict_type.created_time is '创建时间';
comment on column sys_dict_type.updated_time is '更新时间';
alter table sys_dict_type
owner to postgres;
create index ix_sys_dict_type_id
on sys_dict_type (id);
create table sys_notice
(
id serial
@@ -478,95 +569,6 @@ alter table sys_notice
create index ix_sys_notice_id
on sys_notice (id);
create table sys_dict_type
(
id serial
primary key,
name varchar(32) not null
unique,
code varchar(32) not null
unique,
status integer not null,
remark text,
created_time timestamp with time zone not null,
updated_time timestamp with time zone
);
comment on table sys_dict_type is '字典类型表';
comment on column sys_dict_type.id is '主键 ID';
comment on column sys_dict_type.name is '字典类型名称';
comment on column sys_dict_type.code is '字典类型编码';
comment on column sys_dict_type.status is '状态(0停用 1正常)';
comment on column sys_dict_type.remark is '备注';
comment on column sys_dict_type.created_time is '创建时间';
comment on column sys_dict_type.updated_time is '更新时间';
alter table sys_dict_type
owner to postgres;
create index ix_sys_dict_type_id
on sys_dict_type (id);
create table sys_gen_business
(
id serial
primary key,
app_name varchar(50) not null,
table_name_en varchar(255) not null
unique,
table_name_zh varchar(255) not null,
table_simple_name_zh varchar(255) not null,
table_comment varchar(255),
schema_name varchar(255),
default_datetime_column boolean not null,
api_version varchar(20) not null,
gen_path varchar(255),
remark text,
created_time timestamp with time zone not null,
updated_time timestamp with time zone
);
comment on table sys_gen_business is '代码生成业务表';
comment on column sys_gen_business.id is '主键 ID';
comment on column sys_gen_business.app_name is '应用名称(英文)';
comment on column sys_gen_business.table_name_en is '表名称(英文)';
comment on column sys_gen_business.table_name_zh is '表名称(中文)';
comment on column sys_gen_business.table_simple_name_zh is '表名称(中文简称)';
comment on column sys_gen_business.table_comment is '表描述';
comment on column sys_gen_business.schema_name is 'Schema 名称 (默认为英文表名称)';
comment on column sys_gen_business.default_datetime_column is '是否存在默认时间列';
comment on column sys_gen_business.api_version is '代码生成 api 版本,默认为 v1';
comment on column sys_gen_business.gen_path is '代码生成路径(默认为 app 根路径)';
comment on column sys_gen_business.remark is '备注';
comment on column sys_gen_business.created_time is '创建时间';
comment on column sys_gen_business.updated_time is '更新时间';
alter table sys_gen_business
owner to postgres;
create index ix_sys_gen_business_id
on sys_gen_business (id);
create table sys_role_menu
(
id serial,
@@ -681,17 +683,65 @@ comment on column sys_user.updated_time is '更新时间';
alter table sys_user
owner to postgres;
create index ix_sys_user_id
on sys_user (id);
create unique index ix_sys_user_email
on sys_user (email);
create unique index ix_sys_user_username
on sys_user (username);
create index ix_sys_user_status
on sys_user (status);
create index ix_sys_user_id
on sys_user (id);
create table gen_column
(
id serial
primary key,
name varchar(50) not null,
comment varchar(255),
type varchar(20) not null,
pd_type varchar(20) not null,
"default" text,
sort integer,
length integer not null,
is_pk boolean not null,
is_nullable boolean not null,
gen_business_id integer not null
references gen_business
on delete cascade
);
create unique index ix_sys_user_email
on sys_user (email);
comment on table gen_column is '代码生成模型列表';
comment on column gen_column.id is '主键 ID';
comment on column gen_column.name is '列名称';
comment on column gen_column.comment is '列描述';
comment on column gen_column.type is 'SQLA 模型列类型';
comment on column gen_column.pd_type is '列类型对应的 pydantic 类型';
comment on column gen_column."default" is '列默认值';
comment on column gen_column.sort is '列排序';
comment on column gen_column.length is '列长度';
comment on column gen_column.is_pk is '是否主键';
comment on column gen_column.is_nullable is '是否可为空';
comment on column gen_column.gen_business_id is '代码生成业务ID';
alter table gen_column
owner to postgres;
create index ix_gen_column_id
on gen_column (id);
create table sys_dict_data
(
@@ -699,8 +749,7 @@ create table sys_dict_data
primary key,
label varchar(32) not null
unique,
value varchar(32) not null
unique,
value varchar(32) not null,
sort integer not null,
status integer not null,
remark text,
@@ -737,54 +786,6 @@ alter table sys_dict_data
create index ix_sys_dict_data_id
on sys_dict_data (id);
create table sys_gen_model
(
id serial
primary key,
name varchar(50) not null,
comment varchar(255),
type varchar(20) not null,
pd_type varchar(20) not null,
"default" text,
sort integer,
length integer not null,
is_pk boolean not null,
is_nullable boolean not null,
gen_business_id integer not null
references sys_gen_business
on delete cascade
);
comment on table sys_gen_model is '代码生成模型表';
comment on column sys_gen_model.id is '主键 ID';
comment on column sys_gen_model.name is '列名称';
comment on column sys_gen_model.comment is '列描述';
comment on column sys_gen_model.type is 'SQLA 模型列类型';
comment on column sys_gen_model.pd_type is '列类型对应的 pydantic 类型';
comment on column sys_gen_model."default" is '列默认值';
comment on column sys_gen_model.sort is '列排序';
comment on column sys_gen_model.length is '列长度';
comment on column sys_gen_model.is_pk is '是否主键';
comment on column sys_gen_model.is_nullable is '是否可为空';
comment on column sys_gen_model.gen_business_id is '代码生成业务ID';
alter table sys_gen_model
owner to postgres;
create index ix_sys_gen_model_id
on sys_gen_model (id);
create table sys_user_role
(
id serial,
Generated
+3 -3
View File
@@ -684,13 +684,13 @@ wheels = [
[[package]]
name = "fastapi-oauth20"
version = "0.0.1"
source = { registry = "https://pypi.org/simple" }
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
dependencies = [
{ name = "httpx" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/66/25d4ece161689184f47130bc7ea5bf9c73113c2e628cf586546aa574d8d1/fastapi_oauth20-0.0.1.tar.gz", hash = "sha256:22bf6564f3af42a94cb597f8e20af1366e89e19af49291d15784a4c14059d4c9", size = 6681 }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/8e/66/25d4ece161689184f47130bc7ea5bf9c73113c2e628cf586546aa574d8d1/fastapi_oauth20-0.0.1.tar.gz", hash = "sha256:22bf6564f3af42a94cb597f8e20af1366e89e19af49291d15784a4c14059d4c9" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/87/7e0423109ba2c0f8a0794eedff5bb1f79792e5d8d0941ba0b26f962e3e5c/fastapi_oauth20-0.0.1-py3-none-any.whl", hash = "sha256:921fe0e874cb4186998f74b1d951c360400aceb5ca3889a85c806d865a991f88", size = 11242 },
{ url = "https://mirrors.aliyun.com/pypi/packages/cf/87/7e0423109ba2c0f8a0794eedff5bb1f79792e5d8d0941ba0b26f962e3e5c/fastapi_oauth20-0.0.1-py3-none-any.whl", hash = "sha256:921fe0e874cb4186998f74b1d951c360400aceb5ca3889a85c806d865a991f88" },
]
[[package]]