diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f911ed4d7..8b6acf803 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.6.0 hooks: - id: check-added-large-files - id: end-of-file-fixer @@ -8,7 +8,7 @@ repos: - id: check-toml - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.4.2 + rev: v0.5.1 hooks: - id: ruff args: @@ -20,7 +20,7 @@ repos: - id: ruff-format - repo: https://github.com/pdm-project/pdm - rev: 2.12.4 + rev: 2.16.1 hooks: - id: pdm-export args: diff --git a/README.md b/README.md index f01c274af..8e7173567 100644 --- a/README.md +++ b/README.md @@ -70,17 +70,18 @@ Luckily, we now have a demo site: [FBA UI](https://fba.xwboy.top/) ## Built-in features -1. [x] User management: system user role management, permission assignment -2. [x] Department Management: Configure the system organization (company, department, group...) -3. [x] Menu Management: Configuration of system menus, user menus, button privilege identification -4. [x] Role Management: Assign role menu privileges, assign role routing privileges -5. [x] Dictionary Management: Maintain common fixed data or parameters within the system. -6. [x] Operation Logs: logging and querying of normal and abnormal system operations. -7. [x] Login Authentication: graphical authentication code background authentication login -8. [x] Login Logs: Logging and querying of normal and abnormal user logins -9. [x] Service Monitoring: server hardware device information and status -10. [x] Scheduled tasks: automated tasks, asynchronous tasks, and function invocation are supported -11. [x] Interface Documentation: Automatically generate online interactive API interface documentation. +1. [x] User management: management of system user roles, assignment of permissions +2. [x] Departmental management: Configuration of the system organization (company, department, group, ...) +3. [x] Menu management: Configuration of system menus, user menus, button permission labels +4. [x] Role management: assignment of role menu privileges, assignment of role routing privileges +5. [x] Dictionary management: maintenance of commonly used fixed data or parameters within the system +6. [x] Code generation: back-end code is automatically generated, supporting preview, write and download. +7. [x] Operation log: logging and querying of normal and abnormal system operations. +8. [x] Login authentication: graphical captcha backend authentication login +9. [x] Logging: logging and querying of normal and abnormal user logins +10. [x] Service monitoring: server hardware device information and status +11. [x] Timed tasks: automated tasks, asynchronous tasks, support for function calls +12. [x] Interface Documentation: Automatically generate online interactive API interface documentation. ## Local development diff --git a/README.zh-CN.md b/README.zh-CN.md index 20e7ce563..0c0592f0b 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -68,12 +68,13 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三 3. [x] 菜单管理:配置系统菜单,用户菜单,按钮权限标识 4. [x] 角色管理:角色菜单权限分配,角色路由权限分配 5. [x] 字典管理:维护系统内部常用固定数据或参数 -6. [x] 操作日志:系统正常操作和异常操作日志记录和查询 -7. [x] 登录认证:图形验证码后台认证登录 -8. [x] 登录日志:用户正常登录和异常登录的日志记录与查询 -9. [x] 服务监控:服务器硬件设备信息与状态 -10. [x] 定时任务:自动化任务,异步任务,支持函数调用 -11. [x] 接口文档:自动生成在线交互式 API 接口文档 +6. [x] 代码生成:后端代码自动生成,支持预览,写入及下载 +7. [x] 操作日志:系统正常和异常操作的日志记录与查询 +8. [x] 登录认证:图形验证码后台认证登录 +9. [x] 登录日志:用户正常和异常登录的日志记录与查询 +10. [x] 服务监控:服务器硬件设备信息与状态 +11. [x] 定时任务:自动化任务,异步任务,支持函数调用 +12. [x] 接口文档:自动生成在线交互式 API 接口文档 ## 本地开发 diff --git a/backend/.ruff.toml b/backend/.ruff.toml index 4dc4e3856..fa3922e16 100644 --- a/backend/.ruff.toml +++ b/backend/.ruff.toml @@ -26,7 +26,6 @@ select = [ "UP007" ] preview = true -ignore-init-module-imports = true ignore = ["FURB101"] [lint.flake8-pytest-style] diff --git a/backend/alembic/env.py b/backend/alembic/env.py index c6352b705..cb05ffd4a 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -30,9 +30,11 @@ # for 'autogenerate' support # https://alembic.sqlalchemy.org/en/latest/autogenerate.html#autogenerating-multiple-metadata-collections from backend.app.admin.model import MappedBase as AdminBase +from backend.app.generator.model import MappedBase as GeneratorBase target_metadata = [ AdminBase.metadata, + GeneratorBase.metadata, ] # other values from the config, defined by the needs of env.py, diff --git a/backend/app/admin/api/v1/auth/__init__.py b/backend/app/admin/api/v1/auth/__init__.py index 3105461d3..b13b1aa58 100644 --- a/backend/app/admin/api/v1/auth/__init__.py +++ b/backend/app/admin/api/v1/auth/__init__.py @@ -7,6 +7,5 @@ router = APIRouter(prefix='/auth') - router.include_router(auth_router, tags=['授权']) router.include_router(captcha_router, prefix='/captcha', tags=['验证码']) diff --git a/backend/app/admin/model/sys_dict_data.py b/backend/app/admin/model/sys_dict_data.py index 718747682..884bdc6b5 100644 --- a/backend/app/admin/model/sys_dict_data.py +++ b/backend/app/admin/model/sys_dict_data.py @@ -19,5 +19,5 @@ class DictData(Base): status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)') remark: Mapped[str | None] = mapped_column(LONGTEXT, default=None, comment='备注') # 字典类型一对多 - type_id: Mapped[int] = mapped_column(ForeignKey('sys_dict_type.id'), default=None, comment='字典类型关联ID') + type_id: Mapped[int] = mapped_column(ForeignKey('sys_dict_type.id'), default=0, comment='字典类型关联ID') type: Mapped['DictType'] = relationship(init=False, back_populates='datas') # noqa: F821 diff --git a/backend/app/generator/__init__.py b/backend/app/generator/__init__.py new file mode 100644 index 000000000..56fafa58b --- /dev/null +++ b/backend/app/generator/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/generator/api/__init__.py b/backend/app/generator/api/__init__.py new file mode 100644 index 000000000..56fafa58b --- /dev/null +++ b/backend/app/generator/api/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/generator/api/router.py b/backend/app/generator/api/router.py new file mode 100644 index 000000000..f5837532c --- /dev/null +++ b/backend/app/generator/api/router.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from fastapi import APIRouter + +from backend.app.generator.api.v1.gen import router as gen_router + +v1 = APIRouter() + +v1.include_router(gen_router, prefix='/gen', tags=['代码生成']) diff --git a/backend/app/generator/api/v1/__init__.py b/backend/app/generator/api/v1/__init__.py new file mode 100644 index 000000000..56fafa58b --- /dev/null +++ b/backend/app/generator/api/v1/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/generator/api/v1/gen.py b/backend/app/generator/api/v1/gen.py new file mode 100644 index 000000000..6a1647971 --- /dev/null +++ b/backend/app/generator/api/v1/gen.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from typing import Annotated + +from fastapi import APIRouter, Path, Query +from fastapi.responses import StreamingResponse + +from backend.app.generator.conf import generator_settings +from backend.app.generator.schema.gen_business import CreateGenBusinessParam, UpdateGenBusinessParam +from backend.app.generator.schema.gen_model import CreateGenModelParam, UpdateGenModelParam +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.app.generator.service.gen_service import gen_service +from backend.common.response.response_schema import ResponseModel, response_base +from backend.common.security.jwt import DependsJwtAuth +from backend.common.security.rbac import DependsRBAC +from backend.utils.serializers import select_list_serialize + +router = APIRouter() + + +@router.get('/all', summary='获取所有代码生成业务', dependencies=[DependsJwtAuth]) +async def get_all_businesses() -> ResponseModel: + businesses = await gen_business_service.get_all() + data = await select_list_serialize(businesses) + return await response_base.success(data=data) + + +@router.get('/businesses/{pk}', summary='获取代码生成业务详情', dependencies=[DependsJwtAuth]) +async def get_business(pk: Annotated[int, Path(...)]) -> ResponseModel: + data = await gen_service.get_business_and_model(pk=pk) + return await response_base.success(data=data) + + +@router.post('/businesses', summary='创建代码生成业务', deprecated=True, dependencies=[DependsRBAC]) +async def create_business(obj: CreateGenBusinessParam) -> ResponseModel: + await gen_business_service.create(obj=obj) + return await response_base.success() + + +@router.put('/businesses/{pk}', summary='更新代码生成业务', dependencies=[DependsRBAC]) +async def update_business(pk: Annotated[int, Path(...)], obj: UpdateGenBusinessParam) -> ResponseModel: + count = await gen_business_service.update(pk=pk, obj=obj) + if count > 0: + return await response_base.success() + return await response_base.fail() + + +@router.delete('/businesses', summary='删除代码生成业务', dependencies=[DependsRBAC]) +async def delete_business(pk: Annotated[int, Query(...)]) -> ResponseModel: + count = await gen_business_service.delete(pk=pk) + if count > 0: + return await response_base.success() + return await response_base.fail() + + +@router.post('/models', summary='创建代码生成模型', dependencies=[DependsRBAC]) +async def create_model(obj: CreateGenModelParam) -> ResponseModel: + await gen_model_service.create(obj=obj) + return await response_base.success() + + +@router.put('/models/{pk}', summary='更新代码生成模型', dependencies=[DependsRBAC]) +async def update_model(pk: Annotated[int, Path(...)], obj: UpdateGenModelParam) -> ResponseModel: + count = await gen_model_service.update(pk=pk, obj=obj) + if count > 0: + return await response_base.success() + return await response_base.fail() + + +@router.delete('/models/{pk}', summary='删除代码生成模型', dependencies=[DependsRBAC]) +async def delete_model(pk: Annotated[int, Path(...)]) -> ResponseModel: + count = await gen_model_service.delete(pk=pk) + if count > 0: + return await response_base.success() + return await response_base.fail() + + +@router.get('/tables', summary='获取数据库表', dependencies=[DependsRBAC]) +async def get_all_tables(table_schema: Annotated[str, Query(..., description='数据库名')] = 'fba') -> ResponseModel: + data = await gen_service.get_tables(table_schema=table_schema) + return await response_base.success(data=data) + + +@router.post('/import', summary='导入代码生成业务和模型列', dependencies=[DependsRBAC]) +async def import_table( + app: Annotated[str, Query(..., description='应用名称,用于代码生成到指定 app')], + table_name: Annotated[str, Query(..., description='数据库表名')], + table_schema: Annotated[str, Query(..., description='数据库名')] = 'fba', +) -> ResponseModel: + await gen_service.import_business_and_model(app=app, table_schema=table_schema, table_name=table_name) + return await response_base.success() + + +@router.get('/preview/{pk}', summary='生成代码预览', dependencies=[DependsJwtAuth]) +async def preview_code(pk: Annotated[int, Path(..., description='业务ID')]) -> ResponseModel: + data = await gen_service.preview(pk=pk) + return await response_base.success(data=data) + + +@router.post('/generate/{pk}', summary='生成代码', description='文件磁盘写入,请谨慎操作', dependencies=[DependsRBAC]) +async def generate_code(pk: Annotated[int, Path(..., description='业务ID')]) -> ResponseModel: + await gen_service.generate(pk=pk) + return await response_base.success() + + +@router.post('/download/{pk}', summary='下载代码', dependencies=[DependsRBAC]) +async def download_code(pk: Annotated[int, Path(..., description='业务ID')]): + bio = await gen_service.download(pk=pk) + return StreamingResponse( + bio, + media_type='application/x-zip-compressed', + headers={'Content-Disposition': f'attachment; filename={generator_settings.ZIP_FILENAME}.zip'}, + ) diff --git a/backend/app/generator/conf.py b/backend/app/generator/conf.py new file mode 100644 index 000000000..a30213525 --- /dev/null +++ b/backend/app/generator/conf.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from functools import lru_cache + +from pydantic_settings import BaseSettings, SettingsConfigDict + +from backend.core.path_conf import BasePath + + +class GeneratorSettings(BaseSettings): + """Admin Settings""" + + model_config = SettingsConfigDict(env_file=f'{BasePath}/.env', env_file_encoding='utf-8', extra='ignore') + + # 代码下载 + ZIP_FILENAME: str = 'fba_generator' + + +@lru_cache +def get_generator_settings() -> GeneratorSettings: + """获取 generator 配置""" + return GeneratorSettings() + + +generator_settings = get_generator_settings() diff --git a/backend/app/generator/crud/__init__.py b/backend/app/generator/crud/__init__.py new file mode 100644 index 000000000..56fafa58b --- /dev/null +++ b/backend/app/generator/crud/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/generator/crud/crud_gen.py b/backend/app/generator/crud/crud_gen.py new file mode 100644 index 000000000..ecef470ed --- /dev/null +++ b/backend/app/generator/crud/crud_gen.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from typing import Sequence + +from sqlalchemy import Row, text +from sqlalchemy.ext.asyncio import AsyncSession + + +class CRUDGen: + @staticmethod + async def get_all_tables(db: AsyncSession, table_schema: str) -> Sequence[str]: + t = text( + 'select table_name as table_name ' + 'from information_schema.tables ' + 'where table_name not like "sys_gen_%" ' + 'and table_schema = :table_schema;' + ).bindparams(table_schema=table_schema) + stmt = await db.execute(t) + return stmt.scalars().all() + + @staticmethod + async def get_table(db: AsyncSession, table_name: str) -> Row[tuple]: + t = text( + 'select table_name as table_name, table_comment as table_comment ' + 'from information_schema.tables ' + 'where table_name not like "sys_gen_%" ' + 'and table_name = :table_name;' + ).bindparams(table_name=table_name) + stmt = await db.execute(t) + return stmt.fetchone() + + @staticmethod + async def get_all_columns(db: AsyncSession, table_schema: str, table_name: str) -> Sequence[Row[tuple]]: + t = text( + 'select column_name AS column_name, ' + 'case when column_key = "PRI" then 1 else 0 end as is_pk, ' + 'case when is_nullable = "NO" or column_key = "PRI" then 0 else 1 end as is_nullable, ' + 'ordinal_position as sort, ' + 'column_comment as column_comment, ' + 'column_type as column_type ' + 'from information_schema.columns ' + 'where table_schema = :table_schema ' + 'and table_name = :table_name ' + 'and column_name != "id" ' + 'and column_name != "created_time" ' + 'and column_name != "updated_time" ' + 'order by sort;' + ).bindparams(table_schema=table_schema, table_name=table_name) + stmt = await db.execute(t) + return stmt.fetchall() + + +gen_dao = CRUDGen() diff --git a/backend/app/generator/crud/crud_gen_business.py b/backend/app/generator/crud/crud_gen_business.py new file mode 100644 index 000000000..6f539c08b --- /dev/null +++ b/backend/app/generator/crud/crud_gen_business.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# -*- coding: utf-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 + + +class CRUDGenBusiness(CRUDPlus[GenBusiness]): + async def get(self, db: AsyncSession, pk: int) -> GenBusiness | None: + """ + 获取代码生成业务表 + + :param db: + :param pk: + :return: + """ + return await self.select_model_by_id(db, pk) + + async def get_by_name(self, db: AsyncSession, name: str) -> GenBusiness | None: + """ + 通过 name 获取代码生成业务表 + + :param db: + :param name: + :return: + """ + return await self.select_model_by_column(db, 'table_name_en', name) + + async def get_all(self, db: AsyncSession) -> Sequence[GenBusiness]: + """ + 获取所有代码生成业务表 + + :return: + """ + return await self.select_models(db) + + async def create(self, db: AsyncSession, obj_in: CreateGenBusinessParam) -> None: + """ + 创建代码生成业务表 + + :param db: + :param obj_in: + :return: + """ + await self.create_model(db, obj_in) + + async def update(self, db: AsyncSession, pk: int, obj_in: UpdateGenBusinessParam) -> int: + """ + 更新代码生成业务表 + + :param db: + :param pk: + :param obj_in: + :return: + """ + return await self.update_model(db, pk, obj_in) + + async def delete(self, db: AsyncSession, pk: int) -> int: + """ + 删除代码生成业务表 + + :param db: + :param pk: + :return: + """ + return await self.delete_model(db, pk) + + +gen_business_dao = CRUDGenBusiness(GenBusiness) diff --git a/backend/app/generator/crud/crud_gen_model.py b/backend/app/generator/crud/crud_gen_model.py new file mode 100644 index 000000000..7df2755b1 --- /dev/null +++ b/backend/app/generator/crud/crud_gen_model.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from typing import Sequence + +from sqlalchemy import select +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 + + +class CRUDGenModel(CRUDPlus[GenModel]): + async def get_by_name(self, db: AsyncSession, name: str) -> GenModel | None: + """ + 通过 name 获取代码生成模型表 + :param db: + :param name: + :return: + """ + return await self.select_model_by_column(db, 'name', name) + + async def get_by_business_id(self, db: AsyncSession, business_id: int) -> Sequence[GenModel]: + gen_model = await db.execute( + select(self.model).where(self.model.gen_business_id == business_id).order_by(self.model.sort) + ) + return gen_model.scalars().all() + + async def create(self, db: AsyncSession, obj_in: CreateGenModelParam) -> None: + """ + 创建代码生成模型表 + + :param db: + :param obj_in: + :return: + """ + return await self.create_model(db, obj_in) + + async def update(self, db: AsyncSession, pk: int, obj_in: UpdateGenModelParam) -> int: + """ + 更细代码生成模型表 + + :param db: + :param pk: + :param obj_in: + :return: + """ + return await self.update_model(db, pk, obj_in) + + async def delete(self, db: AsyncSession, pk: int) -> int: + """ + 删除代码生成模型表 + + :param db: + :param pk: + :return: + """ + return await self.delete_model(db, pk) + + +gen_model_dao = CRUDGenModel(GenModel) diff --git a/backend/app/generator/model/__init__.py b/backend/app/generator/model/__init__.py new file mode 100644 index 000000000..6ce035a36 --- /dev/null +++ b/backend/app/generator/model/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from backend.common.model import MappedBase # noqa: I001 +from backend.app.generator.model.gen_business import GenBusiness +from backend.app.generator.model.gen_model import GenModel diff --git a/backend/app/generator/model/gen_business.py b/backend/app/generator/model/gen_business.py new file mode 100644 index 000000000..e27e8d2ad --- /dev/null +++ b/backend/app/generator/model/gen_business.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from sqlalchemy import String +from sqlalchemy.dialects.mysql import LONGTEXT +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from backend.common.model import Base, id_key + + +class GenBusiness(Base): + """代码生成业务表""" + + __tablename__ = 'sys_gen_business' + + id: Mapped[id_key] = mapped_column(init=False) + app_name: Mapped[str] = mapped_column(String(50), comment='应用名称(英文)') + table_name_en: Mapped[str] = mapped_column(String(255), unique=True, comment='表名称(英文)') + table_name_zh: Mapped[str] = mapped_column(String(255), comment='表名称(中文)') + table_simple_name_zh: Mapped[str] = mapped_column(String(255), comment='表名称(中文简称)') + 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 名称 (默认为英文表驼峰)') + have_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 根路径)') + remark: Mapped[str | None] = mapped_column(LONGTEXT, default=None, comment='备注') + # 代码生成业务模型一对多 + gen_model: Mapped[list['GenModel']] = relationship(init=False, back_populates='gen_business') # noqa: F821 diff --git a/backend/app/generator/model/gen_model.py b/backend/app/generator/model/gen_model.py new file mode 100644 index 000000000..d9d11570b --- /dev/null +++ b/backend/app/generator/model/gen_model.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from typing import Union + +from sqlalchemy import ForeignKey, String +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from backend.common.model import DataClassBase, id_key + + +class GenModel(DataClassBase): + """代码生成模型表""" + + __tablename__ = 'sys_gen_model' + + 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='string', comment='列类型') + default: Mapped[str | None] = mapped_column(String(50), default=None, comment='列默认值') + sort: Mapped[int | None] = mapped_column(default=1, comment='列排序') + length: Mapped[int] = mapped_column(default=0, comment='列长度') + is_pk: Mapped[bool] = mapped_column(default=False, comment='是否主键') + is_nullable: Mapped[bool] = mapped_column(default=False, comment='是否可为空') + + # 代码生成业务模型一对多 + gen_business_id: Mapped[int] = mapped_column( + ForeignKey('sys_gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID' + ) + gen_business: Mapped[Union['GenBusiness', None]] = relationship(init=False, back_populates='gen_model') # noqa: F821 diff --git a/backend/app/generator/schema/__init__.py b/backend/app/generator/schema/__init__.py new file mode 100644 index 000000000..56fafa58b --- /dev/null +++ b/backend/app/generator/schema/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/generator/schema/gen_business.py b/backend/app/generator/schema/gen_business.py new file mode 100644 index 000000000..5596bd7cf --- /dev/null +++ b/backend/app/generator/schema/gen_business.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from datetime import datetime + +from pydantic import ConfigDict, Field, model_validator +from pydantic.alias_generators import to_pascal + +from backend.common.schema import SchemaBase + + +class GenBusinessSchemaBase(SchemaBase): + app_name: str + table_name_en: str + table_name_zh: str + table_simple_name_zh: str + table_comment: str | None = None + schema_name: str | None = None + have_datetime_column: bool = Field(default=False) + api_version: str = Field(default='v1') + gen_path: str | None = None + remark: str | None = None + + @model_validator(mode='after') + def check_schema_name(self): + if self.schema_name is None: + self.schema_name = to_pascal(self.table_name_en) + return self + + +class CreateGenBusinessParam(GenBusinessSchemaBase): + pass + + +class UpdateGenBusinessParam(GenBusinessSchemaBase): + pass + + +class GetGenBusinessListDetails(GenBusinessSchemaBase): + model_config = ConfigDict(from_attributes=True) + + id: int + created_time: datetime + updated_time: datetime | None = None diff --git a/backend/app/generator/schema/gen_model.py b/backend/app/generator/schema/gen_model.py new file mode 100644 index 000000000..813ef498a --- /dev/null +++ b/backend/app/generator/schema/gen_model.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from pydantic import ConfigDict, Field, field_validator + +from backend.common.enums import GenModelType +from backend.common.schema import SchemaBase + + +class GenModelSchemaBase(SchemaBase): + name: str + comment: str | None = None + type: GenModelType = Field(GenModelType.String, description='模型 column 类型') + default: str | None = None + sort: int + length: int + is_pk: bool = Field(default=False) + is_nullable: bool = Field(default=False) + gen_business_id: int | None = Field(ge=1) + + @field_validator('type') + @classmethod + def sql_type_to_python(cls, v: GenModelType): + type_mapping = { + GenModelType.CHAR: 'str', + GenModelType.VARCHAR: 'str', + GenModelType.String: 'str', + GenModelType.TEXT: 'str', + GenModelType.Text: 'str', + GenModelType.LONGTEXT: 'str', + GenModelType.UnicodeText: 'str', + GenModelType.INT: 'int', + GenModelType.INTEGER: 'int', + GenModelType.Integer: 'int', + GenModelType.BigInteger: 'int', + GenModelType.SmallInteger: 'int', + GenModelType.BIGINT: 'int', + GenModelType.SMALLINT: 'int', + GenModelType.FLOAT: 'float', + GenModelType.Float: 'float', + GenModelType.Boolean: 'bool', + GenModelType.DECIMAL: 'decimal', + GenModelType.UUID: 'UUID', + GenModelType.Uuid: 'UUID', + } + return type_mapping.get(v) or v + + +class CreateGenModelParam(GenModelSchemaBase): + pass + + +class UpdateGenModelParam(GenModelSchemaBase): + pass + + +class GetGenModelListDetails(GenModelSchemaBase): + model_config = ConfigDict(from_attributes=True) + + id: int diff --git a/backend/app/generator/service/__init__.py b/backend/app/generator/service/__init__.py new file mode 100644 index 000000000..56fafa58b --- /dev/null +++ b/backend/app/generator/service/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/generator/service/gen_business_service.py b/backend/app/generator/service/gen_business_service.py new file mode 100644 index 000000000..102d4ac6d --- /dev/null +++ b/backend/app/generator/service/gen_business_service.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- 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_mysql import async_db_session + + +class GenBusinessService: + @staticmethod + async def get(*, pk: int) -> GenBusiness: + async with async_db_session() as db: + business = await gen_business_dao.get(db, pk) + if not business: + raise errors.NotFoundError(msg='代码生成业务不存在') + return business + + @staticmethod + async def get_all() -> Sequence[GenBusiness]: + async with async_db_session() as db: + businesses = await gen_business_dao.get_all(db) + return businesses + + @staticmethod + async def create(*, obj: CreateGenBusinessParam) -> None: + async with async_db_session.begin() as db: + business = await gen_business_dao.get_by_name(db, obj.table_name_en) + if business: + raise errors.ForbiddenError(msg='代码生成业务已存在') + await gen_business_dao.create(db, obj) + + @staticmethod + async def update(*, pk: int, obj: UpdateGenBusinessParam) -> int: + async with async_db_session.begin() as db: + count = await gen_business_dao.update(db, pk, obj) + return count + + @staticmethod + async def delete(*, pk: int) -> int: + async with async_db_session.begin() as db: + count = await gen_business_dao.delete(db, pk) + return count + + +gen_business_service = GenBusinessService() diff --git a/backend/app/generator/service/gen_model_service.py b/backend/app/generator/service/gen_model_service.py new file mode 100644 index 000000000..08df933fc --- /dev/null +++ b/backend/app/generator/service/gen_model_service.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# -*- 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.exception import errors +from backend.database.db_mysql import async_db_session + + +class GenModelService: + @staticmethod + async def get_by_business(*, business_id: int) -> Sequence[GenModel]: + async with async_db_session() as db: + gen_model = await gen_model_dao.get_by_business_id(db, business_id) + return gen_model + + @staticmethod + async def create(*, obj: CreateGenModelParam) -> None: + async with async_db_session.begin() as db: + gen_models = await gen_model_dao.get_by_business_id(db, obj.gen_business_id) + if gen_models: + if obj.name in [name.name for name in gen_models]: + raise errors.ForbiddenError(msg='禁止添加相同列到模型表') + await gen_model_dao.create(db, obj) + + @staticmethod + async def update(*, pk: int, obj: UpdateGenModelParam) -> int: + async with async_db_session.begin() as db: + gen_models = await gen_model_dao.get_by_business_id(obj.gen_business_id) + if gen_models: + if obj.name in [name.name for name in gen_models]: + raise errors.ForbiddenError(msg='禁止添加相同列到模型表') + count = await gen_model_dao.update(db, pk, obj) + return count + + @staticmethod + async def delete(*, pk: int) -> int: + async with async_db_session.begin() as db: + count = await gen_model_dao.delete(db, pk) + return count + + +gen_model_service = GenModelService() diff --git a/backend/app/generator/service/gen_service.py b/backend/app/generator/service/gen_service.py new file mode 100644 index 000000000..cd9220120 --- /dev/null +++ b/backend/app/generator/service/gen_service.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import io +import os.path +import zipfile + +from pathlib import Path +from typing import Sequence + +import aiofiles + +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_business import CreateGenBusinessParam +from backend.app.generator.schema.gen_model import CreateGenModelParam +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.enums import GenModelType +from backend.common.exception import errors +from backend.core.path_conf import BasePath +from backend.database.db_mysql import async_db_session +from backend.utils.gen_template import gen_template +from backend.utils.serializers import select_as_dict, select_list_serialize + + +class GenService: + @staticmethod + async def get_business_and_model(*, pk: int) -> dict: + gen_business = await gen_business_service.get(pk=pk) + gen_models = await gen_model_service.get_by_business(business_id=pk) + business_data = await select_as_dict(gen_business) + if gen_models: + model_data = await select_list_serialize(gen_models) + business_data.update({'models': model_data}) + return business_data + + @staticmethod + async def get_tables(*, table_schema: str) -> Sequence[str]: + async with async_db_session() as db: + return await gen_dao.get_all_tables(db, table_schema) + + @staticmethod + async def import_business_and_model(*, app: str, table_schema: str, table_name: str) -> None: + async with async_db_session.begin() as db: + table_info = await gen_dao.get_table(db, table_name) + if not table_info: + raise errors.NotFoundError(msg='数据库表不存在') + table_name = table_info[0] + business_data = { + 'app_name': 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()) + db.add(new_business) + await db.flush() + column_info = await gen_dao.get_all_columns(db, table_schema, table_name) + for column in column_info: + column_type = column[-1].split('(')[0].lower() + model_data = { + 'name': column[0], + 'comment': column[-2], + 'type': column_type, + 'sort': column[-3], + 'length': column[-1].split('(')[1][:-1] + if column_type == GenModelType.CHAR or column_type == GenModelType.VARCHAR + 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)) + + @staticmethod + async def render_tpl_code(*, business: GenBusiness) -> dict: + gen_models = await gen_model_service.get_by_business(business_id=business.id) + if not gen_models: + raise errors.NotFoundError(msg='代码生成模型表为空') + gen_vars = gen_template.get_vars(business, gen_models) + tpl_code_map = {} + for tpl_path in gen_template.get_template_paths(): + tpl_code_map[tpl_path] = await gen_template.get_template(tpl_path).render_async(**gen_vars) + return tpl_code_map + + async def preview(self, *, pk: int) -> dict: + async with async_db_session() as db: + business = await gen_business_dao.get(db, pk) + if not business: + 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() + } + + async def generate(self, *, pk: int) -> None: + async with async_db_session() as db: + business = await gen_business_dao.get(db, pk) + if not business: + raise errors.NotFoundError(msg='业务不存在') + tpl_code_map = await self.render_tpl_code(business=business) + gen_path = business.gen_path + if not gen_path: + gen_path = os.path.join(BasePath, 'app') + 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:], + ) + code_folder = Path(str(code_filepath)).parent + if not code_folder.exists(): + code_folder.mkdir(parents=True, exist_ok=True) + async with aiofiles.open(code_filepath, 'w', encoding='utf-8') as f: + await f.write(code) + + async def download(self, *, pk: int) -> io.BytesIO: + async with async_db_session() as db: + business = await gen_business_dao.get(db, pk) + if not business: + raise errors.NotFoundError(msg='业务不存在') + bio = io.BytesIO() + zf = zipfile.ZipFile(bio, 'w') + 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) + zf.close() + bio.seek(0) + return bio + + +gen_service = GenService() diff --git a/backend/app/router.py b/backend/app/router.py index 26adf14ec..c51dece17 100644 --- a/backend/app/router.py +++ b/backend/app/router.py @@ -3,10 +3,12 @@ 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 from backend.core.conf import settings route = APIRouter(prefix=settings.API_V1_STR) route.include_router(admin_v1) +route.include_router(generator_v1) route.include_router(task_v1) diff --git a/backend/common/enums.py b/backend/common/enums.py index 352f3553b..9ce64ef2c 100644 --- a/backend/common/enums.py +++ b/backend/common/enums.py @@ -88,3 +88,59 @@ class UserSocialType(StrEnum): github = 'GitHub' linuxdo = 'LinuxDo' + + +class GenModelType(StrEnum): + """代码生成模型类型""" + + # 待优化 + # https://github.com/zy7y/dfs-generate/blob/master/dfs_generate/types_map.py + ARRAY = 'array' + BIGINT = 'bigint' + BigInteger = 'biginteger' + BINARY = 'binary' + BLOB = 'blob' + BOOLEAN = 'boolean' + Boolean = 'boolean' + CHAR = 'char' + CLOB = 'clob' + DATE = 'date' + Date = 'date' + DATETIME = 'datetime' + DateTime = 'datetime' + DECIMAL = 'decimal' + DOUBLE = 'double' + Double = 'double' + DOUBLE_PRECISION = 'double_precision' + Enum = 'enum' + FLOAT = 'float' + Float = 'float' + INT = 'int' + INTEGER = 'integer' + Integer = 'integer' + Interval = 'interval' + JSON = 'json' + LargeBinary = 'largebinary' + LONGTEXT = 'longtext' + NCHAR = 'nchar' + NUMERIC = 'numeric' + Numeric = 'numeric' + NVARCHAR = 'nvarchar' + PickleType = 'pickletype' + REAL = 'real' + SMALLINT = 'smallint' + SmallInteger = 'smallinteger' + String = 'string' + TEXT = 'text' + Text = 'text' + TIME = 'time' + Time = 'time' + TIMESTAMP = 'timestamp' + TupleType = 'tupletype' + TypeDecorator = 'typedecorator' + Unicode = 'unicode' + UnicodeText = 'unicodetext' + UUID = 'uuid' + Uuid = 'uuid' + VARBINARY = 'varbinary' + VARCHAR = 'varchar' diff --git a/backend/core/path_conf.py b/backend/core/path_conf.py index 0514dbb0d..937a1e0dd 100644 --- a/backend/core/path_conf.py +++ b/backend/core/path_conf.py @@ -19,3 +19,6 @@ # 挂载静态目录 STATIC_DIR = os.path.join(BasePath, 'static') + +# jinja2 模版文件路径 +JINJA2_TEMPLATE_DIR = os.path.join(BasePath, 'templates') diff --git a/backend/pdm.lock b/backend/pdm.lock index a0f7a83eb..6aa955260 100644 --- a/backend/pdm.lock +++ b/backend/pdm.lock @@ -5,11 +5,22 @@ groups = ["default", "lint", "deploy"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.1" -content_hash = "sha256:3b713f796e95f9f7ce4b66a840c900aa518ddcaa911d73bdc0f40594a9ac5725" +content_hash = "sha256:7dfcccec290d3aae8c2588285424b0ee04672fd60d1740b3bccf433a946fc9ee" + +[[package]] +name = "aiofiles" +version = "24.1.0" +requires_python = ">=3.8" +summary = "File support for asyncio." +groups = ["default"] +files = [ + {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, + {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, +] [[package]] name = "alembic" -version = "1.13.0" +version = "1.13.1" requires_python = ">=3.8" summary = "A database migration tool for SQLAlchemy." groups = ["default"] @@ -19,8 +30,8 @@ dependencies = [ "typing-extensions>=4", ] files = [ - {file = "alembic-1.13.0-py3-none-any.whl", hash = "sha256:a23974ea301c3ee52705db809c7413cecd165290c6679b9998dd6c74342ca23a"}, - {file = "alembic-1.13.0.tar.gz", hash = "sha256:ab4b3b94d2e1e5f81e34be8a9b7b7575fc9dd5398fccb0bef351ec9b14872623"}, + {file = "alembic-1.13.1-py3-none-any.whl", hash = "sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43"}, + {file = "alembic-1.13.1.tar.gz", hash = "sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595"}, ] [[package]] @@ -67,16 +78,16 @@ files = [ [[package]] name = "asgiref" -version = "3.7.2" -requires_python = ">=3.7" +version = "3.8.1" +requires_python = ">=3.8" summary = "ASGI specs, helper code, and adapters" groups = ["default"] dependencies = [ "typing-extensions>=4; python_version < \"3.11\"", ] files = [ - {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, - {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, ] [[package]] @@ -880,7 +891,7 @@ files = [ [[package]] name = "jinja2" -version = "3.1.3" +version = "3.1.4" requires_python = ">=3.7" summary = "A very fast and expressive template engine." groups = ["default"] @@ -888,8 +899,8 @@ dependencies = [ "MarkupSafe>=2.0", ] files = [ - {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, - {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, ] [[package]] @@ -1275,99 +1286,96 @@ files = [ [[package]] name = "pydantic" -version = "2.5.2" -requires_python = ">=3.7" +version = "2.8.1" +requires_python = ">=3.8" summary = "Data validation using Python type hints" groups = ["default"] dependencies = [ "annotated-types>=0.4.0", - "pydantic-core==2.14.5", - "typing-extensions>=4.6.1", + "pydantic-core==2.20.1", + "typing-extensions>=4.12.2; python_version >= \"3.13\"", + "typing-extensions>=4.6.1; python_version < \"3.13\"", ] files = [ - {file = "pydantic-2.5.2-py3-none-any.whl", hash = "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0"}, - {file = "pydantic-2.5.2.tar.gz", hash = "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd"}, + {file = "pydantic-2.8.1-py3-none-any.whl", hash = "sha256:a7e968e55cc934de69163cce4e90870326f57575984a698d5a96ad1e97762dbe"}, + {file = "pydantic-2.8.1.tar.gz", hash = "sha256:5ac6a29cc27108917fb0923c28542471087bf90b2dc2c3a64d779818dffc1f6f"}, ] [[package]] name = "pydantic-core" -version = "2.14.5" -requires_python = ">=3.7" -summary = "" +version = "2.20.1" +requires_python = ">=3.8" +summary = "Core functionality for Pydantic validation and serialization" groups = ["default"] dependencies = [ "typing-extensions!=4.7.0,>=4.6.0", ] files = [ - {file = "pydantic_core-2.14.5-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd"}, - {file = "pydantic_core-2.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113"}, - {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66"}, - {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997"}, - {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093"}, - {file = "pydantic_core-2.14.5-cp310-none-win32.whl", hash = "sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720"}, - {file = "pydantic_core-2.14.5-cp310-none-win_amd64.whl", hash = "sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b"}, - {file = "pydantic_core-2.14.5-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459"}, - {file = "pydantic_core-2.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6"}, - {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4"}, - {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada"}, - {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda"}, - {file = "pydantic_core-2.14.5-cp311-none-win32.whl", hash = "sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651"}, - {file = "pydantic_core-2.14.5-cp311-none-win_amd64.whl", hash = "sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077"}, - {file = "pydantic_core-2.14.5-cp311-none-win_arm64.whl", hash = "sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf"}, - {file = "pydantic_core-2.14.5-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093"}, - {file = "pydantic_core-2.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc"}, - {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e"}, - {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69"}, - {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d"}, - {file = "pydantic_core-2.14.5-cp312-none-win32.whl", hash = "sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260"}, - {file = "pydantic_core-2.14.5-cp312-none-win_amd64.whl", hash = "sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36"}, - {file = "pydantic_core-2.14.5-cp312-none-win_arm64.whl", hash = "sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7"}, - {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634"}, - {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8"}, - {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe"}, - {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3"}, - {file = "pydantic_core-2.14.5.tar.gz", hash = "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, + {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, + {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, + {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, + {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, + {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, + {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, + {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, + {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, + {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, ] [[package]] @@ -1600,28 +1608,29 @@ files = [ [[package]] name = "ruff" -version = "0.4.2" +version = "0.5.1" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." groups = ["lint"] files = [ - {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d14dc8953f8af7e003a485ef560bbefa5f8cc1ad994eebb5b12136049bbccc5"}, - {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:24016ed18db3dc9786af103ff49c03bdf408ea253f3cb9e3638f39ac9cf2d483"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2e06459042ac841ed510196c350ba35a9b24a643e23db60d79b2db92af0c2b"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3afabaf7ba8e9c485a14ad8f4122feff6b2b93cc53cd4dad2fd24ae35112d5c5"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799eb468ea6bc54b95527143a4ceaf970d5aa3613050c6cff54c85fda3fde480"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ec4ba9436a51527fb6931a8839af4c36a5481f8c19e8f5e42c2f7ad3a49f5069"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a2243f8f434e487c2a010c7252150b1fdf019035130f41b77626f5655c9ca22"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8772130a063f3eebdf7095da00c0b9898bd1774c43b336272c3e98667d4fb8fa"}, - {file = "ruff-0.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab165ef5d72392b4ebb85a8b0fbd321f69832a632e07a74794c0e598e7a8376"}, - {file = "ruff-0.4.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f32cadf44c2020e75e0c56c3408ed1d32c024766bd41aedef92aa3ca28eef68"}, - {file = "ruff-0.4.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:22e306bf15e09af45ca812bc42fa59b628646fa7c26072555f278994890bc7ac"}, - {file = "ruff-0.4.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82986bb77ad83a1719c90b9528a9dd663c9206f7c0ab69282af8223566a0c34e"}, - {file = "ruff-0.4.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:652e4ba553e421a6dc2a6d4868bc3b3881311702633eb3672f9f244ded8908cd"}, - {file = "ruff-0.4.2-py3-none-win32.whl", hash = "sha256:7891ee376770ac094da3ad40c116258a381b86c7352552788377c6eb16d784fe"}, - {file = "ruff-0.4.2-py3-none-win_amd64.whl", hash = "sha256:5ec481661fb2fd88a5d6cf1f83403d388ec90f9daaa36e40e2c003de66751798"}, - {file = "ruff-0.4.2-py3-none-win_arm64.whl", hash = "sha256:cbd1e87c71bca14792948c4ccb51ee61c3296e164019d2d484f3eaa2d360dfaf"}, - {file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"}, + {file = "ruff-0.5.1-py3-none-linux_armv6l.whl", hash = "sha256:6ecf968fcf94d942d42b700af18ede94b07521bd188aaf2cd7bc898dd8cb63b6"}, + {file = "ruff-0.5.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:204fb0a472f00f2e6280a7c8c7c066e11e20e23a37557d63045bf27a616ba61c"}, + {file = "ruff-0.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d235968460e8758d1e1297e1de59a38d94102f60cafb4d5382033c324404ee9d"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38beace10b8d5f9b6bdc91619310af6d63dd2019f3fb2d17a2da26360d7962fa"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e478d2f09cf06add143cf8c4540ef77b6599191e0c50ed976582f06e588c994"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0368d765eec8247b8550251c49ebb20554cc4e812f383ff9f5bf0d5d94190b0"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3a9a9a1b582e37669b0138b7c1d9d60b9edac880b80eb2baba6d0e566bdeca4d"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdd9f723e16003623423affabcc0a807a66552ee6a29f90eddad87a40c750b78"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9fd62c1e99539da05fcdc1e90d20f74aec1b7a1613463ed77870057cd6bd96"}, + {file = "ruff-0.5.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e216fc75a80ea1fbd96af94a6233d90190d5b65cc3d5dfacf2bd48c3e067d3e1"}, + {file = "ruff-0.5.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c4c2112e9883a40967827d5c24803525145e7dab315497fae149764979ac7929"}, + {file = "ruff-0.5.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dfaf11c8a116394da3b65cd4b36de30d8552fa45b8119b9ef5ca6638ab964fa3"}, + {file = "ruff-0.5.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d7ceb9b2fe700ee09a0c6b192c5ef03c56eb82a0514218d8ff700f6ade004108"}, + {file = "ruff-0.5.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bac6288e82f6296f82ed5285f597713acb2a6ae26618ffc6b429c597b392535c"}, + {file = "ruff-0.5.1-py3-none-win32.whl", hash = "sha256:5c441d9c24ec09e1cb190a04535c5379b36b73c4bc20aa180c54812c27d1cca4"}, + {file = "ruff-0.5.1-py3-none-win_amd64.whl", hash = "sha256:b1789bf2cd3d1b5a7d38397cac1398ddf3ad7f73f4de01b1e913e2abc7dfc51d"}, + {file = "ruff-0.5.1-py3-none-win_arm64.whl", hash = "sha256:2875b7596a740cbbd492f32d24be73e545a4ce0a3daf51e4f4e609962bfd3cd2"}, + {file = "ruff-0.5.1.tar.gz", hash = "sha256:3164488aebd89b1745b47fd00604fb4358d774465f20d1fcd907f9c0fc1b0655"}, ] [[package]] @@ -1680,41 +1689,41 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.23" +version = "2.0.30" requires_python = ">=3.7" summary = "Database Abstraction Library" groups = ["default"] dependencies = [ "greenlet!=0.4.17; platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\"", - "typing-extensions>=4.2.0", -] -files = [ - {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-win32.whl", hash = "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-win_amd64.whl", hash = "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-win32.whl", hash = "sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-win_amd64.whl", hash = "sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-win32.whl", hash = "sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-win_amd64.whl", hash = "sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca"}, - {file = "SQLAlchemy-2.0.23-py3-none-any.whl", hash = "sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d"}, - {file = "SQLAlchemy-2.0.23.tar.gz", hash = "sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69"}, + "typing-extensions>=4.6.0", +] +files = [ + {file = "SQLAlchemy-2.0.30-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b48154678e76445c7ded1896715ce05319f74b1e73cf82d4f8b59b46e9c0ddc"}, + {file = "SQLAlchemy-2.0.30-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2753743c2afd061bb95a61a51bbb6a1a11ac1c44292fad898f10c9839a7f75b2"}, + {file = "SQLAlchemy-2.0.30-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7bfc726d167f425d4c16269a9a10fe8630ff6d14b683d588044dcef2d0f6be7"}, + {file = "SQLAlchemy-2.0.30-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4f61ada6979223013d9ab83a3ed003ded6959eae37d0d685db2c147e9143797"}, + {file = "SQLAlchemy-2.0.30-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a365eda439b7a00732638f11072907c1bc8e351c7665e7e5da91b169af794af"}, + {file = "SQLAlchemy-2.0.30-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bba002a9447b291548e8d66fd8c96a6a7ed4f2def0bb155f4f0a1309fd2735d5"}, + {file = "SQLAlchemy-2.0.30-cp310-cp310-win32.whl", hash = "sha256:0138c5c16be3600923fa2169532205d18891b28afa817cb49b50e08f62198bb8"}, + {file = "SQLAlchemy-2.0.30-cp310-cp310-win_amd64.whl", hash = "sha256:99650e9f4cf3ad0d409fed3eec4f071fadd032e9a5edc7270cd646a26446feeb"}, + {file = "SQLAlchemy-2.0.30-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:955991a09f0992c68a499791a753523f50f71a6885531568404fa0f231832aa0"}, + {file = "SQLAlchemy-2.0.30-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f69e4c756ee2686767eb80f94c0125c8b0a0b87ede03eacc5c8ae3b54b99dc46"}, + {file = "SQLAlchemy-2.0.30-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69c9db1ce00e59e8dd09d7bae852a9add716efdc070a3e2068377e6ff0d6fdaa"}, + {file = "SQLAlchemy-2.0.30-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1429a4b0f709f19ff3b0cf13675b2b9bfa8a7e79990003207a011c0db880a13"}, + {file = "SQLAlchemy-2.0.30-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:efedba7e13aa9a6c8407c48facfdfa108a5a4128e35f4c68f20c3407e4376aa9"}, + {file = "SQLAlchemy-2.0.30-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:16863e2b132b761891d6c49f0a0f70030e0bcac4fd208117f6b7e053e68668d0"}, + {file = "SQLAlchemy-2.0.30-cp311-cp311-win32.whl", hash = "sha256:2ecabd9ccaa6e914e3dbb2aa46b76dede7eadc8cbf1b8083c94d936bcd5ffb49"}, + {file = "SQLAlchemy-2.0.30-cp311-cp311-win_amd64.whl", hash = "sha256:0b3f4c438e37d22b83e640f825ef0f37b95db9aa2d68203f2c9549375d0b2260"}, + {file = "SQLAlchemy-2.0.30-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5a79d65395ac5e6b0c2890935bad892eabb911c4aa8e8015067ddb37eea3d56c"}, + {file = "SQLAlchemy-2.0.30-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9a5baf9267b752390252889f0c802ea13b52dfee5e369527da229189b8bd592e"}, + {file = "SQLAlchemy-2.0.30-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cb5a646930c5123f8461f6468901573f334c2c63c795b9af350063a736d0134"}, + {file = "SQLAlchemy-2.0.30-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:296230899df0b77dec4eb799bcea6fbe39a43707ce7bb166519c97b583cfcab3"}, + {file = "SQLAlchemy-2.0.30-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c62d401223f468eb4da32627bffc0c78ed516b03bb8a34a58be54d618b74d472"}, + {file = "SQLAlchemy-2.0.30-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3b69e934f0f2b677ec111b4d83f92dc1a3210a779f69bf905273192cf4ed433e"}, + {file = "SQLAlchemy-2.0.30-cp312-cp312-win32.whl", hash = "sha256:77d2edb1f54aff37e3318f611637171e8ec71472f1fdc7348b41dcb226f93d90"}, + {file = "SQLAlchemy-2.0.30-cp312-cp312-win_amd64.whl", hash = "sha256:b6c7ec2b1f4969fc19b65b7059ed00497e25f54069407a8701091beb69e591a5"}, + {file = "SQLAlchemy-2.0.30-py3-none-any.whl", hash = "sha256:7108d569d3990c71e26a42f60474b4c02c8586c4681af5fd67e51a044fdea86a"}, + {file = "SQLAlchemy-2.0.30.tar.gz", hash = "sha256:2b1708916730f4830bc69d6f49d37f7698b5bd7530aca7f04f785f8849e95255"}, ] [[package]] @@ -1810,13 +1819,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.10.0" +version = "4.12.2" requires_python = ">=3.8" summary = "Backported and Experimental Type Hints for Python 3.8+" groups = ["default"] files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 732999243..0ff5df1e7 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -6,8 +6,8 @@ authors = [ { name = "Wu Clan", email = "jianhengwu0407@gmail.com" }, ] dependencies = [ - "alembic==1.13.0", - "asgiref==3.7.2", + "alembic>=1.13.0", + "asgiref>=3.8.0", "asyncmy==0.2.9", "bcrypt==4.0.1", "casbin==1.34.0", @@ -28,19 +28,21 @@ dependencies = [ "phonenumbers==8.13.27", "pre-commit==3.2.2", "psutil==5.9.6", - "pydantic==2.5.2", + "pydantic==2.8.1", "pytest==7.2.2", "pytest-pretty==1.2.0", "python-jose==3.3.0", "pytz==2023.3", "redis[hiredis]==5.0.1", - "SQLAlchemy==2.0.23", + "SQLAlchemy==2.0.30", "user-agents==2.2.0", "uvicorn[standard]==0.29.0", "XdbSearchIP==1.0.2", "fastapi_oauth20>=0.0.1a2", "flower==2.0.1", "sqlalchemy-crud-plus==0.0.2", + "jinja2==3.1.4", + "aiofiles==24.1.0", ] requires-python = ">=3.10" readme = "README.md" @@ -48,7 +50,7 @@ license = { text = "MIT" } [tool.pdm.dev-dependencies] lint = [ - "ruff>=0.4.2", + "ruff>=0.5.0", ] deploy = [ "supervisor>=4.2.5", diff --git a/backend/requirements.txt b/backend/requirements.txt index c2158c744..7d9dfdf13 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,11 +1,12 @@ # This file is @generated by PDM. # Please do not edit it manually. -alembic==1.13.0 +aiofiles==24.1.0 +alembic==1.13.1 amqp==5.2.0 annotated-types==0.6.0 anyio==4.3.0 -asgiref==3.7.2 +asgiref==3.8.1 async-timeout==4.0.3; python_full_version <= "3.11.2" asyncmy==0.2.9 attrs==23.2.0 @@ -29,11 +30,11 @@ ecdsa==0.18.0 email-validator==2.0.0 exceptiongroup==1.2.0; python_version < "3.11" fast-captcha==0.2.1 -fastapi==0.111.0 fastapi-cli==0.0.2 fastapi-limiter==0.1.6 fastapi-oauth20==0.0.1a2 fastapi-pagination==0.12.13 +fastapi[all]==0.111.0 filelock==3.13.1 flower==2.0.1 greenlet==3.0.3; platform_machine == "win32" or platform_machine == "WIN32" or platform_machine == "AMD64" or platform_machine == "amd64" or platform_machine == "x86_64" or platform_machine == "ppc64le" or platform_machine == "aarch64" @@ -48,7 +49,7 @@ identify==2.5.35 idna==3.6 iniconfig==2.0.0 itsdangerous==2.1.2 -jinja2==3.1.3 +jinja2==3.1.4 kombu==5.3.5 loguru==0.7.2 mako==1.3.2 @@ -71,8 +72,8 @@ prompt-toolkit==3.0.43 psutil==5.9.6 pyasn1==0.5.1 pycparser==2.21 -pydantic==2.5.2 -pydantic-core==2.14.5 +pydantic==2.8.1 +pydantic-core==2.20.1 pydantic-extra-types==2.2.0 pydantic-settings==2.1.0 pygments==2.17.2 @@ -84,28 +85,28 @@ python-jose==3.3.0 python-multipart==0.0.9 pytz==2023.3 pyyaml==6.0.1 -redis==5.0.1 +redis[hiredis]==5.0.1 rich==13.7.1 rsa==4.9 -ruff==0.4.2 +ruff==0.5.1 setuptools==69.2.0 shellingham==1.5.4 simpleeval==0.9.13 six==1.16.0 sniffio==1.3.1 -sqlalchemy==2.0.23 +sqlalchemy==2.0.30 sqlalchemy-crud-plus==0.0.2 starlette==0.37.2 supervisor==4.2.5 tomli==2.0.1; python_version < "3.11" tornado==6.4 typer==0.12.3 -typing-extensions==4.10.0 +typing-extensions==4.12.2 tzdata==2024.1 ua-parser==0.18.0 ujson==5.9.0 user-agents==2.2.0 -uvicorn==0.29.0 +uvicorn[standard]==0.29.0 uvloop==0.19.0; (sys_platform != "cygwin" and sys_platform != "win32") and platform_python_implementation != "PyPy" vine==5.1.0 virtualenv==20.25.1 diff --git a/backend/sql/create_tables.sql b/backend/sql/create_tables.sql index 713bfa91d..12bba367e 100644 --- a/backend/sql/create_tables.sql +++ b/backend/sql/create_tables.sql @@ -48,13 +48,35 @@ CREATE TABLE `sys_dept` `created_time` datetime NOT NULL COMMENT '创建时间', `updated_time` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`), - KEY `ix_sys_dept_parent_id` (`parent_id`), KEY `ix_sys_dept_id` (`id`), + KEY `ix_sys_dept_parent_id` (`parent_id`), CONSTRAINT `sys_dept_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `sys_dept` (`id`) ON DELETE SET NULL ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; +-- sys_dict_data: table +CREATE TABLE `sys_dict_data` +( + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', + `label` varchar(32) NOT NULL COMMENT '字典标签', + `value` varchar(32) NOT NULL COMMENT '字典值', + `sort` int NOT NULL COMMENT '排序', + `status` int NOT NULL COMMENT '状态(0停用 1正常)', + `remark` longtext COMMENT '备注', + `type_id` int NOT NULL COMMENT '字典类型关联ID', + `created_time` datetime NOT NULL COMMENT '创建时间', + `updated_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `label` (`label`), + UNIQUE KEY `value` (`value`), + KEY `type_id` (`type_id`), + KEY `ix_sys_dict_data_id` (`id`), + CONSTRAINT `sys_dict_data_ibfk_1` FOREIGN KEY (`type_id`) REFERENCES `sys_dict_type` (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; + -- sys_dict_type: table CREATE TABLE `sys_dict_type` ( @@ -73,24 +95,46 @@ CREATE TABLE `sys_dict_type` DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; --- sys_dict_data: table -CREATE TABLE `sys_dict_data` +-- sys_gen_business: table +CREATE TABLE `sys_gen_business` ( - `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', - `label` varchar(32) NOT NULL COMMENT '字典标签', - `value` varchar(32) NOT NULL COMMENT '字典值', - `sort` int NOT NULL COMMENT '排序', - `status` int NOT NULL COMMENT '状态(0停用 1正常)', - `remark` longtext COMMENT '备注', - `type_id` int NOT NULL COMMENT '字典类型关联ID', - `created_time` datetime NOT NULL COMMENT '创建时间', - `updated_time` datetime DEFAULT NULL COMMENT '更新时间', + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', + `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) DEFAULT NULL COMMENT '表描述', + `schema_name` varchar(255) DEFAULT NULL COMMENT 'Schema 名称 (默认为英文表驼峰)', + `have_datetime_column` tinyint(1) NOT NULL COMMENT '是否存在默认时间列', + `api_version` varchar(20) NOT NULL COMMENT '代码生成 api 版本,默认为 v1', + `gen_path` varchar(255) DEFAULT NULL COMMENT '代码生成路径(默认为 app 根路径)', + `remark` longtext COMMENT '备注', + `created_time` datetime NOT NULL COMMENT '创建时间', + `updated_time` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`), - UNIQUE KEY `label` (`label`), - UNIQUE KEY `value` (`value`), - KEY `type_id` (`type_id`), - KEY `ix_sys_dict_data_id` (`id`), - CONSTRAINT `sys_dict_data_ibfk_1` FOREIGN KEY (`type_id`) REFERENCES `sys_dict_type` (`id`) + UNIQUE KEY `table_name_en` (`table_name_en`), + KEY `ix_sys_gen_business_id` (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; + +-- sys_gen_model: table +CREATE TABLE `sys_gen_model` +( + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', + `name` varchar(50) NOT NULL COMMENT '列名称', + `comment` varchar(255) DEFAULT NULL COMMENT '列描述', + `type` varchar(20) NOT NULL COMMENT '列类型', + `default` varchar(50) DEFAULT NULL COMMENT '列默认值', + `sort` int DEFAULT 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', + PRIMARY KEY (`id`), + KEY `gen_business_id` (`gen_business_id`), + KEY `ix_sys_gen_model_id` (`id`), + CONSTRAINT `sys_gen_model_ibfk_1` FOREIGN KEY (`gen_business_id`) REFERENCES `sys_gen_business` (`id`) ON DELETE CASCADE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; @@ -233,8 +277,8 @@ CREATE TABLE `sys_user` PRIMARY KEY (`id`), UNIQUE KEY `uuid` (`uuid`), UNIQUE KEY `nickname` (`nickname`), - UNIQUE KEY `ix_sys_user_username` (`username`), UNIQUE KEY `ix_sys_user_email` (`email`), + UNIQUE KEY `ix_sys_user_username` (`username`), KEY `dept_id` (`dept_id`), KEY `ix_sys_user_id` (`id`), CONSTRAINT `sys_user_ibfk_1` FOREIGN KEY (`dept_id`) REFERENCES `sys_dept` (`id`) ON DELETE SET NULL diff --git a/backend/templates/py/api.jinja b/backend/templates/py/api.jinja new file mode 100644 index 000000000..56169e30f --- /dev/null +++ b/backend/templates/py/api.jinja @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from typing import Annotated + +from fastapi import APIRouter, Depends, Path, Query + +from backend.app.{{ app_name }}.schema.{{ table_name_en }} import Create{{ schema_name }}Param, Get{{ schema_name }}ListDetails, Update{{ schema_name }}Param +from backend.app.{{ app_name }}.service.{{ table_name_en }}_service import {{ table_name_en }}_service +from backend.common.pagination import DependsPagination, paging_data +from backend.common.response.response_schema import ResponseModel, 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.database.db_mysql import CurrentSession + +router = APIRouter() + + +@router.get('/{pk}', summary='获取{{ table_simple_name_zh }}详情', dependencies=[DependsJwtAuth]) +async def get_{{ table_name_en }}(pk: Annotated[int, Path(...)]) -> ResponseModel: + {{ table_name_en }} = await {{ table_name_en }}_service.get(pk=pk) + return await response_base.success(data={{ table_name_en }}) + + +@router.get( + '', + summary='(模糊条件)分页获取所有{{ table_simple_name_zh }}', + dependencies=[ + DependsJwtAuth, + DependsPagination, + ], +) +async def get_pagination_{{ table_name_en }}(db: CurrentSession) -> ResponseModel: + {{ table_name_en }}_select = await {{ table_name_en }}_service.get_select() + page_data = await paging_data(db, {{ table_name_en }}_select, Get{{ schema_name }}ListDetails) + return await response_base.success(data=page_data) + + +@router.post( + '', + summary='创建{{ table_simple_name_zh }}', + dependencies=[ + Depends(RequestPermission('{{ permission_sign }}:add')), + DependsRBAC, + ], +) +async def create_{{ table_name_en }}(obj: Create{{ schema_name }}Param) -> ResponseModel: + await {{ table_name_en }}_service.create(obj=obj) + return await response_base.success() + + +@router.put( + '/{pk}', + summary='更新{{ table_simple_name_zh }}', + dependencies=[ + Depends(RequestPermission('{{ permission_sign }}:edit')), + DependsRBAC, + ], +) +async def update_{{ table_name_en }}(pk: Annotated[int, Path(...)], obj: Update{{ schema_name }}Param) -> ResponseModel: + count = await {{ table_name_en }}_service.update(pk=pk, obj=obj) + if count > 0: + return await response_base.success() + return await response_base.fail() + + +@router.delete( + '', + summary='(批量)删除{{ table_simple_name_zh }}', + dependencies=[ + Depends(RequestPermission('{{ permission_sign }}:del')), + DependsRBAC, + ], +) +async def delete_{{ table_name_en }}(pk: Annotated[list[int], Query(...)]) -> ResponseModel: + count = await {{ table_name_en }}_service.delete(pk=pk) + if count > 0: + return await response_base.success() + return await response_base.fail() diff --git a/backend/templates/py/crud.jinja b/backend/templates/py/crud.jinja new file mode 100644 index 000000000..bd2bcc8dc --- /dev/null +++ b/backend/templates/py/crud.jinja @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from typing import Sequence + +from sqlalchemy import delete +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy_crud_plus import CRUDPlus + +from backend.app.{{ app_name }}.model import {{ table_name_class }} +from backend.app.{{ app_name }}.schema.{{ table_name_en }} import Create{{ schema_name }}Param, Update{{ schema_name }}Param + + +class CRUD{{ table_name_class }}(CRUDPlus[{{ schema_name }}]): + async def get(self, db: AsyncSession, pk: int) -> {{ table_name_class }} | None: + """ + 获取 {{ schema_name }} + + :param db: + :param pk: + :return: + """ + return await self.select_model_by_id(db, pk) + + async def get_all(self, db: AsyncSession) -> Sequence[{{ schema_name }}]: + """ + 获取所有 {{ schema_name }} + + :param db: + :return: + """ + return await self.select_models(db) + + async def create(self, db: AsyncSession, obj_in: Create{{ schema_name }}Param) -> None: + """ + 创建 {{ schema_name }} + + :param db: + :param obj_in: + :return: + """ + await self.create_model(db, obj_in) + + async def update(self, db: AsyncSession, pk: int, obj_in: Update{{ schema_name }}Param) -> int: + """ + 更新 {{ schema_name }} + + :param db: + :param pk: + :param obj_in: + :return: + """ + return await self.update_model(db, pk, obj_in) + + async def delete(self, db: AsyncSession, pk: list[int]) -> int: + """ + 删除 {{ schema_name }} + + :param db: + :param pk: + :return: + """ + {{ table_name_en }}s = await db.execute(delete(self.model).where(self.model.id.in_(pk))) + return {{ table_name_en }}s.rowcount + + +{{ table_name_en }}_dao: CRUD{{ schema_name }} = CRUD{{ schema_name }}({{ schema_name }}) diff --git a/backend/templates/py/model.jinja b/backend/templates/py/model.jinja new file mode 100644 index 000000000..20acda476 --- /dev/null +++ b/backend/templates/py/model.jinja @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from sqlalchemy import String +from sqlalchemy.orm import Mapped, mapped_column + +from backend.common.model import {% if have_datetime_column %}Base{% else %}MappedBase{% endif %}, id_key + +class {{ table_name_class }}({% if have_datetime_column %}Base{% else %}MappedBase{% endif %}): + """{{ table_name_zh }}""" + + __tablename__ = '{{ table_name_en }}' + + id: Mapped[id_key] = mapped_column(init=False) + {% for model in models %} + {{ model.name }}: {% if model.is_nullable %}Mapped[{{ model.type | None }}]{% else %}Mapped[{{ model.type }}]{% endif %} = mapped_column({% if model.type == 'str' %}{{ model_type_mapping.get(model.type) }}({{ model.length }}){% else %}{{ model_type_mapping.get(model.type) or model.type}}(){% endif %}, default={{ model.default }}, sort_order={{ model.sort }}, comment={{ model.comment }}) + {% endfor %} diff --git a/backend/templates/py/schema.jinja b/backend/templates/py/schema.jinja new file mode 100644 index 000000000..9897b71a1 --- /dev/null +++ b/backend/templates/py/schema.jinja @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from datetime import datetime + +from pydantic import ConfigDict, Field + +from backend.common.schema import SchemaBase + + +class {{ schema_name }}SchemaBase(SchemaBase): + {% for model in models %} + {{ model.name }}: {% if model.nullable %}{{ model.type }} | None = None{% else %}{{ model.type }}{% endif %} + {% endfor %} + + + +class Create{{ schema_name }}Param({{ schema_name }}SchemaBase): + pass + + +class Update{{ schema_name }}Param({{ schema_name }}SchemaBase): + pass + + +class Get{{ schema_name }}ListDetails({{ schema_name }}SchemaBase): + model_config = ConfigDict(from_attributes=True) + + id: int + {% if have_datetime_column %} + created_time: datetime + updated_time: datetime | None = None + {% endif %} diff --git a/backend/templates/py/service.jinja b/backend/templates/py/service.jinja new file mode 100644 index 000000000..664c034f3 --- /dev/null +++ b/backend/templates/py/service.jinja @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from typing import Sequence + +from backend.app.{{ app_name }}.crud.crud_{{ table_name_en }} import {{ table_name_en }}_dao +from backend.app.{{ app_name }}.model import {{ table_name_class }} +from backend.app.{{ app_name }}.schema.{{ table_name_en }} import Create{{ schema_name }}Param, Update{{ schema_name }}Param +from backend.common.exception import errors +from backend.database.db_mysql import async_db_session + + +class {{ schema_name }}Service: + @staticmethod + async def get(*, pk: int) -> {{ table_name_class }}: + async with async_db_session() as db: + {{ table_name_en }} = await {{ table_name_en }}_dao.get(db, pk) + if not {{ table_name_en }}: + raise errors.NotFoundError(msg='接口不存在') + return {{ table_name_en }} + + @staticmethod + async def get_all() -> Sequence[{{ table_name_class }}]: + async with async_db_session() as db: + {{ table_name_en }}s = await {{ table_name_en }}_dao.get_all(db) + return {{ table_name_en }}s + + @staticmethod + async def create(*, obj: Create{{ schema_name }}Param) -> None: + async with async_db_session.begin() as db: + {{ table_name_en }} = await {{ table_name_en }}_dao.get_by_name(db, obj.name) + if {{ table_name_en }}: + raise errors.ForbiddenError(msg='{{ table_simple_name_zh }}已存在') + await {{ table_name_en }}_dao.create(db, obj) + + @staticmethod + async def update(*, pk: int, obj: Update{{ schema_name }}Param) -> int: + async with async_db_session.begin() as db: + count = await {{ table_name_en }}_dao.update(db, pk, obj) + return count + + @staticmethod + async def delete(*, pk: list[int]) -> int: + async with async_db_session.begin() as db: + count = await {{ table_name_en }}_dao.delete(db, pk) + return count + + +{{ table_name_en }}_service = {{ schema_name }}Service() diff --git a/backend/utils/gen_template.py b/backend/utils/gen_template.py new file mode 100644 index 000000000..567d2aef3 --- /dev/null +++ b/backend/utils/gen_template.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from jinja2 import Environment, FileSystemLoader, Template, select_autoescape +from pydantic.alias_generators import to_pascal, to_snake + +from backend.app.generator.model import GenBusiness, GenModel +from backend.core.path_conf import JINJA2_TEMPLATE_DIR + + +class GenTemplate: + def __init__(self): + self.env = Environment( + loader=FileSystemLoader(JINJA2_TEMPLATE_DIR), + autoescape=select_autoescape(['html', 'xml', 'jinja']), + trim_blocks=True, + lstrip_blocks=True, + enable_async=True, + ) + + def get_template(self, jinja_file: str) -> Template: + """ + 获取模版文件 + + :param jinja_file: + :return: + """ + + return self.env.get_template(jinja_file) + + @staticmethod + def get_template_paths() -> list[str]: + """ + 获取模版文件路径 + + :return: + """ + return [ + 'py/api.jinja', + 'py/crud.jinja', + 'py/model.jinja', + 'py/schema.jinja', + 'py/service.jinja', + ] + + @staticmethod + def get_code_gen_path(tpl_path: str, business: GenBusiness) -> str: + """ + 获取代码生成路径 + + :return: + """ + app_name = business.app_name + module_name = business.table_name_en + code_gen_path_mapping = { + 'py/api.jinja': f'py/{app_name}/api/{business.api_version}/{module_name}.py', + 'py/crud.jinja': f'py/{app_name}/crud/crud_{module_name}.py', + 'py/model.jinja': f'py/{app_name}/model/{module_name}.py', + 'py/schema.jinja': f'py/{app_name}/schema/{module_name}.py', + 'py/service.jinja': f'py/{app_name}/service/{module_name}_service.py', + } + return code_gen_path_mapping.get(tpl_path) + + @staticmethod + def get_vars(business: GenBusiness, models: list[GenModel]) -> dict: + """ + 获取模版变量 + + :param business: + :param models: + :return: + """ + # python 类型对应的 sqlalchemy 类型 + model_type_mapping = { + 'str': 'String', + 'float': 'Float', + 'int': 'Integer', + 'bool': 'Boolean', + } + return { + 'app_name': business.app_name, + 'table_name_en': to_snake(business.table_name_en), + 'table_name_class': to_pascal(business.table_name_en), + 'table_name_zh': business.table_name_zh, + 'table_simple_name_zh': business.table_simple_name_zh, + 'table_comment': business.table_comment, + 'schema_name': to_pascal(business.schema_name), + 'have_datetime_column': business.have_datetime_column, + 'permission_sign': str(business.__tablename__.replace('_', ':')), + 'models': models, + 'model_type_mapping': model_type_mapping, + } + + +gen_template = GenTemplate()