├── tests └── __init__.py ├── .cursor └── rules │ └── python-rule.mdc ├── myboot ├── jobs │ ├── __init__.py │ └── scheduled_job.py ├── __init__.py ├── core │ ├── di │ │ ├── __init__.py │ │ ├── providers.py │ │ ├── decorators.py │ │ ├── registry.py │ │ └── container.py │ ├── __init__.py │ ├── config.py │ ├── container.py │ ├── decorators.py │ ├── logger.py │ └── server.py ├── web │ ├── __init__.py │ ├── response.py │ ├── exceptions.py │ ├── models.py │ ├── decorators.py │ └── middleware.py ├── utils │ ├── __init__.py │ ├── path_utils.py │ ├── async_utils.py │ └── common.py ├── exceptions.py └── cli.py ├── conf └── config.yaml ├── .gitignore ├── .github └── workflows │ └── publish.yml ├── docs ├── pypi-publishing.md ├── rest-api-response-format.md ├── scheduler_refactor_analysis.md └── dependency-injection.md ├── pyproject.toml ├── examples ├── dependency_injection_example.py └── convention_app.py └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 测试模块 3 | """ 4 | -------------------------------------------------------------------------------- /.cursor/rules/python-rule.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | alwaysApply: true 3 | --- 4 | 5 | - 生成文档除了 README.md 以外其他都放在 docs 目录 6 | - 单元测试文件存放在 docs 7 | - 不要自动生成测试文件,只有在用户手动提示的情况下再自动创建 8 | -------------------------------------------------------------------------------- /myboot/jobs/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 定时任务模块 3 | 4 | 包含定时任务和后台任务 5 | """ 6 | 7 | from .scheduled_job import ScheduledJob 8 | 9 | __all__ = [ 10 | 'ScheduledJob', 11 | ] -------------------------------------------------------------------------------- /myboot/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MyBoot 应用包 3 | 4 | 核心应用代码,包含: 5 | - main.py: 应用入口 6 | - api/: 路由与视图层 7 | - core/: 核心基础设施 8 | - models/: ORM 模型层 9 | - services/: 业务逻辑层 10 | - jobs/: 定时任务 11 | - utils/: 工具函数 12 | """ 13 | 14 | __all__ = [ 15 | 16 | ] -------------------------------------------------------------------------------- /myboot/core/di/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 依赖注入模块 3 | 4 | 提供基于 dependency_injector 的自动依赖注入功能 5 | """ 6 | 7 | from .container import DependencyContainer 8 | from .registry import ServiceRegistry 9 | from .decorators import inject, Provide 10 | 11 | __all__ = [ 12 | 'DependencyContainer', 13 | 'ServiceRegistry', 14 | 'inject', 15 | 'Provide', 16 | ] 17 | 18 | -------------------------------------------------------------------------------- /myboot/web/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Web 模块 3 | 4 | 提供 Web 相关的功能,包括中间件、响应格式等 5 | """ 6 | 7 | from .response import ResponseWrapper, ApiResponse, response 8 | from .middleware import Middleware, FunctionMiddleware, ResponseFormatterMiddleware 9 | 10 | __all__ = [ 11 | "ResponseWrapper", 12 | "ApiResponse", 13 | "response", 14 | "Middleware", 15 | "FunctionMiddleware", 16 | "ResponseFormatterMiddleware" 17 | ] 18 | -------------------------------------------------------------------------------- /myboot/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 工具函数模块 3 | 4 | 包含工具函数和公共模块 5 | """ 6 | 7 | from .common import ( 8 | generate_id, 9 | format_datetime, 10 | validate_email, 11 | hash_password, 12 | verify_password, 13 | generate_token, 14 | parse_datetime, 15 | get_local_ip 16 | ) 17 | 18 | __all__ = [ 19 | "generate_id", 20 | "format_datetime", 21 | "validate_email", 22 | "hash_password", 23 | "verify_password", 24 | "generate_token", 25 | "parse_datetime", 26 | "get_local_ip", 27 | ] -------------------------------------------------------------------------------- /conf/config.yaml: -------------------------------------------------------------------------------- 1 | # MyBoot 应用配置文件 2 | 3 | # 应用配置 4 | app: 5 | name: "MyBoot App" 6 | version: "0.1.0" 7 | 8 | # 服务器配置 9 | server: 10 | port: 8000 11 | reload: true 12 | workers: 1 13 | keep_alive_timeout: 5 14 | graceful_timeout: 30 15 | # CORS 配置(可选,如果不存在则不启用 CORS) 16 | cors: 17 | allow_origins: ["*"] 18 | allow_credentials: true 19 | allow_methods: ["*"] 20 | allow_headers: ["*"] 21 | response_format: 22 | enabled: true # 是否启用自动格式化 23 | exclude_paths: # 排除的路径(这些路径不会自动格式化) 24 | - "/docs" 25 | 26 | # 日志配置 27 | logging: 28 | level: "debug" 29 | 30 | # 任务调度配置 31 | scheduler: 32 | enabled: true 33 | timezone: "UTC" 34 | max_workers: 10 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | *.so 5 | .Python 6 | build/ 7 | develop-eggs/ 8 | dist/ 9 | downloads/ 10 | eggs/ 11 | .eggs/ 12 | lib/ 13 | lib64/ 14 | parts/ 15 | sdist/ 16 | var/ 17 | wheels/ 18 | *.egg-info/ 19 | .installed.cfg 20 | *.egg 21 | 22 | # 虚拟环境 23 | venv/ 24 | ENV/ 25 | env/ 26 | .venv/ 27 | 28 | # IDE 29 | .vscode/ 30 | .idea/ 31 | *.swp 32 | *.swo 33 | *~ 34 | 35 | # 日志 36 | logs/ 37 | *.log 38 | 39 | # 数据库 40 | *.db 41 | *.sqlite 42 | *.sqlite3 43 | 44 | # 配置文件(可能包含敏感信息) 45 | .env 46 | .env.local 47 | 48 | # 测试 49 | .pytest_cache/ 50 | .coverage 51 | htmlcov/ 52 | .tox/ 53 | 54 | # 系统文件 55 | .DS_Store 56 | Thumbs.db 57 | .myboot_cache_*.json 58 | 59 | -------------------------------------------------------------------------------- /myboot/core/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 核心基础设施模块 3 | 4 | 包含配置、数据库、日志、调度器等核心功能 5 | """ 6 | 7 | from .config import ( 8 | get_settings, 9 | get_config, 10 | get_config_str, 11 | get_config_int, 12 | get_config_bool, 13 | reload_config 14 | ) 15 | from .logger import Logger, get_logger, logger, setup_logging 16 | from .scheduler import Scheduler, get_scheduler 17 | from .auto_configuration import AutoConfigurationError 18 | 19 | __all__ = [ 20 | "get_settings", 21 | "get_config", 22 | "get_config_str", 23 | "get_config_int", 24 | "get_config_bool", 25 | "reload_config", 26 | "Logger", 27 | "get_logger", 28 | "logger", # loguru logger,建议直接使用 29 | "setup_logging", 30 | "Scheduler", 31 | "get_scheduler", 32 | "AutoConfigurationError", 33 | ] -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" # 匹配所有以 v 开头的 tag,如 v1.0.0, v0.1.0 7 | 8 | jobs: 9 | build-and-publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | id-token: write # 用于 trusted publishing (OIDC) 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: "3.11" 23 | 24 | - name: Install build dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install build hatchling 28 | 29 | - name: Build distribution packages 30 | run: | 31 | python -m build 32 | 33 | - name: Publish to PyPI 34 | uses: pypa/gh-action-pypi-publish@release/v1 35 | with: 36 | # 使用 trusted publishing (推荐方式,无需 token) 37 | # 需要在 PyPI 上配置 trusted publishing 38 | # 参考: https://docs.pypi.org/trusted-publishers/ 39 | print-hash: true # 显示上传文件的哈希值,便于验证 40 | 41 | -------------------------------------------------------------------------------- /myboot/utils/path_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | 路径工具模块 3 | 4 | 提供项目根目录检测和路径解析功能。 5 | """ 6 | 7 | import os 8 | from pathlib import Path 9 | 10 | 11 | def get_project_root() -> str: 12 | """ 13 | 获取项目根目录 14 | 15 | 从当前文件所在目录开始向上查找,直到找到包含pyproject.toml的目录。 16 | 如果没找到,则使用当前工作目录。 17 | 18 | Returns: 19 | str: 项目根目录的绝对路径 20 | """ 21 | current_dir = Path(__file__).parent.absolute() 22 | 23 | while current_dir.parent != current_dir: 24 | if (current_dir / 'pyproject.toml').exists(): 25 | return str(current_dir) 26 | current_dir = current_dir.parent 27 | 28 | return os.getcwd() 29 | 30 | 31 | def resolve_path(path: str) -> str: 32 | """ 33 | 解析路径,如果是相对路径则基于项目根目录 34 | 35 | Args: 36 | path (str): 要解析的路径,可以是相对路径或绝对路径 37 | 38 | Returns: 39 | str: 解析后的绝对路径 40 | 41 | Examples: 42 | >>> resolve_path('./data/file.csv') 43 | '/path/to/project/data/file.csv' 44 | 45 | >>> resolve_path('/absolute/path/file.csv') 46 | '/absolute/path/file.csv' 47 | """ 48 | if os.path.isabs(path): 49 | return path 50 | 51 | # 清理路径,移除多余的./前缀 52 | clean_path = path.lstrip('./') 53 | return os.path.join(get_project_root(), clean_path) 54 | 55 | 56 | def get_data_dir() -> str: 57 | """ 58 | 获取数据目录的绝对路径 59 | 60 | Returns: 61 | str: 数据目录的绝对路径 62 | """ 63 | return resolve_path('data') 64 | 65 | 66 | def get_conf_dir() -> str: 67 | """ 68 | 获取配置目录的绝对路径 69 | 70 | Returns: 71 | str: 配置目录的绝对路径 72 | """ 73 | return resolve_path('conf') 74 | 75 | 76 | def ensure_dir_exists(path: str) -> str: 77 | """ 78 | 确保目录存在,如果不存在则创建 79 | 80 | Args: 81 | path (str): 目录路径 82 | 83 | Returns: 84 | str: 目录的绝对路径 85 | """ 86 | abs_path = resolve_path(path) 87 | os.makedirs(abs_path, exist_ok=True) 88 | return abs_path 89 | -------------------------------------------------------------------------------- /myboot/core/di/providers.py: -------------------------------------------------------------------------------- 1 | """ 2 | 服务提供者配置 3 | 4 | 定义服务的提供者配置和生命周期管理 5 | """ 6 | 7 | from typing import Any, Type, Optional 8 | from dependency_injector import providers 9 | 10 | 11 | class ServiceProvider: 12 | """服务提供者配置""" 13 | 14 | SINGLETON = 'singleton' 15 | FACTORY = 'factory' 16 | 17 | def __init__( 18 | self, 19 | service_class: Type, 20 | service_name: str, 21 | scope: str = SINGLETON, 22 | **kwargs 23 | ): 24 | """ 25 | 初始化服务提供者 26 | 27 | Args: 28 | service_class: 服务类 29 | service_name: 服务名称 30 | scope: 生命周期范围 (singleton/factory) 31 | **kwargs: 其他配置参数 32 | """ 33 | self.service_class = service_class 34 | self.service_name = service_name 35 | self.scope = scope 36 | self.kwargs = kwargs 37 | self._provider: Optional[Any] = None 38 | 39 | def create_provider(self, dependencies: dict = None) -> Any: 40 | """ 41 | 创建 dependency_injector Provider 42 | 43 | Args: 44 | dependencies: 依赖的服务提供者字典 45 | 46 | Returns: 47 | dependency_injector Provider 实例 48 | """ 49 | if self.scope == self.SINGLETON: 50 | if dependencies: 51 | self._provider = providers.Singleton( 52 | self.service_class, 53 | **dependencies 54 | ) 55 | else: 56 | self._provider = providers.Singleton(self.service_class) 57 | else: 58 | if dependencies: 59 | self._provider = providers.Factory( 60 | self.service_class, 61 | **dependencies 62 | ) 63 | else: 64 | self._provider = providers.Factory(self.service_class) 65 | 66 | return self._provider 67 | 68 | def get_provider(self) -> Any: 69 | """获取提供者实例""" 70 | return self._provider 71 | 72 | -------------------------------------------------------------------------------- /docs/pypi-publishing.md: -------------------------------------------------------------------------------- 1 | # PyPI 自动发布配置指南 2 | 3 | 本文档说明如何配置 GitHub Actions 自动发布到 PyPI。 4 | 5 | ## 工作流说明 6 | 7 | 项目已配置 GitHub Actions 工作流 (`.github/workflows/publish.yml`),当创建新的 tag(以 `v` 开头,如 `v1.0.0`)时,会自动构建并发布到 PyPI。 8 | 9 | ## 配置 Trusted Publishing(推荐方式) 10 | 11 | Trusted Publishing 是 PyPI 推荐的安全发布方式,无需使用 API token,通过 OIDC 进行身份验证。 12 | 13 | ### 步骤 1: 在 PyPI 上配置 Trusted Publisher 14 | 15 | 1. 登录 [PyPI](https://pypi.org/) 16 | 2. 进入您的项目页面 17 | 3. 点击左侧菜单的 **"Publishing"** 或 **"Manage"** → **"Publishing"** 18 | 4. 在 **"Trusted publishers"** 部分,点击 **"Add"** 19 | 5. 填写以下信息: 20 | - **PyPI project name**: `myboot`(您的项目名称) 21 | - **Publisher name**: 自定义名称,如 `github-actions` 22 | - **Workflow filename**: `publish.yml` 23 | - **Environment name**: 留空(或填写特定环境名称) 24 | - **GitHub Owner**: 您的 GitHub 用户名或组织名 25 | - **GitHub Repository**: `myboot`(您的仓库名) 26 | - **Workflow filename**: `publish.yml` 27 | 6. 点击 **"Add"** 保存 28 | 29 | ### 步骤 2: 验证配置 30 | 31 | 配置完成后,当您创建新的 tag 时,GitHub Actions 会自动触发发布流程: 32 | 33 | ```bash 34 | # 创建并推送 tag 35 | git tag v1.0.0 36 | git push origin v1.0.0 37 | ``` 38 | 39 | 工作流会自动: 40 | 41 | 1. 检出代码 42 | 2. 安装构建依赖 43 | 3. 构建分发包(wheel 和 sdist) 44 | 4. 使用 trusted publishing 发布到 PyPI 45 | 46 | ## 使用 API Token 方式(备选) 47 | 48 | 如果您不想使用 trusted publishing,也可以使用传统的 API token 方式: 49 | 50 | ### 步骤 1: 创建 PyPI API Token 51 | 52 | 1. 登录 [PyPI](https://pypi.org/) 53 | 2. 进入 **"Account settings"** → **"API tokens"** 54 | 3. 点击 **"Add API token"** 55 | 4. 填写: 56 | - **Token name**: 如 `github-actions-publish` 57 | - **Scope**: 选择 **"Project: myboot"**(项目范围)或 **"Entire account"**(账户范围) 58 | 5. 复制生成的 token(只显示一次,请妥善保存) 59 | 60 | ### 步骤 2: 在 GitHub 中配置 Secret 61 | 62 | 1. 进入您的 GitHub 仓库 63 | 2. 点击 **"Settings"** → **"Secrets and variables"** → **"Actions"** 64 | 3. 点击 **"New repository secret"** 65 | 4. 填写: 66 | - **Name**: `PYPI_API_TOKEN` 67 | - **Secret**: 粘贴刚才复制的 API token 68 | 5. 点击 **"Add secret"** 69 | 70 | ### 步骤 3: 修改工作流文件 71 | 72 | 修改 `.github/workflows/publish.yml`,添加 `password` 参数: 73 | 74 | ```yaml 75 | - name: Publish to PyPI 76 | uses: pypa/gh-action-pypi-publish@release/v1 77 | with: 78 | password: ${{ secrets.PYPI_API_TOKEN }} 79 | print-hash: true 80 | ``` 81 | 82 | **注意**: 使用 API token 会禁用 trusted publishing,但两种方式不能同时使用。 83 | 84 | ## Tag 命名规则 85 | 86 | 工作流配置为匹配以 `v` 开头的 tag: 87 | 88 | - ✅ `v1.0.0` - 匹配 89 | - ✅ `v0.1.0` - 匹配 90 | - ✅ `v2.3.4` - 匹配 91 | - ❌ `1.0.0` - 不匹配(缺少 `v` 前缀) 92 | - ❌ `release-1.0.0` - 不匹配 93 | 94 | ## 发布流程 95 | 96 | 1. **更新版本号**: 在 `pyproject.toml` 中更新 `version` 字段 97 | 2. **提交更改**: 提交并推送代码到仓库 98 | 3. **创建 Tag**: 创建并推送以 `v` 开头的 tag 99 | 4. **自动发布**: GitHub Actions 会自动触发发布流程 100 | 101 | ```bash 102 | # 示例流程 103 | # 1. 更新 pyproject.toml 中的版本号 104 | # version = "1.0.0" 105 | 106 | # 2. 提交更改 107 | git add pyproject.toml 108 | git commit -m "Bump version to 1.0.0" 109 | git push 110 | 111 | # 3. 创建并推送 tag 112 | git tag v1.0.0 113 | git push origin v1.0.0 114 | ``` 115 | 116 | ## 验证发布 117 | 118 | 发布完成后,您可以: 119 | 120 | 1. 在 [PyPI 项目页面](https://pypi.org/project/myboot/) 查看新版本 121 | 2. 使用 pip 安装测试: 122 | ```bash 123 | pip install myboot==1.0.0 124 | ``` 125 | 126 | ## 故障排查 127 | 128 | ### 问题 1: Trusted Publishing 配置失败 129 | 130 | **错误信息**: `403 Client Error: Invalid or non-existent authentication information` 131 | 132 | **解决方案**: 133 | 134 | - 检查 PyPI 上的 trusted publisher 配置是否正确 135 | - 确认 GitHub 仓库名称、工作流文件名等信息匹配 136 | - 确保工作流文件中的 `permissions` 包含 `id-token: write` 137 | 138 | ### 问题 2: 版本已存在 139 | 140 | **错误信息**: `File already exists` 141 | 142 | **解决方案**: 143 | 144 | - 检查 PyPI 上是否已存在该版本 145 | - 如果确实需要重新发布,需要先删除 PyPI 上的版本(不推荐) 146 | - 或者使用新的版本号 147 | 148 | ### 问题 3: 构建失败 149 | 150 | **错误信息**: 构建步骤失败 151 | 152 | **解决方案**: 153 | 154 | - 检查 `pyproject.toml` 配置是否正确 155 | - 确认所有必需的文件都已包含在分发包中 156 | - 查看 GitHub Actions 日志获取详细错误信息 157 | 158 | ## 参考资源 159 | 160 | - [PyPI Trusted Publishers 文档](https://docs.pypi.org/trusted-publishers/) 161 | - [pypa/gh-action-pypi-publish 项目](https://github.com/pypa/gh-action-pypi-publish) 162 | - [PyPI 发布指南](https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/) 163 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "myboot" 3 | version = "0.1.9" 4 | description = "类似 Spring Boot 的 Python 快速开发框架" 5 | authors = [ 6 | {name = "TrumanDu", email = "truman.p.du@qq.com"} 7 | ] 8 | readme = "README.md" 9 | license = {text = "MIT"} 10 | requires-python = ">=3.9" 11 | keywords = ["framework", "web", "api", "scheduler", "logging", "config"] 12 | classifiers = [ 13 | "Development Status :: 4 - Beta", 14 | "Intended Audience :: Developers", 15 | "License :: OSI Approved :: MIT License", 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3.9", 18 | "Programming Language :: Python :: 3.10", 19 | "Programming Language :: Python :: 3.11", 20 | "Programming Language :: Python :: 3.12", 21 | "Programming Language :: Python :: 3.13", 22 | "Topic :: Software Development :: Libraries :: Application Frameworks", 23 | "Topic :: Internet :: WWW/HTTP :: HTTP Servers", 24 | ] 25 | 26 | dependencies = [ 27 | "fastapi>=0.104.0", 28 | "hypercorn>=0.14.0", 29 | "pydantic>=2.0.0", 30 | "pyyaml>=6.0", 31 | "APScheduler>=3.10.0", 32 | "python-multipart>=0.0.6", 33 | "jinja2>=3.1.0", 34 | "python-dotenv>=1.0.0", 35 | "loguru>=0.7.0", 36 | "colorama>=0.4.6", 37 | "click>=8.0.0", 38 | "dynaconf>=3.2.0", 39 | "requests>=2.28.0", 40 | "dependency-injector>=4.41.0", 41 | "pytz>=2025.2", 42 | ] 43 | 44 | [project.optional-dependencies] 45 | dev = [ 46 | "pytest>=7.0", 47 | "pytest-cov>=4.0", 48 | "pytest-asyncio>=0.21.0", 49 | "black>=23.0", 50 | "isort>=5.12", 51 | "flake8>=6.0", 52 | "mypy>=1.0", 53 | "pre-commit>=3.0", 54 | ] 55 | 56 | test = [ 57 | "pytest>=7.0", 58 | "pytest-cov>=4.0", 59 | "pytest-asyncio>=0.21.0", 60 | "httpx>=0.24.0", 61 | "pytest-mock>=3.10", 62 | ] 63 | 64 | docs = [ 65 | "sphinx>=7.0", 66 | "sphinx-rtd-theme>=1.3", 67 | "myst-parser>=2.0", 68 | ] 69 | 70 | [project.scripts] 71 | myboot = "myboot.cli:cli" 72 | 73 | [build-system] 74 | requires = ["hatchling"] 75 | build-backend = "hatchling.build" 76 | 77 | [tool.hatch.build.targets.wheel] 78 | packages = ["myboot"] 79 | 80 | [tool.hatch.build.targets.sdist] 81 | include = [ 82 | "/myboot", 83 | "/tests", 84 | "/pyproject.toml", 85 | ] 86 | 87 | [tool.black] 88 | line-length = 88 89 | target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] 90 | include = '\.pyi?$' 91 | extend-exclude = ''' 92 | /( 93 | # directories 94 | \.eggs 95 | | \.git 96 | | \.hg 97 | | \.mypy_cache 98 | | \.tox 99 | | \.venv 100 | | build 101 | | dist 102 | )/ 103 | ''' 104 | 105 | [tool.isort] 106 | profile = "black" 107 | multi_line_output = 3 108 | line_length = 88 109 | known_first_party = ["myboot"] 110 | 111 | [tool.mypy] 112 | python_version = "3.9" 113 | warn_return_any = true 114 | warn_unused_configs = true 115 | disallow_untyped_defs = true 116 | disallow_incomplete_defs = true 117 | check_untyped_defs = true 118 | disallow_untyped_decorators = true 119 | no_implicit_optional = true 120 | warn_redundant_casts = true 121 | warn_unused_ignores = true 122 | warn_no_return = true 123 | warn_unreachable = true 124 | strict_equality = true 125 | 126 | [tool.pytest.ini_options] 127 | testpaths = ["tests"] 128 | python_files = ["test_*.py", "*_test.py"] 129 | python_classes = ["Test*"] 130 | python_functions = ["test_*"] 131 | addopts = [ 132 | "--strict-markers", 133 | "--strict-config", 134 | "--verbose", 135 | "--tb=short", 136 | ] 137 | markers = [ 138 | "slow: marks tests as slow (deselect with '-m \"not slow\"')", 139 | "integration: marks tests as integration tests", 140 | "unit: marks tests as unit tests", 141 | ] 142 | 143 | [tool.coverage.run] 144 | source = ["myboot"] 145 | omit = [ 146 | "*/tests/*", 147 | "*/test_*", 148 | "*/__pycache__/*", 149 | "*/venv/*", 150 | "*/.venv/*", 151 | ] 152 | 153 | [tool.coverage.report] 154 | exclude_lines = [ 155 | "pragma: no cover", 156 | "def __repr__", 157 | "if self.debug:", 158 | "if settings.DEBUG", 159 | "raise AssertionError", 160 | "raise NotImplementedError", 161 | "if 0:", 162 | "if __name__ == .__main__.:", 163 | "class .*\\bProtocol\\):", 164 | "@(abc\\.)?abstractmethod", 165 | ] 166 | -------------------------------------------------------------------------------- /myboot/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | MyBoot 异常模块 3 | 4 | 提供框架相关的异常类 5 | """ 6 | 7 | from typing import Any, Dict, Optional 8 | 9 | 10 | class MyBootException(Exception): 11 | """MyBoot 框架异常基类""" 12 | 13 | def __init__( 14 | self, 15 | message: str = "MyBoot 框架错误", 16 | code: str = "MYBOOT_ERROR", 17 | details: Optional[Dict[str, Any]] = None 18 | ): 19 | self.message = message 20 | self.code = code 21 | self.details = details or {} 22 | super().__init__(self.message) 23 | 24 | 25 | class ConfigurationError(MyBootException): 26 | """配置错误异常""" 27 | 28 | def __init__( 29 | self, 30 | message: str = "配置错误", 31 | config_key: Optional[str] = None, 32 | details: Optional[Dict[str, Any]] = None 33 | ): 34 | self.config_key = config_key 35 | super().__init__(message, "CONFIGURATION_ERROR", details) 36 | 37 | 38 | class ValidationError(MyBootException): 39 | """验证错误异常""" 40 | 41 | def __init__( 42 | self, 43 | message: str = "验证失败", 44 | field: Optional[str] = None, 45 | value: Any = None, 46 | details: Optional[Dict[str, Any]] = None 47 | ): 48 | self.field = field 49 | self.value = value 50 | super().__init__(message, "VALIDATION_ERROR", details) 51 | 52 | 53 | class InitializationError(MyBootException): 54 | """初始化错误异常""" 55 | 56 | def __init__( 57 | self, 58 | message: str = "初始化失败", 59 | component: Optional[str] = None, 60 | details: Optional[Dict[str, Any]] = None 61 | ): 62 | self.component = component 63 | super().__init__(message, "INITIALIZATION_ERROR", details) 64 | 65 | 66 | class DependencyError(MyBootException): 67 | """依赖错误异常""" 68 | 69 | def __init__( 70 | self, 71 | message: str = "依赖错误", 72 | dependency: Optional[str] = None, 73 | details: Optional[Dict[str, Any]] = None 74 | ): 75 | self.dependency = dependency 76 | super().__init__(message, "DEPENDENCY_ERROR", details) 77 | 78 | 79 | class ServiceError(MyBootException): 80 | """服务错误异常""" 81 | 82 | def __init__( 83 | self, 84 | message: str = "服务错误", 85 | service: Optional[str] = None, 86 | details: Optional[Dict[str, Any]] = None 87 | ): 88 | self.service = service 89 | super().__init__(message, "SERVICE_ERROR", details) 90 | 91 | 92 | class JobError(MyBootException): 93 | """任务错误异常""" 94 | 95 | def __init__( 96 | self, 97 | message: str = "任务错误", 98 | job_name: Optional[str] = None, 99 | details: Optional[Dict[str, Any]] = None 100 | ): 101 | self.job_name = job_name 102 | super().__init__(message, "JOB_ERROR", details) 103 | 104 | 105 | class SchedulerError(MyBootException): 106 | """调度器错误异常""" 107 | 108 | def __init__( 109 | self, 110 | message: str = "调度器错误", 111 | scheduler: Optional[str] = None, 112 | details: Optional[Dict[str, Any]] = None 113 | ): 114 | self.scheduler = scheduler 115 | super().__init__(message, "SCHEDULER_ERROR", details) 116 | 117 | 118 | class MiddlewareError(MyBootException): 119 | """中间件错误异常""" 120 | 121 | def __init__( 122 | self, 123 | message: str = "中间件错误", 124 | middleware: Optional[str] = None, 125 | details: Optional[Dict[str, Any]] = None 126 | ): 127 | self.middleware = middleware 128 | super().__init__(message, "MIDDLEWARE_ERROR", details) 129 | 130 | 131 | class RouteError(MyBootException): 132 | """路由错误异常""" 133 | 134 | def __init__( 135 | self, 136 | message: str = "路由错误", 137 | route: Optional[str] = None, 138 | details: Optional[Dict[str, Any]] = None 139 | ): 140 | self.route = route 141 | super().__init__(message, "ROUTE_ERROR", details) 142 | 143 | 144 | class LifecycleError(MyBootException): 145 | """生命周期错误异常""" 146 | 147 | def __init__( 148 | self, 149 | message: str = "生命周期错误", 150 | phase: Optional[str] = None, 151 | details: Optional[Dict[str, Any]] = None 152 | ): 153 | self.phase = phase 154 | super().__init__(message, "LIFECYCLE_ERROR", details) 155 | -------------------------------------------------------------------------------- /myboot/web/response.py: -------------------------------------------------------------------------------- 1 | """ 2 | REST API 响应格式封装 3 | 4 | 提供统一的 REST API 响应格式,确保所有 API 返回一致的格式: 5 | { 6 | "success": true/false, 7 | "code": 200, 8 | "message": "操作成功", 9 | "data": {...} 10 | } 11 | """ 12 | 13 | from typing import Any, Dict, Optional, Union 14 | from pydantic import BaseModel, Field 15 | 16 | 17 | class ApiResponse(BaseModel): 18 | """统一的 REST API 响应格式""" 19 | 20 | success: bool = Field(description="是否成功") 21 | code: int = Field(description="HTTP 状态码") 22 | message: Optional[str] = Field(default=None, description="响应消息") 23 | data: Optional[Any] = Field(default=None, description="响应数据") 24 | 25 | class Config: 26 | # 序列化时排除值为 None 的字段 27 | exclude_none = True 28 | json_encoders = { 29 | # 可以在这里添加自定义编码器 30 | } 31 | 32 | 33 | class ResponseWrapper: 34 | """响应包装器 35 | 36 | 提供便捷方法创建统一格式的响应 37 | """ 38 | 39 | @staticmethod 40 | def success( 41 | data: Any = None, 42 | message: Optional[str] = None, 43 | code: int = 200 44 | ) -> Dict[str, Any]: 45 | """ 46 | 创建成功响应 47 | 48 | Args: 49 | data: 响应数据 50 | message: 响应消息 51 | code: HTTP 状态码 52 | 53 | Returns: 54 | 统一格式的响应字典 55 | """ 56 | result = { 57 | "success": True, 58 | "code": code, 59 | } 60 | if message is not None: 61 | result["message"] = message 62 | if data is not None: 63 | result["data"] = data 64 | return result 65 | 66 | @staticmethod 67 | def error( 68 | message: Optional[str] = None, 69 | code: int = 500, 70 | data: Optional[Any] = None 71 | ) -> Dict[str, Any]: 72 | """ 73 | 创建错误响应 74 | 75 | Args: 76 | message: 错误消息 77 | code: HTTP 状态码 78 | data: 错误详情数据 79 | 80 | Returns: 81 | 统一格式的响应字典 82 | """ 83 | result = { 84 | "success": False, 85 | "code": code, 86 | } 87 | if message is not None: 88 | result["message"] = message 89 | if data is not None: 90 | result["data"] = data 91 | return result 92 | 93 | @staticmethod 94 | def created( 95 | data: Any = None, 96 | message: Optional[str] = None 97 | ) -> Dict[str, Any]: 98 | """创建成功响应(201)""" 99 | return ResponseWrapper.success(data=data, message=message, code=201) 100 | 101 | @staticmethod 102 | def updated( 103 | data: Any = None, 104 | message: Optional[str] = None 105 | ) -> Dict[str, Any]: 106 | """更新成功响应(200)""" 107 | return ResponseWrapper.success(data=data, message=message, code=200) 108 | 109 | @staticmethod 110 | def deleted( 111 | message: Optional[str] = None 112 | ) -> Dict[str, Any]: 113 | """删除成功响应(200)""" 114 | return ResponseWrapper.success(data=None, message=message, code=200) 115 | 116 | @staticmethod 117 | def no_content() -> Dict[str, Any]: 118 | """无内容响应(204)""" 119 | return ResponseWrapper.success(data=None, message=None, code=204) 120 | 121 | @staticmethod 122 | def pagination( 123 | data: list, 124 | total: int, 125 | page: int, 126 | size: int, 127 | message: Optional[str] = None 128 | ) -> Dict[str, Any]: 129 | """ 130 | 创建分页响应 131 | 132 | Args: 133 | data: 数据列表 134 | total: 总记录数 135 | page: 当前页码 136 | size: 每页大小 137 | message: 响应消息 138 | 139 | Returns: 140 | 统一格式的分页响应 141 | """ 142 | total_pages = (total + size - 1) // size if size > 0 else 0 143 | pagination_data = { 144 | "list": data, 145 | "pagination": { 146 | "total": total, 147 | "page": page, 148 | "size": size, 149 | "totalPages": total_pages, 150 | "hasNext": page < total_pages, 151 | "hasPrev": page > 1 152 | } 153 | } 154 | return ResponseWrapper.success(data=pagination_data, message=message, code=200) 155 | 156 | @staticmethod 157 | def wrap(data: Any, message: Optional[str] = None, code: int = 200) -> Dict[str, Any]: 158 | """ 159 | 包装任意数据为统一格式 160 | 161 | Args: 162 | data: 要包装的数据 163 | message: 响应消息(如果为 None,返回中不包含该字段) 164 | code: HTTP 状态码 165 | 166 | Returns: 167 | 统一格式的响应字典 168 | """ 169 | # 如果已经是统一格式,直接返回 170 | if isinstance(data, dict) and all(key in data for key in ["success", "code"]): 171 | return data 172 | 173 | return ResponseWrapper.success(data=data, message=message, code=code) 174 | 175 | 176 | # 全局响应包装器实例 177 | response = ResponseWrapper() 178 | 179 | -------------------------------------------------------------------------------- /myboot/core/di/decorators.py: -------------------------------------------------------------------------------- 1 | """ 2 | 依赖注入装饰器 3 | 4 | 提供 @inject 装饰器和 Provide 类型提示 5 | """ 6 | 7 | from typing import Any, TypeVar, Generic 8 | from functools import wraps 9 | import inspect 10 | 11 | T = TypeVar('T') 12 | 13 | 14 | class Provide(Generic[T]): 15 | """ 16 | 依赖提供者类型提示 17 | 18 | 用于在类型注解中显式指定需要注入的服务名称 19 | 20 | Example: 21 | @service() 22 | class OrderService: 23 | @inject 24 | def __init__( 25 | self, 26 | user_service: Provide['user_service'], 27 | email_service: Provide['email_service'] 28 | ): 29 | self.user_service = user_service 30 | self.email_service = email_service 31 | """ 32 | def __class_getitem__(cls, item: str) -> str: 33 | """支持 Provide['service_name'] 语法""" 34 | return item 35 | 36 | 37 | def inject(func): 38 | """ 39 | 依赖注入装饰器 40 | 41 | 用于标记需要自动注入依赖的方法(通常是 __init__) 42 | 43 | Example: 44 | @service() 45 | class OrderService: 46 | @inject 47 | def __init__(self, user_service: UserService): 48 | self.user_service = user_service 49 | 50 | Note: 51 | 如果使用类型注解,通常不需要显式使用 @inject 装饰器 52 | 框架会自动检测并注入依赖 53 | """ 54 | if not hasattr(func, '__myboot_inject__'): 55 | func.__myboot_inject__ = True 56 | 57 | @wraps(func) 58 | def wrapper(*args, **kwargs): 59 | return func(*args, **kwargs) 60 | 61 | return wrapper 62 | 63 | 64 | def get_injectable_params(func) -> dict: 65 | """ 66 | 获取可注入的参数信息 67 | 68 | Args: 69 | func: 函数或方法 70 | 71 | Returns: 72 | 参数字典,格式: {param_name: {type, service_name, is_optional, default}} 73 | """ 74 | signature = inspect.signature(func) 75 | params = {} 76 | 77 | for param_name, param in signature.parameters.items(): 78 | if param_name == 'self': 79 | continue 80 | 81 | param_type = param.annotation 82 | is_optional = False 83 | service_name = None 84 | 85 | # 处理类型注解 86 | if param_type != inspect.Parameter.empty: 87 | # 处理 Provide['service_name'] 类型(字符串形式) 88 | if isinstance(param_type, str): 89 | if param_type.startswith("Provide['") and param_type.endswith("']"): 90 | service_name = param_type[9:-2] # 提取 'service_name' 91 | param_type = None 92 | # 处理类型对象 93 | elif hasattr(param_type, '__origin__'): 94 | origin = param_type.__origin__ 95 | args = getattr(param_type, '__args__', ()) 96 | 97 | # 处理 Optional[Type] 或 Union[Type, None] 98 | if origin is type(None) or (hasattr(origin, '__name__') and origin.__name__ == 'Union'): 99 | # 取第一个非 None 的类型 100 | for arg in args: 101 | if arg is not type(None): 102 | param_type = arg 103 | is_optional = True 104 | break 105 | 106 | # 处理 Provide[Type] 泛型 107 | if hasattr(origin, '__name__'): 108 | origin_name = origin.__name__ 109 | if 'Provide' in origin_name or str(origin).startswith('typing.Union') and any('Provide' in str(arg) for arg in args): 110 | # 查找 Provide 类型参数 111 | for arg in args: 112 | if isinstance(arg, str): 113 | service_name = arg 114 | param_type = None 115 | break 116 | elif hasattr(arg, '__origin__') and 'Provide' in str(arg): 117 | # 处理嵌套的 Provide 118 | nested_args = getattr(arg, '__args__', ()) 119 | if nested_args and isinstance(nested_args[0], str): 120 | service_name = nested_args[0] 121 | param_type = None 122 | break 123 | 124 | # 如果没有显式指定服务名,尝试从类型推断 125 | if service_name is None and param_type != inspect.Parameter.empty: 126 | if hasattr(param_type, '__name__'): 127 | # 将类名转换为服务名(驼峰转下划线) 128 | from myboot.core.decorators import _camel_to_snake 129 | service_name = _camel_to_snake(param_type.__name__) 130 | elif isinstance(param_type, type): 131 | # 处理直接的类型对象 132 | from myboot.core.decorators import _camel_to_snake 133 | service_name = _camel_to_snake(param_type.__name__) 134 | 135 | # 检查默认值(表示可选) 136 | if param.default != inspect.Parameter.empty: 137 | is_optional = True 138 | 139 | params[param_name] = { 140 | 'type': param_type, 141 | 'service_name': service_name, 142 | 'is_optional': is_optional, 143 | 'default': param.default if param.default != inspect.Parameter.empty else None 144 | } 145 | 146 | return params 147 | 148 | -------------------------------------------------------------------------------- /docs/rest-api-response-format.md: -------------------------------------------------------------------------------- 1 | # REST API 统一响应格式 2 | 3 | MyBoot 框架提供了统一的 REST API 响应格式封装,确保所有 API 返回一致的格式。 4 | 5 | ## 响应格式结构 6 | 7 | ### 成功响应 8 | 9 | ```json 10 | { 11 | "success": true, 12 | "code": 200, 13 | "message": "操作成功", 14 | "data": { 15 | // 实际业务数据 16 | } 17 | } 18 | ``` 19 | 20 | ### 错误响应 21 | 22 | ```json 23 | { 24 | "success": false, 25 | "code": 422, 26 | "message": "参数校验失败", 27 | "data": { 28 | "fieldErrors": [ 29 | { 30 | "field": "username", 31 | "message": "用户名长度必须在3-20个字符之间" 32 | } 33 | ] 34 | } 35 | } 36 | ``` 37 | 38 | ## 使用方法 39 | 40 | ### 1. 自动格式化(推荐) 41 | 42 | 默认情况下,框架会自动将所有路由的响应包装为统一格式。你只需要在路由函数中返回业务数据即可: 43 | 44 | ```python 45 | from myboot.core.decorators import get, post 46 | from myboot.core.application import app 47 | 48 | @get('/users') 49 | def get_users(): 50 | """获取用户列表""" 51 | users = [{"id": 1, "name": "张三"}, {"id": 2, "name": "李四"}] 52 | return {"users": users} # 自动包装为统一格式 53 | ``` 54 | 55 | 实际返回: 56 | 57 | ```json 58 | { 59 | "success": true, 60 | "code": 200, 61 | "message": "操作成功", 62 | "data": { 63 | "users": [ 64 | { "id": 1, "name": "张三" }, 65 | { "id": 2, "name": "李四" } 66 | ] 67 | } 68 | } 69 | ``` 70 | 71 | ### 2. 手动使用响应包装器 72 | 73 | 如果你需要自定义响应消息,可以使用响应包装器: 74 | 75 | ```python 76 | from myboot.web.response import response 77 | 78 | @get('/users/{user_id}') 79 | def get_user(user_id: int): 80 | """获取单个用户""" 81 | user = {"id": user_id, "name": "张三"} 82 | 83 | # 使用响应包装器 84 | return response.success( 85 | data=user, 86 | message="查询成功", 87 | code=200 88 | ) 89 | ``` 90 | 91 | ### 3. 便捷方法 92 | 93 | 响应包装器提供了多个便捷方法: 94 | 95 | ```python 96 | from myboot.web.response import response 97 | 98 | # 创建成功(201) 99 | @post('/users') 100 | def create_user(name: str, email: str): 101 | user = create_user_service(name, email) 102 | return response.created(data=user, message="用户创建成功") 103 | 104 | # 更新成功 105 | @put('/users/{user_id}') 106 | def update_user(user_id: int, name: str): 107 | user = update_user_service(user_id, name) 108 | return response.updated(data=user, message="用户更新成功") 109 | 110 | # 删除成功 111 | @delete('/users/{user_id}') 112 | def delete_user(user_id: int): 113 | delete_user_service(user_id) 114 | return response.deleted(message="用户删除成功") 115 | 116 | # 分页响应 117 | @get('/users') 118 | def get_users(page: int = 1, size: int = 10): 119 | users, total = get_users_service(page, size) 120 | return response.pagination( 121 | data=users, 122 | total=total, 123 | page=page, 124 | size=size, 125 | message="查询成功" 126 | ) 127 | ``` 128 | 129 | ## 配置选项 130 | 131 | ### 启用/禁用自动格式化 132 | 133 | 在配置文件中设置: 134 | 135 | ```yaml 136 | server: 137 | response_format: 138 | enabled: true # 是否启用自动格式化 139 | exclude_paths: # 排除的路径(这些路径不会自动格式化) 140 | - "/custom/path" 141 | - "/another/path" 142 | ``` 143 | 144 | 或者在代码中(通过配置参数): 145 | 146 | ```python 147 | app = Application( 148 | name="My App" 149 | ) 150 | # 配置在 config.yaml 中设置: 151 | # server: 152 | # response_format: 153 | # enabled: true 154 | # exclude_paths: 155 | # - "/custom/path" 156 | ``` 157 | 158 | ### 默认排除的路径 159 | 160 | 以下路径默认不会被格式化(系统路径和文档路径): 161 | 162 | - `/docs` 163 | - `/openapi.json` 164 | - `/redoc` 165 | - `/health` 166 | - `/health/ready` 167 | - `/health/live` 168 | 169 | ## 异常响应格式 170 | 171 | 框架已经统一了异常响应格式,所有异常都会自动返回统一格式: 172 | 173 | ### 验证错误(422) 174 | 175 | ```json 176 | { 177 | "success": false, 178 | "code": 422, 179 | "message": "Validation Error", 180 | "data": { 181 | "fieldErrors": [ 182 | { 183 | "field": "username", 184 | "message": "用户名格式不正确" 185 | } 186 | ] 187 | } 188 | } 189 | ``` 190 | 191 | ### HTTP 错误(4xx, 5xx) 192 | 193 | ```json 194 | { 195 | "success": false, 196 | "code": 404, 197 | "message": "HTTP Error", 198 | "data": {} 199 | } 200 | ``` 201 | 202 | ### 服务器错误(500) 203 | 204 | ```json 205 | { 206 | "success": false, 207 | "code": 500, 208 | "message": "Internal Server Error", 209 | "data": { 210 | "type": "ExceptionClassName" 211 | } 212 | } 213 | ``` 214 | 215 | ## 注意事项 216 | 217 | 1. **已经是统一格式的响应不会被重复包装**:如果你手动返回统一格式的响应,中间件会检测并直接返回。 218 | 219 | 2. **非 JSON 响应不会格式化**:只有 `JSONResponse` 类型的响应会被格式化。 220 | 221 | 3. **排除路径不会被格式化**:配置在排除列表中的路径不会被自动格式化,适用于需要返回原始格式的接口。 222 | 223 | 4. **中间件执行顺序**:响应格式化中间件是最后添加的,因此会最先执行(FastAPI 中间件是 LIFO)。 224 | 225 | ## 示例代码 226 | 227 | 完整示例: 228 | 229 | ```python 230 | from myboot.core.application import Application 231 | from myboot.core.decorators import get, post, put, delete 232 | from myboot.web.response import response 233 | 234 | app = Application(name="My API") 235 | 236 | # 自动格式化 237 | @get('/users') 238 | def get_users(): 239 | return {"users": [...]} # 自动包装 240 | 241 | # 手动格式化(自定义消息) 242 | @get('/users/{user_id}') 243 | def get_user(user_id: int): 244 | user = get_user_by_id(user_id) 245 | return response.success(data=user, message="查询成功") 246 | 247 | # 创建操作 248 | @post('/users') 249 | def create_user(name: str, email: str): 250 | user = create_user(name, email) 251 | return response.created(data=user, message="用户创建成功") 252 | 253 | # 分页 254 | @get('/posts') 255 | def get_posts(page: int = 1, size: int = 10): 256 | posts, total = get_posts_paged(page, size) 257 | return response.pagination( 258 | data=posts, 259 | total=total, 260 | page=page, 261 | size=size 262 | ) 263 | ``` 264 | -------------------------------------------------------------------------------- /myboot/utils/async_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 3 | """ 4 | 异步执行工具模块 5 | 提供可复用的异步执行方法 6 | """ 7 | 8 | import asyncio 9 | import functools 10 | from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor 11 | from typing import Callable 12 | 13 | from loguru import logger as loguru_logger 14 | 15 | logger = loguru_logger.bind(name='async_utils') 16 | 17 | 18 | class AsyncExecutor: 19 | """异步执行器,提供可复用的异步执行方法""" 20 | 21 | def __init__(self, max_workers: int = 4, executor_type: str = 'thread'): 22 | """ 23 | 初始化异步执行器 24 | 25 | Args: 26 | max_workers: 最大工作线程/进程数 27 | executor_type: 执行器类型,'thread' 或 'process' 28 | """ 29 | self.max_workers = max_workers 30 | self.executor_type = executor_type 31 | self._executor = None 32 | self._loop = None 33 | 34 | @property 35 | def executor(self): 36 | """获取执行器实例""" 37 | if self._executor is None: 38 | if self.executor_type == 'thread': 39 | self._executor = ThreadPoolExecutor(max_workers=self.max_workers) 40 | elif self.executor_type == 'process': 41 | self._executor = ProcessPoolExecutor(max_workers=self.max_workers) 42 | else: 43 | raise ValueError(f"不支持的执行器类型: {self.executor_type}") 44 | return self._executor 45 | 46 | @property 47 | def loop(self): 48 | """获取事件循环""" 49 | if self._loop is None: 50 | try: 51 | self._loop = asyncio.get_event_loop() 52 | except RuntimeError: 53 | self._loop = asyncio.new_event_loop() 54 | asyncio.set_event_loop(self._loop) 55 | return self._loop 56 | 57 | async def run_in_background(self, func: Callable, *args, **kwargs) -> asyncio.Task: 58 | """ 59 | 在后台异步执行函数 60 | 61 | Args: 62 | func: 要执行的函数 63 | *args: 函数参数 64 | **kwargs: 函数关键字参数 65 | 66 | Returns: 67 | asyncio.Task: 异步任务对象 68 | """ 69 | task = asyncio.create_task(self._execute_func(func, *args, **kwargs)) 70 | return task 71 | 72 | async def _execute_func(self, func: Callable, *args, task_name: str = None, **kwargs): 73 | """执行函数的内部方法""" 74 | try: 75 | # 使用提供的任务名称或函数名 76 | display_name = task_name if task_name else func.__name__ 77 | # logger.info(f"开始执行后台任务: {display_name}") 78 | result = await self.loop.run_in_executor( 79 | self.executor, 80 | functools.partial(func, *args, **kwargs) 81 | ) 82 | # logger.info(f"后台任务执行完成: {display_name}") 83 | return result 84 | except Exception as e: 85 | display_name = task_name if task_name else func.__name__ 86 | # logger.error(f"后台任务执行失败: {display_name}, 错误: {e}", exc_info=True) 87 | raise 88 | 89 | def close(self): 90 | """关闭执行器,释放资源""" 91 | if self._executor: 92 | self._executor.shutdown(wait=True) 93 | self._executor = None 94 | # logger.info("异步执行器已关闭") 95 | 96 | 97 | # 全局异步执行器实例 98 | _global_executor = None 99 | 100 | 101 | def get_async_executor(max_workers: int = 4, executor_type: str = 'thread') -> AsyncExecutor: 102 | """ 103 | 获取全局异步执行器实例 104 | 105 | Args: 106 | max_workers: 最大工作线程/进程数 107 | executor_type: 执行器类型 108 | 109 | Returns: 110 | AsyncExecutor: 异步执行器实例 111 | """ 112 | global _global_executor 113 | if _global_executor is None: 114 | _global_executor = AsyncExecutor(max_workers, executor_type) 115 | return _global_executor 116 | 117 | 118 | def async_run(func: Callable, *args, **kwargs) -> asyncio.Task: 119 | """ 120 | 快速启动后台任务的便捷函数 121 | 122 | Args: 123 | func: 要执行的函数 124 | *args: 函数参数 125 | **kwargs: 函数关键字参数,支持 task_name 用于日志显示 126 | 127 | Returns: 128 | asyncio.Task: 异步任务对象 129 | """ 130 | executor = get_async_executor() 131 | # 从kwargs中提取task_name,如果没有则使用函数名 132 | task_name = kwargs.pop('task_name', None) 133 | display_name = task_name if task_name else func.__name__ 134 | 135 | # 创建异步任务,确保在后台运行 136 | task = asyncio.create_task(executor._execute_func(func, *args, task_name=display_name, **kwargs)) 137 | return task 138 | 139 | 140 | # 装饰器:将同步函数转换为异步函数 141 | def async_task(func: Callable) -> Callable: 142 | """ 143 | 装饰器:将同步函数包装为异步函数 144 | 145 | Usage: 146 | @async_task 147 | def my_sync_function(): 148 | # 同步代码 149 | pass 150 | 151 | # 现在可以异步调用 152 | await my_sync_function() 153 | """ 154 | 155 | @functools.wraps(func) 156 | async def wrapper(*args, **kwargs): 157 | executor = get_async_executor() 158 | return await executor._execute_func(func, *args, **kwargs) 159 | 160 | return wrapper 161 | 162 | 163 | # 上下文管理器:自动管理执行器生命周期 164 | class AsyncExecutorContext: 165 | """异步执行器上下文管理器""" 166 | 167 | def __init__(self, max_workers: int = 4, executor_type: str = 'thread'): 168 | self.max_workers = max_workers 169 | self.executor_type = executor_type 170 | self.executor = None 171 | 172 | async def __aenter__(self): 173 | self.executor = AsyncExecutor(self.max_workers, self.executor_type) 174 | return self.executor 175 | 176 | async def __aexit__(self, exc_type, exc_val, exc_tb): 177 | if self.executor: 178 | self.executor.close() 179 | 180 | 181 | # 清理函数 182 | def cleanup_async_executor(): 183 | """清理全局异步执行器""" 184 | global _global_executor 185 | if _global_executor: 186 | _global_executor.close() 187 | _global_executor = None 188 | -------------------------------------------------------------------------------- /examples/dependency_injection_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 依赖注入示例应用 5 | 6 | 展示 MyBoot 框架的依赖注入功能 7 | """ 8 | 9 | import sys 10 | from pathlib import Path 11 | from typing import Optional 12 | 13 | from myboot.core.application import Application 14 | from myboot.core.decorators import service, rest_controller, get, post 15 | 16 | # 添加项目根目录到 Python 路径 17 | project_root = Path(__file__).parent.parent 18 | sys.path.insert(0, str(project_root)) 19 | 20 | # 创建应用实例 21 | app = Application( 22 | name="依赖注入示例", 23 | auto_configuration=True, 24 | auto_discover_package="examples" 25 | ) 26 | 27 | 28 | # ==================== 基础服务层 ==================== 29 | 30 | @service() 31 | class DatabaseClient: 32 | """数据库客户端""" 33 | 34 | def __init__(self): 35 | self.connection = "connected" 36 | print("✅ DatabaseClient 已初始化") 37 | 38 | def query(self, sql: str): 39 | print(f"📊 执行查询: {sql}") 40 | return [{"id": 1, "name": "用户1"}] 41 | 42 | 43 | @service() 44 | class CacheService: 45 | """缓存服务""" 46 | 47 | def __init__(self): 48 | self.cache = {} 49 | print("✅ CacheService 已初始化") 50 | 51 | def get(self, key: str): 52 | return self.cache.get(key) 53 | 54 | def set(self, key: str, value: any): 55 | self.cache[key] = value 56 | 57 | 58 | # ==================== 仓储层 ==================== 59 | 60 | @service() 61 | class UserRepository: 62 | """用户仓储 - 依赖 DatabaseClient""" 63 | 64 | def __init__(self, db: DatabaseClient): 65 | self.db = db 66 | print("✅ UserRepository 已初始化(依赖: DatabaseClient)") 67 | 68 | def find_by_id(self, user_id: int): 69 | result = self.db.query(f"SELECT * FROM users WHERE id = {user_id}") 70 | return result[0] if result else {"id": user_id, "name": f"用户{user_id}"} 71 | 72 | 73 | # ==================== 服务层 ==================== 74 | 75 | @service() 76 | class UserService: 77 | """用户服务 - 依赖 UserRepository 和可选的 CacheService""" 78 | 79 | def __init__( 80 | self, 81 | user_repo: UserRepository, 82 | cache: Optional[CacheService] = None 83 | ): 84 | self.user_repo = user_repo 85 | self.cache = cache 86 | print("✅ UserService 已初始化(依赖: UserRepository, CacheService)") 87 | 88 | def get_user(self, user_id: int): 89 | # 尝试从缓存获取 90 | if self.cache: 91 | cached = self.cache.get(f"user:{user_id}") 92 | if cached: 93 | print(f"📦 从缓存获取用户 {user_id}") 94 | return cached 95 | 96 | # 从数据库获取 97 | user = self.user_repo.find_by_id(user_id) 98 | 99 | # 存入缓存 100 | if self.cache: 101 | self.cache.set(f"user:{user_id}", user) 102 | print(f"💾 用户 {user_id} 已存入缓存") 103 | 104 | return user 105 | 106 | 107 | @service() 108 | class EmailService: 109 | """邮件服务""" 110 | 111 | def __init__(self): 112 | print("✅ EmailService 已初始化") 113 | 114 | def send_email(self, to: str, subject: str, body: str): 115 | print(f"📧 发送邮件到 {to}: {subject} - {body}") 116 | return {"status": "sent", "to": to, "subject": subject} 117 | 118 | 119 | @service() 120 | class OrderService: 121 | """订单服务 - 依赖 UserService 和 EmailService""" 122 | 123 | def __init__(self, user_service: UserService, email_service: EmailService): 124 | self.user_service = user_service 125 | self.email_service = email_service 126 | print("✅ OrderService 已初始化(依赖: UserService, EmailService)") 127 | 128 | def create_order(self, user_id: int, product: str): 129 | # 获取用户信息 130 | user = self.user_service.get_user(user_id) 131 | 132 | # 发送邮件通知 133 | self.email_service.send_email( 134 | to=user.get('email', 'user@example.com'), 135 | subject="订单创建", 136 | body=f"您的订单 {product} 已创建" 137 | ) 138 | 139 | return { 140 | "order_id": 12345, 141 | "user_id": user_id, 142 | "product": product, 143 | "status": "created" 144 | } 145 | 146 | 147 | # ==================== REST 控制器 ==================== 148 | # 注意:路由必须在 @rest_controller 装饰的类中定义,支持依赖注入 149 | 150 | @rest_controller('/') 151 | class HomeController: 152 | """首页控制器""" 153 | 154 | @get('/') 155 | def home(self): 156 | """首页(依赖注入示例)""" 157 | return { 158 | "message": "依赖注入示例应用", 159 | "features": [ 160 | "自动依赖注入", 161 | "多级依赖支持", 162 | "可选依赖支持", 163 | "循环依赖检测", 164 | "REST 控制器" 165 | ], 166 | "endpoints": [ 167 | "GET /api/users/{user_id} - 获取用户信息", 168 | "POST /api/orders - 创建订单" 169 | ] 170 | } 171 | 172 | 173 | @rest_controller('/api/users') 174 | class UserController: 175 | """用户控制器 - 自动注入 UserService""" 176 | 177 | def __init__(self, user_service: UserService): 178 | self.user_service = user_service 179 | 180 | @get('/{user_id}') 181 | def get_user(self, user_id: int): 182 | """获取用户信息 - GET /api/users/{user_id}""" 183 | return self.user_service.get_user(user_id) 184 | 185 | 186 | @rest_controller('/api/orders') 187 | class OrderController: 188 | """订单控制器 - 自动注入 OrderService""" 189 | 190 | def __init__(self, order_service: OrderService): 191 | self.order_service = order_service 192 | 193 | @post('/') 194 | def create_order(self, user_id: int, product: str): 195 | """创建订单 - POST /api/orders""" 196 | return self.order_service.create_order(user_id, product) 197 | 198 | 199 | if __name__ == "__main__": 200 | print("\n" + "="*60) 201 | print("依赖注入示例应用") 202 | print("="*60 + "\n") 203 | 204 | # 运行应用 205 | app.run(host="0.0.0.0", port=8000, reload=False) 206 | 207 | -------------------------------------------------------------------------------- /myboot/core/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | 配置管理模块 3 | 4 | 使用 Dynaconf 提供强大的配置管理功能 5 | 支持远程加载文件、配置文件优先级、环境变量覆盖等 6 | """ 7 | 8 | import os 9 | import tempfile 10 | from pathlib import Path 11 | from typing import Any, Optional 12 | 13 | import requests 14 | from dynaconf import Dynaconf 15 | 16 | 17 | def _find_project_root() -> str: 18 | """查找项目根目录""" 19 | current_dir = Path(__file__).parent.absolute() 20 | 21 | while current_dir.parent != current_dir: 22 | if (current_dir / 'pyproject.toml').exists(): 23 | return str(current_dir) 24 | current_dir = current_dir.parent 25 | 26 | return os.getcwd() 27 | 28 | 29 | def _is_url(path: str) -> bool: 30 | """检查是否为 URL""" 31 | return path and path.startswith(('http://', 'https://')) 32 | 33 | 34 | def _download_config(url: str, cache_dir: str) -> str: 35 | """下载配置文件到缓存""" 36 | import hashlib 37 | 38 | url_hash = hashlib.md5(url.encode()).hexdigest() 39 | cache_path = os.path.join(cache_dir, f"{url_hash}.yaml") 40 | 41 | try: 42 | print(f"正在下载配置文件: {url}") 43 | response = requests.get(url, timeout=30) 44 | response.raise_for_status() 45 | 46 | with open(cache_path, 'w', encoding='utf-8') as f: 47 | f.write(response.text) 48 | 49 | print(f"配置文件已更新并缓存到: {cache_path}") 50 | return cache_path 51 | except Exception as e: 52 | print(f"下载配置文件失败: {e}") 53 | 54 | if os.path.exists(cache_path): 55 | print(f"网络连接失败,使用缓存的配置文件: {cache_path}") 56 | return cache_path 57 | else: 58 | print(f"无可用缓存文件,下载失败: {e}") 59 | raise 60 | 61 | 62 | def _get_config_files(config_file: Optional[str] = None) -> list: 63 | """获取配置文件列表,按优先级排序 64 | 65 | 优先级:环境变量 > 参数指定 > 项目根目录/conf > 项目根目录 66 | """ 67 | project_root = _find_project_root() 68 | cache_dir = os.path.join(tempfile.gettempdir(), 'myboot_config_cache') 69 | os.makedirs(cache_dir, exist_ok=True) 70 | 71 | config_files = [] 72 | added_paths = set() # 用于去重 73 | 74 | # 查找配置文件,优先级:环境变量 > 参数指定 > 项目根目录/conf > 项目根目录 75 | config_paths = [ 76 | # 1. 环境变量指定(最高优先级) 77 | os.getenv('CONFIG_FILE'), 78 | 79 | # 2. 参数指定的配置文件 80 | config_file, 81 | 82 | # 3. 项目根目录/conf/config.yaml 和 config.yml 83 | os.path.join(project_root, 'conf', 'config.yaml'), 84 | os.path.join(project_root, 'conf', 'config.yml'), 85 | 86 | # 4. 项目根目录配置文件 87 | os.path.join(project_root, 'config.yaml'), 88 | os.path.join(project_root, 'config.yml'), 89 | ] 90 | 91 | for config_path in config_paths: 92 | if not config_path: 93 | continue 94 | 95 | # 处理 URL 配置 96 | if _is_url(config_path): 97 | downloaded_path = _download_config(config_path, cache_dir) 98 | if downloaded_path and downloaded_path not in added_paths: 99 | config_files.append(downloaded_path) 100 | added_paths.add(downloaded_path) 101 | # 处理文件路径 102 | elif os.path.exists(config_path) and config_path not in added_paths: 103 | config_files.append(config_path) 104 | added_paths.add(config_path) 105 | 106 | return config_files 107 | 108 | 109 | def create_settings(config_file: Optional[str] = None) -> Dynaconf: 110 | """创建 Dynaconf 设置实例""" 111 | config_files = _get_config_files(config_file) 112 | 113 | # 创建 Dynaconf 配置 114 | settings = Dynaconf( 115 | # 配置文件列表 116 | settings_files=config_files, 117 | 118 | # 环境变量前缀(禁用前缀) 119 | envvar_prefix=False, 120 | 121 | # 环境变量分隔符 122 | envvar_separator="__", 123 | 124 | # 是否自动转换环境变量类型 125 | env_parse_values=True, 126 | 127 | # 是否忽略空值 128 | ignore_unknown_envvars=True, 129 | 130 | # 是否合并环境变量 131 | merge_enabled=True, 132 | 133 | # 默认值 134 | default_settings={ 135 | "app": { 136 | "name": "MyBoot App", 137 | "version": "0.1.0" 138 | }, 139 | "server": { 140 | "host": "0.0.0.0", 141 | "port": 8000, 142 | "reload": True, 143 | "workers": 1, 144 | "keep_alive_timeout": 5, 145 | "graceful_timeout": 30, 146 | "response_format": { 147 | "enabled": True, 148 | "exclude_paths": ["/docs"] 149 | } 150 | }, 151 | "logging": { 152 | "level": "INFO" 153 | }, 154 | "scheduler": { 155 | "enabled": True, 156 | "timezone": "UTC", 157 | "max_workers": 10 158 | } 159 | } 160 | ) 161 | 162 | return settings 163 | 164 | 165 | # 全局配置实例 166 | _settings: Optional[Dynaconf] = None 167 | 168 | 169 | def get_settings(config_file: Optional[str] = None) -> Dynaconf: 170 | """获取 Dynaconf 设置实例""" 171 | global _settings 172 | 173 | if _settings is None: 174 | _settings = create_settings(config_file) 175 | 176 | return _settings 177 | 178 | 179 | # 为了向后兼容,保留一些便捷函数 180 | def get_config(key: str, default: Any = None) -> Any: 181 | """获取配置值的便捷函数""" 182 | return get_settings().get(key, default) 183 | 184 | 185 | def get_config_str(key: str, default: str = "") -> str: 186 | """获取字符串配置值的便捷函数""" 187 | value = get_config(key, default) 188 | return str(value) if value is not None else default 189 | 190 | 191 | def get_config_int(key: str, default: int = 0) -> int: 192 | """获取整数配置值的便捷函数""" 193 | value = get_config(key, default) 194 | try: 195 | return int(value) 196 | except (ValueError, TypeError): 197 | return default 198 | 199 | 200 | def get_config_bool(key: str, default: bool = False) -> bool: 201 | """获取布尔配置值的便捷函数""" 202 | value = get_config(key, default) 203 | if isinstance(value, bool): 204 | return value 205 | if isinstance(value, str): 206 | return value.lower() in ('true', '1', 'yes', 'on') 207 | return bool(value) 208 | 209 | 210 | def reload_config() -> None: 211 | """重新加载配置""" 212 | global _settings 213 | _settings = None -------------------------------------------------------------------------------- /myboot/core/container.py: -------------------------------------------------------------------------------- 1 | """ 2 | 统一容器模块 3 | 4 | 提供统一的容器接口,支持从 container、components、services、clients 中获取实例 5 | """ 6 | 7 | from typing import Any, Dict, List, Type, TYPE_CHECKING 8 | 9 | if TYPE_CHECKING: 10 | from .application import Application 11 | 12 | 13 | class Container: 14 | """统一容器类,支持从 container、components、services、clients 中获取实例""" 15 | 16 | def __init__(self, app: 'Application'): 17 | """ 18 | 初始化容器 19 | 20 | Args: 21 | app: 应用实例 22 | """ 23 | self._app = app 24 | # 容器自己的存储字典 25 | self._storage: Dict[str, Any] = {} 26 | 27 | def put(self, name: str, instance: Any) -> None: 28 | """ 29 | 将实例放入容器 30 | 31 | Args: 32 | name: 实例名称 33 | instance: 实例对象 34 | """ 35 | self._storage[name] = instance 36 | self._app.logger.debug(f"已放入容器: {name} ({type(instance).__name__})") 37 | 38 | def get(self, name: str, default: Any = None) -> Any: 39 | """ 40 | 从容器中获取实例(优先从 container,然后从 components、services,最后从 clients) 41 | 42 | Args: 43 | name: 实例名称 44 | default: 如果不存在时返回的默认值 45 | 46 | Returns: 47 | 实例对象,如果不存在则返回 default 48 | """ 49 | # 优先从 container 中获取 50 | if name in self._storage: 51 | return self._storage[name] 52 | 53 | # 然后从 components 中获取 54 | if name in self._app.components: 55 | return self._app.components[name] 56 | 57 | # 然后从 services 中获取 58 | if name in self._app.services: 59 | return self._app.services[name] 60 | 61 | # 最后从 clients 中获取 62 | if name in self._app.clients: 63 | return self._app.clients[name] 64 | 65 | return default 66 | 67 | def get_or_raise(self, name: str) -> Any: 68 | """ 69 | 从容器中获取实例,如果不存在则抛出异常 70 | 71 | Args: 72 | name: 实例名称 73 | 74 | Returns: 75 | 实例对象 76 | 77 | Raises: 78 | KeyError: 如果实例不存在 79 | """ 80 | instance = self.get(name) 81 | if instance is None: 82 | raise KeyError(f"容器中不存在实例: {name} (已检查 container、components、services、clients)") 83 | return instance 84 | 85 | def has(self, name: str) -> bool: 86 | """ 87 | 检查容器中是否存在指定名称的实例(检查 container、components、services、clients) 88 | 89 | Args: 90 | name: 实例名称 91 | 92 | Returns: 93 | 是否存在 94 | """ 95 | return (name in self._storage or 96 | name in self._app.components or 97 | name in self._app.services or 98 | name in self._app.clients) 99 | 100 | def remove(self, name: str) -> bool: 101 | """ 102 | 从容器中移除实例(按顺序从 container、components、services、clients 中移除) 103 | 104 | Args: 105 | name: 实例名称 106 | 107 | Returns: 108 | 是否成功移除 109 | """ 110 | removed = False 111 | if name in self._storage: 112 | del self._storage[name] 113 | self._app.logger.debug(f"已从容器移除: {name}") 114 | removed = True 115 | if name in self._app.components: 116 | del self._app.components[name] 117 | self._app.logger.debug(f"已从组件移除: {name}") 118 | removed = True 119 | if name in self._app.services: 120 | del self._app.services[name] 121 | self._app.logger.debug(f"已从服务移除: {name}") 122 | removed = True 123 | if name in self._app.clients: 124 | del self._app.clients[name] 125 | self._app.logger.debug(f"已从客户端移除: {name}") 126 | removed = True 127 | return removed 128 | 129 | def get_by_type(self, instance_type: Type) -> List[Any]: 130 | """ 131 | 根据类型获取容器中所有匹配的实例(从 container、components、services、clients 中查找) 132 | 133 | Args: 134 | instance_type: 实例类型 135 | 136 | Returns: 137 | 匹配的实例列表 138 | """ 139 | instances = [] 140 | # 从 container 中查找 141 | instances.extend([instance for instance in self._storage.values() 142 | if isinstance(instance, instance_type)]) 143 | # 从 components 中查找 144 | instances.extend([instance for instance in self._app.components.values() 145 | if isinstance(instance, instance_type)]) 146 | # 从 services 中查找 147 | instances.extend([instance for instance in self._app.services.values() 148 | if isinstance(instance, instance_type)]) 149 | # 从 clients 中查找 150 | instances.extend([instance for instance in self._app.clients.values() 151 | if isinstance(instance, instance_type)]) 152 | return instances 153 | 154 | def list_all(self) -> Dict[str, Any]: 155 | """ 156 | 列出容器中所有实例(包括 container、components、services、clients) 157 | 158 | Returns: 159 | 所有实例的字典(名称 -> 实例) 160 | """ 161 | all_instances = {} 162 | all_instances.update(self._storage) 163 | all_instances.update(self._app.components) 164 | all_instances.update(self._app.services) 165 | all_instances.update(self._app.clients) 166 | return all_instances 167 | 168 | def clear(self) -> None: 169 | """清空容器(清空 container、components、services、clients)""" 170 | self._storage.clear() 171 | self._app.components.clear() 172 | self._app.services.clear() 173 | self._app.clients.clear() 174 | self._app.logger.debug("容器已清空") 175 | 176 | def __getitem__(self, name: str) -> Any: 177 | """支持使用 [] 语法获取实例""" 178 | return self.get_or_raise(name) 179 | 180 | def __setitem__(self, name: str, instance: Any) -> None: 181 | """支持使用 [] 语法设置实例""" 182 | self.put(name, instance) 183 | 184 | def __contains__(self, name: str) -> bool: 185 | """支持使用 in 语法检查实例是否存在""" 186 | return self.has(name) 187 | 188 | def __delitem__(self, name: str) -> None: 189 | """支持使用 del 语法删除实例""" 190 | if not self.remove(name): 191 | raise KeyError(f"容器中不存在实例: {name}") 192 | 193 | -------------------------------------------------------------------------------- /myboot/jobs/scheduled_job.py: -------------------------------------------------------------------------------- 1 | """ 2 | 定时任务基类模块 3 | 4 | 提供 ScheduledJob 基类,用户可以继承此类创建自定义的定时任务 5 | """ 6 | 7 | import asyncio 8 | import time 9 | from abc import ABC, abstractmethod 10 | from datetime import datetime 11 | from typing import Any, Dict, Optional, Union 12 | 13 | from loguru import logger 14 | 15 | 16 | class ScheduledJob(ABC): 17 | """ 18 | 定时任务基类 19 | 20 | 用户可以继承此类创建自定义的定时任务 21 | """ 22 | 23 | def __init__( 24 | self, 25 | name: Optional[str] = None, 26 | description: Optional[str] = None, 27 | trigger: Union[str, Dict[str, Any]] = None, 28 | max_retries: int = 3, 29 | retry_delay: float = 1.0, 30 | timeout: Optional[float] = None 31 | ): 32 | """ 33 | 初始化定时任务 34 | 35 | Args: 36 | name: 任务名称 37 | description: 任务描述 38 | trigger: 触发器,可以是: 39 | - 字符串:cron 表达式(如 "0 0 * * * *") 40 | - 字典:包含 type 和配置的字典 41 | - {'type': 'cron', 'cron': '0 0 * * * *'} 42 | - {'type': 'interval', 'seconds': 60} 43 | - {'type': 'date', 'run_date': '2024-01-01 12:00:00'} 44 | max_retries: 最大重试次数 45 | retry_delay: 重试延迟(秒) 46 | timeout: 超时时间(秒) 47 | """ 48 | self.name = name or self.__class__.__name__ 49 | self.description = description or self.__doc__ or "" 50 | self.trigger = trigger 51 | self.max_retries = max_retries 52 | self.retry_delay = retry_delay 53 | self.timeout = timeout 54 | 55 | # 任务状态 56 | self.status = "pending" # pending, running, completed, failed, cancelled 57 | self.created_at = datetime.now() 58 | self.started_at: Optional[datetime] = None 59 | self.completed_at: Optional[datetime] = None 60 | self.last_run: Optional[datetime] = None 61 | self.run_count = 0 62 | self.failure_count = 0 63 | self.last_error: Optional[Exception] = None 64 | self.job_id: Optional[str] = None 65 | 66 | # 日志 67 | self.logger = logger.bind(name=f"scheduled_job.{self.name}") 68 | 69 | @abstractmethod 70 | def run(self, *args, **kwargs) -> Any: 71 | """ 72 | 执行任务的具体逻辑(子类必须实现) 73 | 74 | Args: 75 | *args: 位置参数 76 | **kwargs: 关键字参数 77 | 78 | Returns: 79 | Any: 任务执行结果 80 | """ 81 | pass 82 | 83 | def execute(self, *args, **kwargs) -> Any: 84 | """ 85 | 执行任务的主入口(包含状态管理和重试逻辑) 86 | 87 | Args: 88 | *args: 位置参数 89 | **kwargs: 关键字参数 90 | 91 | Returns: 92 | Any: 任务执行结果 93 | """ 94 | self.status = "running" 95 | self.started_at = datetime.now() 96 | self.run_count += 1 97 | 98 | self.logger.info(f"开始执行任务: {self.name}") 99 | 100 | try: 101 | # 检查超时 102 | if self.timeout: 103 | result = self._execute_with_timeout(*args, **kwargs) 104 | else: 105 | result = self._execute_task(*args, **kwargs) 106 | 107 | self.status = "completed" 108 | self.completed_at = datetime.now() 109 | self.last_run = self.started_at 110 | 111 | duration = (self.completed_at - self.started_at).total_seconds() 112 | self.logger.info(f"任务执行完成: {self.name}, 耗时: {duration:.2f}秒") 113 | 114 | return result 115 | 116 | except Exception as e: 117 | self.status = "failed" 118 | self.failure_count += 1 119 | self.last_error = e 120 | self.completed_at = datetime.now() 121 | self.last_run = self.started_at 122 | 123 | self.logger.error(f"任务执行失败: {self.name}, 错误: {e}", exc_info=True) 124 | 125 | # 重试逻辑 126 | if self.failure_count <= self.max_retries: 127 | self.logger.info(f"任务将在 {self.retry_delay} 秒后重试: {self.name}") 128 | time.sleep(self.retry_delay) 129 | return self.execute(*args, **kwargs) 130 | else: 131 | self.logger.error(f"任务重试次数已达上限: {self.name}") 132 | raise 133 | 134 | def _execute_with_timeout(self, *args, **kwargs) -> Any: 135 | """带超时的任务执行""" 136 | if asyncio.iscoroutinefunction(self.run): 137 | # 异步任务 138 | loop = asyncio.new_event_loop() 139 | asyncio.set_event_loop(loop) 140 | try: 141 | return loop.run_until_complete( 142 | asyncio.wait_for(self.run(*args, **kwargs), timeout=self.timeout) 143 | ) 144 | finally: 145 | loop.close() 146 | else: 147 | # 同步任务 - 使用 ThreadPoolExecutor 实现跨平台超时 148 | from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError 149 | 150 | def run_task(): 151 | return self.run(*args, **kwargs) 152 | 153 | with ThreadPoolExecutor(max_workers=1) as executor: 154 | future = executor.submit(run_task) 155 | try: 156 | return future.result(timeout=self.timeout) 157 | except FutureTimeoutError: 158 | future.cancel() 159 | raise TimeoutError(f"任务执行超时: {self.name}") 160 | 161 | def _execute_task(self, *args, **kwargs) -> Any: 162 | """执行任务""" 163 | if asyncio.iscoroutinefunction(self.run): 164 | # 异步任务 165 | loop = asyncio.new_event_loop() 166 | asyncio.set_event_loop(loop) 167 | try: 168 | return loop.run_until_complete(self.run(*args, **kwargs)) 169 | finally: 170 | loop.close() 171 | else: 172 | # 同步任务 173 | return self.run(*args, **kwargs) 174 | 175 | def get_info(self) -> Dict[str, Any]: 176 | """获取任务信息""" 177 | return { 178 | "name": self.name, 179 | "description": self.description, 180 | "status": self.status, 181 | "trigger": str(self.trigger) if self.trigger else None, 182 | "created_at": self.created_at.isoformat(), 183 | "started_at": self.started_at.isoformat() if self.started_at else None, 184 | "completed_at": self.completed_at.isoformat() if self.completed_at else None, 185 | "last_run": self.last_run.isoformat() if self.last_run else None, 186 | "run_count": self.run_count, 187 | "failure_count": self.failure_count, 188 | "last_error": str(self.last_error) if self.last_error else None, 189 | "max_retries": self.max_retries, 190 | "retry_delay": self.retry_delay, 191 | "timeout": self.timeout, 192 | "job_id": self.job_id 193 | } 194 | 195 | def reset(self) -> None: 196 | """重置任务状态""" 197 | self.status = "pending" 198 | self.started_at = None 199 | self.completed_at = None 200 | self.failure_count = 0 201 | self.last_error = None 202 | self.logger.info(f"任务状态已重置: {self.name}") 203 | 204 | -------------------------------------------------------------------------------- /myboot/web/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Web 异常模块 3 | 4 | 提供 Web 相关的异常类 5 | """ 6 | 7 | from typing import Any, Dict, Optional 8 | 9 | 10 | class HTTPException(Exception): 11 | """HTTP 异常基类""" 12 | 13 | def __init__( 14 | self, 15 | status_code: int, 16 | message: str = "HTTP Error", 17 | details: Optional[Dict[str, Any]] = None 18 | ): 19 | self.status_code = status_code 20 | self.message = message 21 | self.details = details or {} 22 | super().__init__(self.message) 23 | 24 | 25 | class BadRequestError(HTTPException): 26 | """400 错误请求异常""" 27 | 28 | def __init__(self, message: str = "错误请求", details: Optional[Dict[str, Any]] = None): 29 | super().__init__(400, message, details) 30 | 31 | 32 | class UnauthorizedError(HTTPException): 33 | """401 未授权异常""" 34 | 35 | def __init__(self, message: str = "未授权", details: Optional[Dict[str, Any]] = None): 36 | super().__init__(401, message, details) 37 | 38 | 39 | class ForbiddenError(HTTPException): 40 | """403 禁止访问异常""" 41 | 42 | def __init__(self, message: str = "禁止访问", details: Optional[Dict[str, Any]] = None): 43 | super().__init__(403, message, details) 44 | 45 | 46 | class NotFoundError(HTTPException): 47 | """404 未找到异常""" 48 | 49 | def __init__(self, message: str = "未找到", details: Optional[Dict[str, Any]] = None): 50 | super().__init__(404, message, details) 51 | 52 | 53 | class MethodNotAllowedError(HTTPException): 54 | """405 方法不允许异常""" 55 | 56 | def __init__(self, message: str = "方法不允许", details: Optional[Dict[str, Any]] = None): 57 | super().__init__(405, message, details) 58 | 59 | 60 | class ConflictError(HTTPException): 61 | """409 冲突异常""" 62 | 63 | def __init__(self, message: str = "资源冲突", details: Optional[Dict[str, Any]] = None): 64 | super().__init__(409, message, details) 65 | 66 | 67 | class UnprocessableEntityError(HTTPException): 68 | """422 无法处理的实体异常""" 69 | 70 | def __init__(self, message: str = "无法处理的实体", details: Optional[Dict[str, Any]] = None): 71 | super().__init__(422, message, details) 72 | 73 | 74 | class TooManyRequestsError(HTTPException): 75 | """429 请求过多异常""" 76 | 77 | def __init__(self, message: str = "请求过多", details: Optional[Dict[str, Any]] = None): 78 | super().__init__(429, message, details) 79 | 80 | 81 | class InternalServerError(HTTPException): 82 | """500 内部服务器错误异常""" 83 | 84 | def __init__(self, message: str = "内部服务器错误", details: Optional[Dict[str, Any]] = None): 85 | super().__init__(500, message, details) 86 | 87 | 88 | class ServiceUnavailableError(HTTPException): 89 | """503 服务不可用异常""" 90 | 91 | def __init__(self, message: str = "服务不可用", details: Optional[Dict[str, Any]] = None): 92 | super().__init__(503, message, details) 93 | 94 | 95 | class ValidationError(Exception): 96 | """验证错误异常""" 97 | 98 | def __init__( 99 | self, 100 | message: str = "验证失败", 101 | field: Optional[str] = None, 102 | value: Any = None, 103 | error_type: str = "validation_error" 104 | ): 105 | self.message = message 106 | self.field = field 107 | self.value = value 108 | self.error_type = error_type 109 | super().__init__(self.message) 110 | 111 | 112 | class AuthenticationError(Exception): 113 | """认证错误异常""" 114 | 115 | def __init__(self, message: str = "认证失败"): 116 | self.message = message 117 | super().__init__(self.message) 118 | 119 | 120 | class AuthorizationError(Exception): 121 | """授权错误异常""" 122 | 123 | def __init__(self, message: str = "授权失败"): 124 | self.message = message 125 | super().__init__(self.message) 126 | 127 | 128 | class RateLimitError(Exception): 129 | """限流错误异常""" 130 | 131 | def __init__(self, message: str = "请求频率过高", retry_after: Optional[int] = None): 132 | self.message = message 133 | self.retry_after = retry_after 134 | super().__init__(self.message) 135 | 136 | 137 | class BusinessError(Exception): 138 | """业务错误异常""" 139 | 140 | def __init__( 141 | self, 142 | message: str = "业务处理失败", 143 | code: str = "BUSINESS_ERROR", 144 | details: Optional[Dict[str, Any]] = None 145 | ): 146 | self.message = message 147 | self.code = code 148 | self.details = details or {} 149 | super().__init__(self.message) 150 | 151 | 152 | class ExternalServiceError(Exception): 153 | """外部服务错误异常""" 154 | 155 | def __init__( 156 | self, 157 | service_name: str, 158 | message: str = "外部服务调用失败", 159 | status_code: Optional[int] = None, 160 | details: Optional[Dict[str, Any]] = None 161 | ): 162 | self.service_name = service_name 163 | self.message = message 164 | self.status_code = status_code 165 | self.details = details or {} 166 | super().__init__(self.message) 167 | 168 | 169 | class DatabaseError(Exception): 170 | """数据库错误异常""" 171 | 172 | def __init__( 173 | self, 174 | message: str = "数据库操作失败", 175 | operation: Optional[str] = None, 176 | table: Optional[str] = None, 177 | details: Optional[Dict[str, Any]] = None 178 | ): 179 | self.message = message 180 | self.operation = operation 181 | self.table = table 182 | self.details = details or {} 183 | super().__init__(self.message) 184 | 185 | 186 | class CacheError(Exception): 187 | """缓存错误异常""" 188 | 189 | def __init__( 190 | self, 191 | message: str = "缓存操作失败", 192 | operation: Optional[str] = None, 193 | key: Optional[str] = None, 194 | details: Optional[Dict[str, Any]] = None 195 | ): 196 | self.message = message 197 | self.operation = operation 198 | self.key = key 199 | self.details = details or {} 200 | super().__init__(self.message) 201 | 202 | 203 | class ConfigurationError(Exception): 204 | """配置错误异常""" 205 | 206 | def __init__( 207 | self, 208 | message: str = "配置错误", 209 | config_key: Optional[str] = None, 210 | details: Optional[Dict[str, Any]] = None 211 | ): 212 | self.message = message 213 | self.config_key = config_key 214 | self.details = details or {} 215 | super().__init__(self.message) 216 | 217 | 218 | class TimeoutError(Exception): 219 | """超时错误异常""" 220 | 221 | def __init__( 222 | self, 223 | message: str = "操作超时", 224 | timeout: Optional[float] = None, 225 | operation: Optional[str] = None 226 | ): 227 | self.message = message 228 | self.timeout = timeout 229 | self.operation = operation 230 | super().__init__(self.message) 231 | 232 | 233 | # 异常映射 234 | HTTP_STATUS_EXCEPTIONS = { 235 | 400: BadRequestError, 236 | 401: UnauthorizedError, 237 | 403: ForbiddenError, 238 | 404: NotFoundError, 239 | 405: MethodNotAllowedError, 240 | 409: ConflictError, 241 | 422: UnprocessableEntityError, 242 | 429: TooManyRequestsError, 243 | 500: InternalServerError, 244 | 503: ServiceUnavailableError, 245 | } 246 | 247 | 248 | def create_http_exception(status_code: int, message: str, details: Optional[Dict[str, Any]] = None) -> HTTPException: 249 | """根据状态码创建 HTTP 异常""" 250 | exception_class = HTTP_STATUS_EXCEPTIONS.get(status_code, HTTPException) 251 | return exception_class(status_code, message, details) 252 | -------------------------------------------------------------------------------- /myboot/web/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Web 数据模型 3 | 4 | 提供请求和响应的数据模型 5 | """ 6 | 7 | from datetime import datetime 8 | from typing import Any, Dict, List, Optional 9 | 10 | from pydantic import BaseModel, Field, field_validator 11 | 12 | 13 | class BaseResponse(BaseModel): 14 | """基础响应模型""" 15 | 16 | success: bool = Field(default=True, description="是否成功") 17 | message: str = Field(default="操作成功", description="响应消息") 18 | data: Optional[Any] = Field(default=None, description="响应数据") 19 | timestamp: datetime = Field(default_factory=datetime.now, description="时间戳") 20 | 21 | class Config: 22 | json_encoders = { 23 | datetime: lambda v: v.isoformat() 24 | } 25 | 26 | 27 | class ErrorResponse(BaseResponse): 28 | """错误响应模型""" 29 | 30 | success: bool = Field(default=False, description="是否成功") 31 | message: str = Field(default="操作失败", description="错误消息") 32 | error_code: Optional[str] = Field(default=None, description="错误代码") 33 | details: Optional[Dict[str, Any]] = Field(default=None, description="错误详情") 34 | 35 | 36 | class PaginationRequest(BaseModel): 37 | """分页请求模型""" 38 | 39 | page: int = Field(default=1, ge=1, description="页码") 40 | size: int = Field(default=10, ge=1, le=100, description="每页大小") 41 | sort: Optional[str] = Field(default=None, description="排序字段") 42 | order: str = Field(default="asc", pattern="^(asc|desc)$", description="排序方向") 43 | 44 | @field_validator('page') 45 | def validate_page(cls, v): 46 | if v < 1: 47 | raise ValueError('页码必须大于 0') 48 | return v 49 | 50 | @field_validator('size') 51 | def validate_size(cls, v): 52 | if v < 1 or v > 100: 53 | raise ValueError('每页大小必须在 1-100 之间') 54 | return v 55 | 56 | 57 | class PaginationResponse(BaseResponse): 58 | """分页响应模型""" 59 | 60 | data: List[Any] = Field(default=[], description="数据列表") 61 | pagination: Dict[str, Any] = Field(description="分页信息") 62 | 63 | @classmethod 64 | def create( 65 | cls, 66 | data: List[Any], 67 | total: int, 68 | page: int, 69 | size: int, 70 | message: str = "查询成功" 71 | ) -> "PaginationResponse": 72 | """创建分页响应""" 73 | total_pages = (total + size - 1) // size 74 | 75 | pagination = { 76 | "total": total, 77 | "page": page, 78 | "size": size, 79 | "total_pages": total_pages, 80 | "has_next": page < total_pages, 81 | "has_prev": page > 1 82 | } 83 | 84 | return cls( 85 | success=True, 86 | message=message, 87 | data=data, 88 | pagination=pagination 89 | ) 90 | 91 | 92 | class HealthCheckResponse(BaseModel): 93 | """健康检查响应模型""" 94 | 95 | status: str = Field(description="服务状态") 96 | app: str = Field(description="应用名称") 97 | version: str = Field(description="应用版本") 98 | uptime: str = Field(description="运行时间") 99 | timestamp: datetime = Field(default_factory=datetime.now, description="检查时间") 100 | 101 | class Config: 102 | json_encoders = { 103 | datetime: lambda v: v.isoformat() 104 | } 105 | 106 | 107 | class RequestInfo(BaseModel): 108 | """请求信息模型""" 109 | 110 | method: str = Field(description="HTTP 方法") 111 | url: str = Field(description="请求 URL") 112 | headers: Dict[str, str] = Field(description="请求头") 113 | query_params: Dict[str, Any] = Field(default={}, description="查询参数") 114 | path_params: Dict[str, Any] = Field(default={}, description="路径参数") 115 | body: Optional[Any] = Field(default=None, description="请求体") 116 | client_ip: Optional[str] = Field(default=None, description="客户端 IP") 117 | user_agent: Optional[str] = Field(default=None, description="用户代理") 118 | timestamp: datetime = Field(default_factory=datetime.now, description="请求时间") 119 | 120 | class Config: 121 | json_encoders = { 122 | datetime: lambda v: v.isoformat() 123 | } 124 | 125 | 126 | class ResponseInfo(BaseModel): 127 | """响应信息模型""" 128 | 129 | status_code: int = Field(description="状态码") 130 | headers: Dict[str, str] = Field(description="响应头") 131 | body: Optional[Any] = Field(default=None, description="响应体") 132 | process_time: float = Field(description="处理时间(秒)") 133 | timestamp: datetime = Field(default_factory=datetime.now, description="响应时间") 134 | 135 | class Config: 136 | json_encoders = { 137 | datetime: lambda v: v.isoformat() 138 | } 139 | 140 | 141 | class ValidationErrorDetail(BaseModel): 142 | """验证错误详情模型""" 143 | 144 | field: str = Field(description="字段名") 145 | message: str = Field(description="错误消息") 146 | value: Any = Field(description="字段值") 147 | type: str = Field(description="错误类型") 148 | 149 | 150 | class ValidationErrorResponse(ErrorResponse): 151 | """验证错误响应模型""" 152 | 153 | message: str = Field(default="请求参数验证失败", description="错误消息") 154 | error_code: str = Field(default="VALIDATION_ERROR", description="错误代码") 155 | details: List[ValidationErrorDetail] = Field(description="验证错误详情") 156 | 157 | 158 | class APIError(BaseModel): 159 | """API 错误模型""" 160 | 161 | code: str = Field(description="错误代码") 162 | message: str = Field(description="错误消息") 163 | details: Optional[Dict[str, Any]] = Field(default=None, description="错误详情") 164 | timestamp: datetime = Field(default_factory=datetime.now, description="错误时间") 165 | 166 | class Config: 167 | json_encoders = { 168 | datetime: lambda v: v.isoformat() 169 | } 170 | 171 | 172 | class SuccessResponse(BaseResponse): 173 | """成功响应模型""" 174 | 175 | success: bool = Field(default=True, description="是否成功") 176 | message: str = Field(default="操作成功", description="成功消息") 177 | data: Optional[Any] = Field(default=None, description="响应数据") 178 | 179 | 180 | class CreatedResponse(SuccessResponse): 181 | """创建成功响应模型""" 182 | 183 | message: str = Field(default="创建成功", description="成功消息") 184 | status_code: int = Field(default=201, description="状态码") 185 | 186 | 187 | class UpdatedResponse(SuccessResponse): 188 | """更新成功响应模型""" 189 | 190 | message: str = Field(default="更新成功", description="成功消息") 191 | status_code: int = Field(default=200, description="状态码") 192 | 193 | 194 | class DeletedResponse(SuccessResponse): 195 | """删除成功响应模型""" 196 | 197 | message: str = Field(default="删除成功", description="成功消息") 198 | status_code: int = Field(default=204, description="状态码") 199 | data: Optional[Any] = Field(default=None, description="响应数据") 200 | 201 | 202 | # 便捷函数 203 | def success_response(data: Any = None, message: str = "操作成功") -> SuccessResponse: 204 | """创建成功响应""" 205 | return SuccessResponse(data=data, message=message) 206 | 207 | 208 | def error_response( 209 | message: str = "操作失败", 210 | error_code: str = "UNKNOWN_ERROR", 211 | details: Optional[Dict[str, Any]] = None 212 | ) -> ErrorResponse: 213 | """创建错误响应""" 214 | return ErrorResponse( 215 | message=message, 216 | error_code=error_code, 217 | details=details 218 | ) 219 | 220 | 221 | def validation_error_response(errors: List[ValidationErrorDetail]) -> ValidationErrorResponse: 222 | """创建验证错误响应""" 223 | return ValidationErrorResponse(details=errors) 224 | 225 | 226 | def pagination_response( 227 | data: List[Any], 228 | total: int, 229 | page: int, 230 | size: int, 231 | message: str = "查询成功" 232 | ) -> PaginationResponse: 233 | """创建分页响应""" 234 | return PaginationResponse.create(data, total, page, size, message) 235 | -------------------------------------------------------------------------------- /myboot/web/decorators.py: -------------------------------------------------------------------------------- 1 | """ 2 | Web 路由装饰器 3 | 4 | 提供类似 Spring Boot 的注解式路由功能 5 | """ 6 | 7 | from functools import wraps 8 | from typing import Any, Callable, List, Optional 9 | 10 | from pydantic import BaseModel 11 | 12 | 13 | def route( 14 | path: str, 15 | methods: Optional[List[str]] = None, 16 | response_model: Optional[Any] = None, 17 | status_code: int = 200, 18 | tags: Optional[List[str]] = None, 19 | summary: Optional[str] = None, 20 | description: Optional[str] = None, 21 | **kwargs 22 | ) -> Callable: 23 | """ 24 | 通用路由装饰器 25 | 26 | Args: 27 | path: 路由路径 28 | methods: HTTP 方法列表 29 | response_model: 响应模型 30 | status_code: 状态码 31 | tags: 标签 32 | summary: 摘要 33 | description: 描述 34 | **kwargs: 其他 FastAPI 参数 35 | """ 36 | if methods is None: 37 | methods = ["GET"] 38 | 39 | def decorator(func: Callable) -> Callable: 40 | # 添加路由元数据 41 | func._route_metadata = { 42 | 'path': path, 43 | 'methods': methods, 44 | 'response_model': response_model, 45 | 'status_code': status_code, 46 | 'tags': tags or [], 47 | 'summary': summary or func.__doc__ or "", 48 | 'description': description or func.__doc__ or "", 49 | **kwargs 50 | } 51 | 52 | @wraps(func) 53 | def wrapper(*args, **kwargs): 54 | return func(*args, **kwargs) 55 | 56 | return wrapper 57 | 58 | return decorator 59 | 60 | 61 | def get( 62 | path: str, 63 | response_model: Optional[Any] = None, 64 | status_code: int = 200, 65 | tags: Optional[List[str]] = None, 66 | summary: Optional[str] = None, 67 | description: Optional[str] = None, 68 | **kwargs 69 | ) -> Callable: 70 | """GET 路由装饰器""" 71 | return route( 72 | path=path, 73 | methods=["GET"], 74 | response_model=response_model, 75 | status_code=status_code, 76 | tags=tags, 77 | summary=summary, 78 | description=description, 79 | **kwargs 80 | ) 81 | 82 | 83 | def post( 84 | path: str, 85 | response_model: Optional[Any] = None, 86 | status_code: int = 201, 87 | tags: Optional[List[str]] = None, 88 | summary: Optional[str] = None, 89 | description: Optional[str] = None, 90 | **kwargs 91 | ) -> Callable: 92 | """POST 路由装饰器""" 93 | return route( 94 | path=path, 95 | methods=["POST"], 96 | response_model=response_model, 97 | status_code=status_code, 98 | tags=tags, 99 | summary=summary, 100 | description=description, 101 | **kwargs 102 | ) 103 | 104 | 105 | def put( 106 | path: str, 107 | response_model: Optional[Any] = None, 108 | status_code: int = 200, 109 | tags: Optional[List[str]] = None, 110 | summary: Optional[str] = None, 111 | description: Optional[str] = None, 112 | **kwargs 113 | ) -> Callable: 114 | """PUT 路由装饰器""" 115 | return route( 116 | path=path, 117 | methods=["PUT"], 118 | response_model=response_model, 119 | status_code=status_code, 120 | tags=tags, 121 | summary=summary, 122 | description=description, 123 | **kwargs 124 | ) 125 | 126 | 127 | def delete( 128 | path: str, 129 | response_model: Optional[Any] = None, 130 | status_code: int = 204, 131 | tags: Optional[List[str]] = None, 132 | summary: Optional[str] = None, 133 | description: Optional[str] = None, 134 | **kwargs 135 | ) -> Callable: 136 | """DELETE 路由装饰器""" 137 | return route( 138 | path=path, 139 | methods=["DELETE"], 140 | response_model=response_model, 141 | status_code=status_code, 142 | tags=tags, 143 | summary=summary, 144 | description=description, 145 | **kwargs 146 | ) 147 | 148 | 149 | def patch( 150 | path: str, 151 | response_model: Optional[Any] = None, 152 | status_code: int = 200, 153 | tags: Optional[List[str]] = None, 154 | summary: Optional[str] = None, 155 | description: Optional[str] = None, 156 | **kwargs 157 | ) -> Callable: 158 | """PATCH 路由装饰器""" 159 | return route( 160 | path=path, 161 | methods=["PATCH"], 162 | response_model=response_model, 163 | status_code=status_code, 164 | tags=tags, 165 | summary=summary, 166 | description=description, 167 | **kwargs 168 | ) 169 | 170 | 171 | def validate_request(model: BaseModel) -> Callable: 172 | """ 173 | 请求验证装饰器 174 | 175 | Args: 176 | model: Pydantic 模型类 177 | """ 178 | def decorator(func: Callable) -> Callable: 179 | @wraps(func) 180 | def wrapper(*args, **kwargs): 181 | # 这里可以添加请求验证逻辑 182 | # 在实际应用中,FastAPI 会自动处理 Pydantic 模型验证 183 | return func(*args, **kwargs) 184 | 185 | return wrapper 186 | 187 | return decorator 188 | 189 | 190 | def require_auth(roles: Optional[List[str]] = None) -> Callable: 191 | """ 192 | 身份验证装饰器 193 | 194 | Args: 195 | roles: 需要的角色列表 196 | """ 197 | def decorator(func: Callable) -> Callable: 198 | @wraps(func) 199 | def wrapper(*args, **kwargs): 200 | # 这里可以添加身份验证逻辑 201 | # 在实际应用中,可以集成 JWT、OAuth 等认证方式 202 | return func(*args, **kwargs) 203 | 204 | return wrapper 205 | 206 | return decorator 207 | 208 | 209 | def rate_limit(requests: int = 100, window: int = 60) -> Callable: 210 | """ 211 | 限流装饰器 212 | 213 | Args: 214 | requests: 时间窗口内允许的请求数 215 | window: 时间窗口(秒) 216 | """ 217 | def decorator(func: Callable) -> Callable: 218 | @wraps(func) 219 | def wrapper(*args, **kwargs): 220 | # 这里可以添加限流逻辑 221 | # 在实际应用中,可以集成 Redis 等存储来实现分布式限流 222 | return func(*args, **kwargs) 223 | 224 | return wrapper 225 | 226 | return decorator 227 | 228 | 229 | def cache(ttl: int = 300) -> Callable: 230 | """ 231 | 缓存装饰器 232 | 233 | Args: 234 | ttl: 缓存生存时间(秒) 235 | """ 236 | def decorator(func: Callable) -> Callable: 237 | @wraps(func) 238 | def wrapper(*args, **kwargs): 239 | # 这里可以添加缓存逻辑 240 | # 在实际应用中,可以集成 Redis、Memcached 等缓存系统 241 | return func(*args, **kwargs) 242 | 243 | return wrapper 244 | 245 | return decorator 246 | 247 | 248 | def async_route( 249 | path: str, 250 | methods: Optional[List[str]] = None, 251 | response_model: Optional[Any] = None, 252 | status_code: int = 200, 253 | tags: Optional[List[str]] = None, 254 | summary: Optional[str] = None, 255 | description: Optional[str] = None, 256 | **kwargs 257 | ) -> Callable: 258 | """ 259 | 异步路由装饰器 260 | 261 | 用于标记异步处理函数 262 | """ 263 | if methods is None: 264 | methods = ["GET"] 265 | 266 | def decorator(func: Callable) -> Callable: 267 | # 添加路由元数据 268 | func._route_metadata = { 269 | 'path': path, 270 | 'methods': methods, 271 | 'response_model': response_model, 272 | 'status_code': status_code, 273 | 'tags': tags or [], 274 | 'summary': summary or func.__doc__ or "", 275 | 'description': description or func.__doc__ or "", 276 | 'async': True, 277 | **kwargs 278 | } 279 | 280 | @wraps(func) 281 | async def wrapper(*args, **kwargs): 282 | return await func(*args, **kwargs) 283 | 284 | return wrapper 285 | 286 | return decorator 287 | -------------------------------------------------------------------------------- /myboot/core/di/registry.py: -------------------------------------------------------------------------------- 1 | """ 2 | 服务注册表 3 | 4 | 负责扫描服务、分析依赖关系、构建依赖图 5 | """ 6 | 7 | import inspect 8 | from typing import Dict, List, Type, Set, Optional, Tuple 9 | from collections import defaultdict, deque 10 | from loguru import logger 11 | 12 | from .decorators import get_injectable_params 13 | 14 | 15 | class ServiceRegistry: 16 | """服务注册表""" 17 | 18 | def __init__(self): 19 | """初始化服务注册表""" 20 | self.services: Dict[str, Type] = {} # service_name -> service_class 21 | self.service_configs: Dict[str, dict] = {} # service_name -> config 22 | self.dependencies: Dict[str, Set[str]] = {} # service_name -> set of dependency names 23 | self.dependents: Dict[str, Set[str]] = {} # service_name -> set of dependent names 24 | self._dependency_graph: Optional[Dict[str, Set[str]]] = None 25 | self.known_instances: Set[str] = set() # 已知的外部实例名称(如 Client) 26 | 27 | def register_service(self, service_class: Type, service_name: str, config: dict = None) -> None: 28 | """ 29 | 注册服务类 30 | 31 | Args: 32 | service_class: 服务类 33 | service_name: 服务名称 34 | config: 服务配置 35 | """ 36 | self.services[service_name] = service_class 37 | self.service_configs[service_name] = config or {} 38 | self.dependencies[service_name] = set() 39 | self.dependents[service_name] = set() 40 | 41 | # 分析依赖关系 42 | self._analyze_dependencies(service_name, service_class) 43 | 44 | def _analyze_dependencies(self, service_name: str, service_class: Type) -> None: 45 | """ 46 | 分析服务的依赖关系 47 | 48 | Args: 49 | service_name: 服务名称 50 | service_class: 服务类 51 | """ 52 | if not hasattr(service_class, '__init__'): 53 | return 54 | 55 | init_method = service_class.__init__ 56 | params = get_injectable_params(init_method) 57 | 58 | for param_name, param_info in params.items(): 59 | dep_service_name = param_info['service_name'] 60 | 61 | if dep_service_name: 62 | # 检查依赖的服务是否存在(可能还未注册) 63 | # 先记录依赖关系,稍后在构建依赖图时验证 64 | self.dependencies[service_name].add(dep_service_name) 65 | if dep_service_name not in self.dependents: 66 | self.dependents[dep_service_name] = set() 67 | self.dependents[dep_service_name].add(service_name) 68 | 69 | def build_dependency_graph(self) -> Dict[str, Set[str]]: 70 | """ 71 | 构建依赖关系图 72 | 73 | Returns: 74 | 依赖关系图字典 75 | """ 76 | if self._dependency_graph is not None: 77 | return self._dependency_graph 78 | 79 | self._dependency_graph = {} 80 | 81 | # 验证所有依赖的服务都已注册(排除已知的外部实例如 Client) 82 | known_names = set(self.services.keys()) | self.known_instances 83 | for service_name, deps in self.dependencies.items(): 84 | missing_deps = deps - known_names 85 | if missing_deps: 86 | logger.warning( 87 | f"服务 '{service_name}' 的依赖 '{missing_deps}' 未找到," 88 | f"将在运行时检查" 89 | ) 90 | 91 | # 构建依赖图 92 | for service_name in self.services.keys(): 93 | self._dependency_graph[service_name] = self.dependencies.get(service_name, set()) 94 | 95 | return self._dependency_graph 96 | 97 | def detect_circular_dependencies(self) -> List[List[str]]: 98 | """ 99 | 检测循环依赖 100 | 101 | Returns: 102 | 循环依赖列表,每个元素是一个循环依赖链 103 | """ 104 | graph = self.build_dependency_graph() 105 | cycles = [] 106 | visited = set() 107 | rec_stack = set() 108 | path = [] 109 | 110 | def dfs(node: str) -> None: 111 | if node in rec_stack: 112 | # 找到循环 113 | cycle_start = path.index(node) 114 | cycle = path[cycle_start:] + [node] 115 | cycles.append(cycle) 116 | return 117 | 118 | if node in visited: 119 | return 120 | 121 | visited.add(node) 122 | rec_stack.add(node) 123 | path.append(node) 124 | 125 | for neighbor in graph.get(node, set()): 126 | if neighbor in self.services: # 只检查已注册的服务 127 | dfs(neighbor) 128 | 129 | rec_stack.remove(node) 130 | path.pop() 131 | 132 | for service_name in self.services.keys(): 133 | if service_name not in visited: 134 | dfs(service_name) 135 | 136 | return cycles 137 | 138 | def get_initialization_order(self) -> List[str]: 139 | """ 140 | 获取服务初始化顺序(拓扑排序) 141 | 142 | Returns: 143 | 服务名称列表,按初始化顺序排列 144 | 145 | Raises: 146 | ValueError: 如果存在循环依赖 147 | """ 148 | cycles = self.detect_circular_dependencies() 149 | if cycles: 150 | cycle_str = ' -> '.join(cycles[0]) 151 | raise ValueError( 152 | f"检测到循环依赖: {cycle_str}。" 153 | f"请重构代码以消除循环依赖。" 154 | ) 155 | 156 | graph = self.build_dependency_graph() 157 | in_degree = defaultdict(int) 158 | 159 | # 计算入度 160 | for service_name in self.services.keys(): 161 | in_degree[service_name] = 0 162 | 163 | for service_name, deps in graph.items(): 164 | for dep in deps: 165 | if dep in self.services: # 只考虑已注册的服务 166 | in_degree[service_name] += 1 167 | 168 | # 拓扑排序 169 | queue = deque([name for name, degree in in_degree.items() if degree == 0]) 170 | result = [] 171 | 172 | while queue: 173 | service_name = queue.popleft() 174 | result.append(service_name) 175 | 176 | # 更新依赖此服务的其他服务的入度 177 | for dependent in self.dependents.get(service_name, set()): 178 | if dependent in in_degree: 179 | in_degree[dependent] -= 1 180 | if in_degree[dependent] == 0: 181 | queue.append(dependent) 182 | 183 | # 检查是否所有服务都已处理 184 | if len(result) != len(self.services): 185 | remaining = set(self.services.keys()) - set(result) 186 | logger.warning(f"以下服务无法确定初始化顺序: {remaining}") 187 | # 将剩余的服务添加到末尾 188 | result.extend(remaining) 189 | 190 | return result 191 | 192 | def get_dependencies(self, service_name: str) -> Set[str]: 193 | """ 194 | 获取服务的依赖列表 195 | 196 | Args: 197 | service_name: 服务名称 198 | 199 | Returns: 200 | 依赖的服务名称集合 201 | """ 202 | return self.dependencies.get(service_name, set()) 203 | 204 | def get_dependents(self, service_name: str) -> Set[str]: 205 | """ 206 | 获取依赖此服务的服务列表 207 | 208 | Args: 209 | service_name: 服务名称 210 | 211 | Returns: 212 | 依赖此服务的服务名称集合 213 | """ 214 | return self.dependents.get(service_name, set()) 215 | 216 | def get_service_class(self, service_name: str) -> Optional[Type]: 217 | """ 218 | 获取服务类 219 | 220 | Args: 221 | service_name: 服务名称 222 | 223 | Returns: 224 | 服务类,如果不存在则返回 None 225 | """ 226 | return self.services.get(service_name) 227 | 228 | def get_service_config(self, service_name: str) -> dict: 229 | """ 230 | 获取服务配置 231 | 232 | Args: 233 | service_name: 服务名称 234 | 235 | Returns: 236 | 服务配置字典 237 | """ 238 | return self.service_configs.get(service_name, {}) 239 | 240 | def has_service(self, service_name: str) -> bool: 241 | """ 242 | 检查服务是否已注册 243 | 244 | Args: 245 | service_name: 服务名称 246 | 247 | Returns: 248 | 是否存在 249 | """ 250 | return service_name in self.services 251 | 252 | def clear(self) -> None: 253 | """清空注册表""" 254 | self.services.clear() 255 | self.service_configs.clear() 256 | self.dependencies.clear() 257 | self.dependents.clear() 258 | self._dependency_graph = None 259 | 260 | -------------------------------------------------------------------------------- /myboot/core/di/container.py: -------------------------------------------------------------------------------- 1 | """ 2 | 依赖注入容器 3 | 4 | 管理 dependency_injector Container 和服务的生命周期 5 | """ 6 | 7 | from typing import Dict, Type, Any, Optional 8 | from dependency_injector import containers, providers 9 | from loguru import logger 10 | 11 | from .registry import ServiceRegistry 12 | from .providers import ServiceProvider 13 | 14 | 15 | class DependencyContainer: 16 | """依赖注入容器管理器""" 17 | 18 | def __init__(self): 19 | """初始化依赖注入容器""" 20 | self.container = containers.DynamicContainer() 21 | self.registry = ServiceRegistry() 22 | self.service_providers: Dict[str, ServiceProvider] = {} 23 | self.service_instances: Dict[str, Any] = {} 24 | self._wired = False 25 | 26 | def register_service( 27 | self, 28 | service_class: Type, 29 | service_name: str, 30 | scope: str = ServiceProvider.SINGLETON, 31 | config: dict = None 32 | ) -> None: 33 | """ 34 | 注册服务到容器 35 | 36 | Args: 37 | service_class: 服务类 38 | service_name: 服务名称 39 | scope: 生命周期范围 (singleton/factory) 40 | config: 服务配置 41 | """ 42 | # 注册到注册表 43 | self.registry.register_service(service_class, service_name, config) 44 | 45 | # 创建服务提供者 46 | provider = ServiceProvider(service_class, service_name, scope, **(config or {})) 47 | self.service_providers[service_name] = provider 48 | 49 | def register_instance(self, name: str, instance: Any) -> None: 50 | """ 51 | 注册已创建的实例到容器(用于 Client 等外部创建的实例) 52 | 53 | Args: 54 | name: 实例名称 55 | instance: 实例对象 56 | """ 57 | # 直接缓存实例 58 | self.service_instances[name] = instance 59 | 60 | # 通知注册表(避免依赖检查时产生警告) 61 | self.registry.known_instances.add(name) 62 | 63 | # 创建一个返回已有实例的提供者 64 | di_provider = providers.Object(instance) 65 | setattr(self.container, name, di_provider) 66 | 67 | # 创建占位的 ServiceProvider(用于 has_service 检查) 68 | placeholder = ServiceProvider(type(instance), name, ServiceProvider.SINGLETON) 69 | placeholder._provider = di_provider 70 | self.service_providers[name] = placeholder 71 | 72 | def build_container(self) -> None: 73 | """ 74 | 构建依赖注入容器 75 | 76 | 按照依赖顺序注册所有服务到 Container 77 | """ 78 | # 构建依赖图 79 | self.registry.build_dependency_graph() 80 | 81 | # 获取初始化顺序 82 | try: 83 | init_order = self.registry.get_initialization_order() 84 | except ValueError as e: 85 | logger.error(f"无法构建依赖注入容器: {e}") 86 | raise 87 | 88 | # 获取服务的参数信息,用于正确映射依赖 89 | service_params = {} 90 | for service_name in self.service_providers.keys(): 91 | service_class = self.registry.get_service_class(service_name) 92 | if service_class and hasattr(service_class, '__init__'): 93 | from .decorators import get_injectable_params 94 | params = get_injectable_params(service_class.__init__) 95 | service_params[service_name] = params 96 | 97 | # 按顺序注册服务 98 | for service_name in init_order: 99 | if service_name not in self.service_providers: 100 | continue 101 | 102 | provider = self.service_providers[service_name] 103 | deps = self.registry.get_dependencies(service_name) 104 | 105 | # 构建依赖字典(使用参数名作为键) 106 | dependencies = {} 107 | params = service_params.get(service_name, {}) 108 | 109 | for param_name, param_info in params.items(): 110 | dep_service_name = param_info.get('service_name') 111 | if dep_service_name and dep_service_name in self.service_providers: 112 | # 从容器中获取依赖的提供者 113 | dep_provider = self.service_providers[dep_service_name] 114 | if not dep_provider.get_provider(): 115 | # 如果依赖的提供者还未创建,先创建它 116 | dep_deps = self.registry.get_dependencies(dep_service_name) 117 | dep_dependencies = {} 118 | dep_params = service_params.get(dep_service_name, {}) 119 | for dep_param_name, dep_param_info in dep_params.items(): 120 | nested_dep_name = dep_param_info.get('service_name') 121 | if nested_dep_name and nested_dep_name in self.service_providers: 122 | nested_provider = self.service_providers[nested_dep_name] 123 | if nested_provider.get_provider(): 124 | dep_dependencies[dep_param_name] = nested_provider.get_provider() 125 | dep_provider.create_provider(dep_dependencies if dep_dependencies else None) 126 | 127 | # 使用参数名作为键(dependency_injector 需要参数名匹配) 128 | dependencies[param_name] = dep_provider.get_provider() 129 | 130 | # 创建提供者 131 | di_provider = provider.create_provider(dependencies if dependencies else None) 132 | 133 | # 注册到容器 134 | setattr(self.container, service_name, di_provider) 135 | 136 | logger.debug(f"已注册服务提供者: {service_name} (依赖: {deps})") 137 | 138 | def wire_modules(self, *modules) -> None: 139 | """ 140 | 连接模块以启用依赖注入 141 | 142 | Args: 143 | *modules: 要连接的模块列表 144 | """ 145 | if not self._wired: 146 | self.container.wire(modules=modules) 147 | self._wired = True 148 | logger.debug(f"已连接模块: {modules}") 149 | 150 | def unwire_modules(self) -> None: 151 | """断开模块连接""" 152 | if self._wired: 153 | self.container.unwire() 154 | self._wired = False 155 | logger.debug("已断开模块连接") 156 | 157 | def get_service(self, service_name: str) -> Any: 158 | """ 159 | 获取服务实例 160 | 161 | Args: 162 | service_name: 服务名称 163 | 164 | Returns: 165 | 服务实例 166 | 167 | Raises: 168 | KeyError: 如果服务不存在 169 | """ 170 | # 先检查是否有已缓存的实例(包括通过 register_instance 注册的) 171 | if service_name in self.service_instances: 172 | return self.service_instances[service_name] 173 | 174 | if service_name not in self.service_providers: 175 | raise KeyError(f"服务 '{service_name}' 未注册") 176 | 177 | # 如果是单例,缓存实例 178 | provider = self.service_providers[service_name] 179 | if provider.scope == ServiceProvider.SINGLETON: 180 | di_provider = getattr(self.container, service_name, None) 181 | if di_provider: 182 | self.service_instances[service_name] = di_provider() 183 | else: 184 | raise RuntimeError(f"服务 '{service_name}' 的提供者未正确配置") 185 | return self.service_instances[service_name] 186 | else: 187 | # 工厂模式,每次创建新实例 188 | di_provider = getattr(self.container, service_name, None) 189 | if di_provider: 190 | return di_provider() 191 | else: 192 | raise RuntimeError(f"服务 '{service_name}' 的提供者未正确配置") 193 | 194 | def has_service(self, service_name: str) -> bool: 195 | """ 196 | 检查服务是否已注册 197 | 198 | Args: 199 | service_name: 服务名称 200 | 201 | Returns: 202 | 是否存在 203 | """ 204 | return service_name in self.service_providers or service_name in self.service_instances 205 | 206 | def get_all_services(self) -> Dict[str, Any]: 207 | """ 208 | 获取所有服务实例(仅单例) 209 | 210 | Returns: 211 | 服务名称到实例的字典 212 | """ 213 | result = {} 214 | for service_name in self.service_providers.keys(): 215 | try: 216 | result[service_name] = self.get_service(service_name) 217 | except Exception as e: 218 | logger.warning(f"无法获取服务 '{service_name}': {e}") 219 | return result 220 | 221 | def clear(self) -> None: 222 | """清空容器""" 223 | self.unwire_modules() 224 | self.container = containers.DynamicContainer() 225 | self.registry.clear() 226 | self.service_providers.clear() 227 | self.service_instances.clear() 228 | self._wired = False 229 | 230 | -------------------------------------------------------------------------------- /myboot/utils/common.py: -------------------------------------------------------------------------------- 1 | """ 2 | 公共工具函数 3 | 4 | 包含常用的工具函数和辅助方法 5 | """ 6 | 7 | import hashlib 8 | import secrets 9 | import socket 10 | import re 11 | import uuid 12 | from datetime import datetime, timezone 13 | from typing import Optional, Union 14 | 15 | from loguru import logger as loguru_logger 16 | 17 | logger = loguru_logger.bind(name="utils") 18 | 19 | 20 | def generate_id() -> str: 21 | """生成唯一ID""" 22 | return str(uuid.uuid4()) 23 | 24 | 25 | def get_local_ip() -> str: 26 | """获取本机真实 IP 地址""" 27 | try: 28 | # 连接到一个外部地址来获取本地 IP 29 | # 不实际发送数据,只是用于获取本地 IP 30 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 31 | try: 32 | # 连接到公共 DNS(不需要实际连接成功) 33 | s.connect(('8.8.8.8', 80)) 34 | ip = s.getsockname()[0] 35 | finally: 36 | s.close() 37 | return ip 38 | except Exception: 39 | # 如果失败,尝试使用主机名 40 | try: 41 | hostname = socket.gethostname() 42 | ip = socket.gethostbyname(hostname) 43 | # 如果不是有效的 IP(可能是 127.0.0.1),返回 localhost 44 | if ip.startswith('127.'): 45 | return 'localhost' 46 | return ip 47 | except Exception: 48 | return 'localhost' 49 | 50 | 51 | def generate_short_id(length: int = 8) -> str: 52 | """生成短ID""" 53 | return secrets.token_urlsafe(length) 54 | 55 | 56 | def format_datetime(dt: datetime, format_str: str = "%Y-%m-%d %H:%M:%S") -> str: 57 | """ 58 | 格式化日期时间 59 | 60 | Args: 61 | dt: 日期时间对象 62 | format_str: 格式字符串 63 | 64 | Returns: 65 | str: 格式化后的字符串 66 | """ 67 | if dt is None: 68 | return "" 69 | 70 | return dt.strftime(format_str) 71 | 72 | 73 | def parse_datetime(date_str: str, format_str: str = "%Y-%m-%d %H:%M:%S") -> Optional[datetime]: 74 | """ 75 | 解析日期时间字符串 76 | 77 | Args: 78 | date_str: 日期时间字符串 79 | format_str: 格式字符串 80 | 81 | Returns: 82 | Optional[datetime]: 解析后的日期时间对象 83 | """ 84 | try: 85 | return datetime.strptime(date_str, format_str) 86 | except ValueError: 87 | logger.warning(f"无法解析日期时间: {date_str}") 88 | return None 89 | 90 | 91 | def validate_email(email: str) -> bool: 92 | """ 93 | 验证邮箱格式 94 | 95 | Args: 96 | email: 邮箱地址 97 | 98 | Returns: 99 | bool: 是否有效 100 | """ 101 | pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' 102 | return bool(re.match(pattern, email)) 103 | 104 | 105 | def validate_phone(phone: str) -> bool: 106 | """ 107 | 验证手机号格式 108 | 109 | Args: 110 | phone: 手机号 111 | 112 | Returns: 113 | bool: 是否有效 114 | """ 115 | pattern = r'^1[3-9]\d{9}$' 116 | return bool(re.match(pattern, phone)) 117 | 118 | 119 | def hash_password(password: str) -> str: 120 | """ 121 | 哈希密码 122 | 123 | Args: 124 | password: 原始密码 125 | 126 | Returns: 127 | str: 哈希后的密码 128 | """ 129 | salt = secrets.token_hex(16) 130 | password_hash = hashlib.pbkdf2_hmac( 131 | 'sha256', 132 | password.encode('utf-8'), 133 | salt.encode('utf-8'), 134 | 100000 135 | ) 136 | return f"{salt}:{password_hash.hex()}" 137 | 138 | 139 | def verify_password(password: str, password_hash: str) -> bool: 140 | """ 141 | 验证密码 142 | 143 | Args: 144 | password: 原始密码 145 | password_hash: 哈希后的密码 146 | 147 | Returns: 148 | bool: 是否匹配 149 | """ 150 | try: 151 | salt, hash_value = password_hash.split(':') 152 | password_hash_check = hashlib.pbkdf2_hmac( 153 | 'sha256', 154 | password.encode('utf-8'), 155 | salt.encode('utf-8'), 156 | 100000 157 | ) 158 | return password_hash_check.hex() == hash_value 159 | except ValueError: 160 | return False 161 | 162 | 163 | def generate_token(length: int = 32) -> str: 164 | """ 165 | 生成随机令牌 166 | 167 | Args: 168 | length: 令牌长度 169 | 170 | Returns: 171 | str: 随机令牌 172 | """ 173 | return secrets.token_urlsafe(length) 174 | 175 | 176 | def generate_api_key() -> str: 177 | """生成API密钥""" 178 | return f"pk_{secrets.token_urlsafe(32)}" 179 | 180 | 181 | def mask_sensitive_data(data: str, mask_char: str = "*", visible_chars: int = 4) -> str: 182 | """ 183 | 遮蔽敏感数据 184 | 185 | Args: 186 | data: 原始数据 187 | mask_char: 遮蔽字符 188 | visible_chars: 可见字符数 189 | 190 | Returns: 191 | str: 遮蔽后的数据 192 | """ 193 | if len(data) <= visible_chars: 194 | return mask_char * len(data) 195 | 196 | return data[:visible_chars] + mask_char * (len(data) - visible_chars) 197 | 198 | 199 | def safe_get(data: dict, key: str, default: any = None) -> any: 200 | """ 201 | 安全获取字典值 202 | 203 | Args: 204 | data: 字典数据 205 | key: 键,支持点号分隔的嵌套键 206 | default: 默认值 207 | 208 | Returns: 209 | any: 值或默认值 210 | """ 211 | keys = key.split('.') 212 | value = data 213 | 214 | try: 215 | for k in keys: 216 | value = value[k] 217 | return value 218 | except (KeyError, TypeError): 219 | return default 220 | 221 | 222 | def chunk_list(lst: list, chunk_size: int) -> list: 223 | """ 224 | 将列表分块 225 | 226 | Args: 227 | lst: 原始列表 228 | chunk_size: 块大小 229 | 230 | Returns: 231 | list: 分块后的列表 232 | """ 233 | return [lst[i:i + chunk_size] for i in range(0, len(lst), chunk_size)] 234 | 235 | 236 | def remove_none_values(data: dict) -> dict: 237 | """ 238 | 移除字典中的None值 239 | 240 | Args: 241 | data: 原始字典 242 | 243 | Returns: 244 | dict: 清理后的字典 245 | """ 246 | return {k: v for k, v in data.items() if v is not None} 247 | 248 | 249 | def deep_merge_dict(dict1: dict, dict2: dict) -> dict: 250 | """ 251 | 深度合并字典 252 | 253 | Args: 254 | dict1: 第一个字典 255 | dict2: 第二个字典 256 | 257 | Returns: 258 | dict: 合并后的字典 259 | """ 260 | result = dict1.copy() 261 | 262 | for key, value in dict2.items(): 263 | if key in result and isinstance(result[key], dict) and isinstance(value, dict): 264 | result[key] = deep_merge_dict(result[key], value) 265 | else: 266 | result[key] = value 267 | 268 | return result 269 | 270 | 271 | def get_current_timestamp() -> int: 272 | """获取当前时间戳(毫秒)""" 273 | return int(datetime.now(timezone.utc).timestamp() * 1000) 274 | 275 | 276 | def format_file_size(size_bytes: int) -> str: 277 | """ 278 | 格式化文件大小 279 | 280 | Args: 281 | size_bytes: 字节数 282 | 283 | Returns: 284 | str: 格式化后的大小 285 | """ 286 | if size_bytes == 0: 287 | return "0 B" 288 | 289 | size_names = ["B", "KB", "MB", "GB", "TB"] 290 | i = 0 291 | while size_bytes >= 1024 and i < len(size_names) - 1: 292 | size_bytes /= 1024.0 293 | i += 1 294 | 295 | return f"{size_bytes:.1f} {size_names[i]}" 296 | 297 | 298 | def is_valid_url(url: str) -> bool: 299 | """ 300 | 验证URL格式 301 | 302 | Args: 303 | url: URL字符串 304 | 305 | Returns: 306 | bool: 是否有效 307 | """ 308 | pattern = r'^https?://(?:[-\w.])+(?:\:[0-9]+)?(?:/(?:[\w/_.])*(?:\?(?:[\w&=%.])*)?(?:\#(?:[\w.])*)?)?$' 309 | return bool(re.match(pattern, url)) 310 | 311 | 312 | class RetryHelper: 313 | """重试辅助类""" 314 | 315 | def __init__(self, max_retries: int = 3, delay: float = 1.0, backoff_factor: float = 2.0): 316 | self.max_retries = max_retries 317 | self.delay = delay 318 | self.backoff_factor = backoff_factor 319 | 320 | def execute(self, func, *args, **kwargs): 321 | """执行带重试的函数""" 322 | import time 323 | 324 | last_exception = None 325 | current_delay = self.delay 326 | 327 | for attempt in range(self.max_retries + 1): 328 | try: 329 | return func(*args, **kwargs) 330 | except Exception as e: 331 | last_exception = e 332 | 333 | if attempt < self.max_retries: 334 | logger.warning(f"执行失败,{current_delay}秒后重试 (尝试 {attempt + 1}/{self.max_retries + 1}): {e}") 335 | time.sleep(current_delay) 336 | current_delay *= self.backoff_factor 337 | else: 338 | logger.error(f"执行失败,已达到最大重试次数: {e}") 339 | 340 | raise last_exception 341 | -------------------------------------------------------------------------------- /docs/scheduler_refactor_analysis.md: -------------------------------------------------------------------------------- 1 | # 调度器重构可行性分析:使用 APScheduler 2 | 3 | ## 1. 当前实现分析 4 | 5 | ### 1.1 现有功能 6 | - **任务类型支持**: 7 | - Cron 任务(6位表达式:秒 分 时 日 月 周) 8 | - 间隔任务(interval,支持秒/分/小时) 9 | - 一次性任务(date,指定日期时间执行) 10 | 11 | - **API 接口**: 12 | - `add_cron_job(func, cron, job_id, **kwargs)` 13 | - `add_interval_job(func, interval, job_id, **kwargs)` 14 | - `add_date_job(func, run_date, job_id, **kwargs)` 15 | - `remove_job(job_id)` 16 | - `get_job(job_id)` 17 | - `list_jobs()` 18 | - `start()` / `stop()` 19 | - `add_scheduled_job(job: ScheduledJob)` 20 | 21 | - **集成点**: 22 | - 与 `Application` 生命周期集成(启动/停止) 23 | - 自动配置系统自动注册装饰器标记的任务 24 | - 支持 `ScheduledJob` 类对象 25 | - 配置支持(时区、最大工作线程数、启用/禁用) 26 | 27 | ### 1.2 当前实现的问题 28 | - 自定义实现,维护成本高 29 | - Cron 表达式解析逻辑复杂,可能存在边界情况 30 | - 时区处理需要手动实现(依赖 pytz) 31 | - 任务执行状态跟踪有限 32 | - 缺少任务持久化能力 33 | - 错误处理和重试机制简单 34 | 35 | ## 2. APScheduler 能力分析 36 | 37 | ### 2.1 APScheduler 核心特性 38 | - **触发器(Triggers)**: 39 | - `CronTrigger`:完整的 Cron 表达式支持(5位或6位) 40 | - `IntervalTrigger`:固定间隔执行 41 | - `DateTrigger`:指定日期时间执行 42 | 43 | - **执行器(Executors)**: 44 | - `ThreadPoolExecutor`:线程池执行 45 | - `ProcessPoolExecutor`:进程池执行 46 | - `AsyncIOExecutor`:异步执行 47 | - `GeventExecutor`:Gevent 协程执行 48 | 49 | - **任务存储(Job Stores)**: 50 | - `MemoryJobStore`:内存存储(默认) 51 | - `SQLAlchemyJobStore`:数据库持久化 52 | - `RedisJobStore`:Redis 持久化 53 | - `MongoDBJobStore`:MongoDB 持久化 54 | 55 | - **调度器(Schedulers)**: 56 | - `BlockingScheduler`:阻塞式调度器 57 | - `BackgroundScheduler`:后台线程调度器(推荐) 58 | - `AsyncIOScheduler`:异步调度器 59 | - `GeventScheduler`:Gevent 调度器 60 | - `TornadoScheduler`:Tornado 调度器 61 | - `TwistedScheduler`:Twisted 调度器 62 | 63 | ### 2.2 APScheduler 优势 64 | - ✅ 成熟稳定,社区活跃 65 | - ✅ 完整的 Cron 表达式支持(包括复杂表达式) 66 | - ✅ 内置时区支持(无需手动处理) 67 | - ✅ 任务持久化能力 68 | - ✅ 丰富的任务状态和事件监听 69 | - ✅ 更好的错误处理和日志 70 | - ✅ 支持任务暂停/恢复 71 | - ✅ 支持任务修改(修改触发器) 72 | 73 | ## 3. 功能映射关系 74 | 75 | ### 3.1 任务类型映射 76 | 77 | | 当前实现 | APScheduler | 映射方式 | 78 | |---------|------------|---------| 79 | | `add_cron_job(func, cron, ...)` | `scheduler.add_job(func, CronTrigger.from_crontab(cron), ...)` | ✅ 直接映射 | 80 | | `add_interval_job(func, interval, ...)` | `scheduler.add_job(func, IntervalTrigger(seconds=interval), ...)` | ✅ 直接映射 | 81 | | `add_date_job(func, run_date, ...)` | `scheduler.add_job(func, DateTrigger(run_date=...), ...)` | ✅ 直接映射 | 82 | 83 | ### 3.2 API 接口映射 84 | 85 | | 当前方法 | APScheduler 对应方法 | 兼容性 | 86 | |---------|-------------------|--------| 87 | | `add_cron_job()` | `add_job(..., trigger=CronTrigger(...))` | ✅ 可封装保持兼容 | 88 | | `add_interval_job()` | `add_job(..., trigger=IntervalTrigger(...))` | ✅ 可封装保持兼容 | 89 | | `add_date_job()` | `add_job(..., trigger=DateTrigger(...))` | ✅ 可封装保持兼容 | 90 | | `remove_job(job_id)` | `remove_job(job_id)` | ✅ 完全兼容 | 91 | | `get_job(job_id)` | `get_job(job_id)` | ✅ 完全兼容 | 92 | | `list_jobs()` | `get_jobs()` | ✅ 可封装保持兼容 | 93 | | `start()` | `start()` | ✅ 完全兼容 | 94 | | `stop()` | `shutdown()` | ⚠️ 需要封装(方法名不同) | 95 | 96 | ### 3.3 配置映射 97 | 98 | | 当前配置 | APScheduler 配置 | 映射方式 | 99 | |---------|-----------------|---------| 100 | | `scheduler.enabled` | 通过 `start()` 控制 | ✅ 逻辑控制 | 101 | | `scheduler.timezone` | `timezone` 参数 | ✅ 直接映射 | 102 | | `scheduler.max_workers` | `executors['default']['max_workers']` | ✅ 直接映射 | 103 | 104 | ## 4. 重构方案设计 105 | 106 | ### 4.1 方案一:完全封装(推荐) 107 | 108 | **设计思路**:保持现有 API 接口不变,内部使用 APScheduler 实现。 109 | 110 | **优点**: 111 | - ✅ 完全向后兼容,现有代码无需修改 112 | - ✅ 可以逐步迁移,降低风险 113 | - ✅ 保留自定义扩展能力 114 | 115 | **实现要点**: 116 | ```python 117 | from apscheduler.schedulers.background import BackgroundScheduler 118 | from apscheduler.triggers.cron import CronTrigger 119 | from apscheduler.triggers.interval import IntervalTrigger 120 | from apscheduler.triggers.date import DateTrigger 121 | 122 | class Scheduler: 123 | def __init__(self, config): 124 | # 创建 APScheduler 实例 125 | self._scheduler = BackgroundScheduler( 126 | timezone=config.get('scheduler.timezone', 'UTC'), 127 | executors={ 128 | 'default': { 129 | 'type': 'threadpool', 130 | 'max_workers': config.get('scheduler.max_workers', 10) 131 | } 132 | } 133 | ) 134 | # 保持兼容性的映射 135 | self._jobs = {} # job_id -> APScheduler Job 136 | self._scheduled_jobs = {} # ScheduledJob 对象 137 | 138 | def add_cron_job(self, func, cron, job_id=None, **kwargs): 139 | # 转换 cron 表达式格式(6位 -> APScheduler 格式) 140 | trigger = self._parse_cron(cron) 141 | job = self._scheduler.add_job( 142 | func, 143 | trigger=trigger, 144 | id=job_id, 145 | **kwargs 146 | ) 147 | self._jobs[job_id] = job 148 | return job_id 149 | ``` 150 | 151 | ### 4.2 方案二:直接使用 APScheduler 152 | 153 | **设计思路**:直接暴露 APScheduler 的 API,简化封装层。 154 | 155 | **优点**: 156 | - ✅ 代码更简洁 157 | - ✅ 可以直接使用 APScheduler 的高级特性 158 | 159 | **缺点**: 160 | - ❌ 需要修改现有代码 161 | - ❌ 破坏向后兼容性 162 | 163 | ### 4.3 推荐方案:方案一(完全封装) 164 | 165 | **理由**: 166 | 1. 保持 API 兼容性,现有代码无需修改 167 | 2. 可以逐步增强功能(如添加持久化) 168 | 3. 保留扩展空间 169 | 170 | ## 5. 实施步骤 171 | 172 | ### 5.1 阶段一:基础重构(保持兼容) 173 | 1. **创建新的 Scheduler 类**(基于 APScheduler) 174 | - 实现所有现有 API 方法 175 | - 保持方法签名和返回值一致 176 | - 内部使用 APScheduler 177 | 178 | 2. **Cron 表达式转换** 179 | - 当前格式:`"秒 分 时 日 月 周"`(6位) 180 | - APScheduler 格式:`"分 时 日 月 周"`(5位)或 `CronTrigger` 对象 181 | - 需要实现转换函数 182 | 183 | 3. **测试验证** 184 | - 单元测试覆盖所有 API 185 | - 集成测试验证现有功能 186 | - 确保装饰器和自动配置正常工作 187 | 188 | ### 5.2 阶段二:功能增强(可选) 189 | 1. **添加任务持久化** 190 | - 支持 SQLAlchemyJobStore 191 | - 支持 RedisJobStore 192 | 193 | 2. **增强任务管理** 194 | - 任务暂停/恢复 195 | - 任务修改触发器 196 | - 任务执行历史 197 | 198 | 3. **事件监听** 199 | - 任务执行成功/失败事件 200 | - 任务错过执行事件 201 | 202 | ### 5.3 阶段三:优化和文档 203 | 1. 性能优化 204 | 2. 文档更新 205 | 3. 示例代码更新 206 | 207 | ## 6. 关键技术点 208 | 209 | ### 6.1 Cron 表达式转换 210 | 211 | **当前格式**(6位): 212 | ``` 213 | "秒 分 时 日 月 周" 214 | 例如:"0 0 * * * *" # 每小时 215 | ``` 216 | 217 | **APScheduler 格式**(5位或 CronTrigger): 218 | ```python 219 | # 方式1:5位字符串(标准 Cron) 220 | "分 时 日 月 周" 221 | "0 * * * *" # 每小时 222 | 223 | # 方式2:CronTrigger 对象(推荐) 224 | CronTrigger(second=0, minute=0, hour='*', day='*', month='*', day_of_week='*') 225 | ``` 226 | 227 | **转换函数**: 228 | ```python 229 | def _parse_cron(self, cron_expr: str) -> CronTrigger: 230 | """将6位 Cron 表达式转换为 APScheduler CronTrigger""" 231 | parts = cron_expr.split() 232 | if len(parts) == 6: 233 | second, minute, hour, day, month, weekday = parts 234 | return CronTrigger( 235 | second=second, 236 | minute=minute, 237 | hour=hour, 238 | day=day, 239 | month=month, 240 | day_of_week=weekday 241 | ) 242 | elif len(parts) == 5: 243 | # 标准5位格式,秒默认为0 244 | return CronTrigger.from_crontab(cron_expr) 245 | else: 246 | raise ValueError(f"无效的 Cron 表达式: {cron_expr}") 247 | ``` 248 | 249 | ### 6.2 ScheduledJob 集成 250 | 251 | 需要将 `ScheduledJob.execute()` 方法适配到 APScheduler: 252 | 253 | ```python 254 | def add_scheduled_job(self, job: ScheduledJob, job_id: Optional[str] = None): 255 | # 包装 execute 方法,保持状态跟踪 256 | def wrapped_execute(): 257 | return job.execute() 258 | 259 | # 根据 trigger 类型添加任务 260 | if isinstance(job.trigger, str): 261 | return self.add_cron_job(wrapped_execute, job.trigger, job_id) 262 | # ... 其他类型 263 | ``` 264 | 265 | ### 6.3 时区处理 266 | 267 | APScheduler 内置时区支持,无需手动处理: 268 | 269 | ```python 270 | from pytz import timezone 271 | 272 | scheduler = BackgroundScheduler( 273 | timezone=timezone('Asia/Shanghai') # 直接支持时区对象 274 | ) 275 | ``` 276 | 277 | ### 6.4 任务状态管理 278 | 279 | APScheduler 提供任务状态: 280 | - `pending`:等待执行 281 | - `running`:正在执行 282 | - `completed`:已完成 283 | - `failed`:执行失败 284 | 285 | 可以通过 `get_job(job_id).next_run_time` 获取下次执行时间。 286 | 287 | ## 7. 风险评估 288 | 289 | ### 7.1 兼容性风险 290 | - **风险**:Cron 表达式格式差异 291 | - **缓解**:实现转换函数,确保兼容 292 | 293 | ### 7.2 行为差异风险 294 | - **风险**:APScheduler 的执行时机可能与当前实现略有差异 295 | - **缓解**:充分测试,特别是边界情况 296 | 297 | ### 7.3 性能风险 298 | - **风险**:APScheduler 可能有额外的开销 299 | - **缓解**:性能测试,通常 APScheduler 性能更好 300 | 301 | ## 8. 测试策略 302 | 303 | ### 8.1 单元测试 304 | - 所有 API 方法的测试 305 | - Cron 表达式转换测试 306 | - 任务添加/删除测试 307 | 308 | ### 8.2 集成测试 309 | - 装饰器自动注册测试 310 | - ScheduledJob 集成测试 311 | - 应用生命周期集成测试 312 | 313 | ### 8.3 兼容性测试 314 | - 现有示例代码运行测试 315 | - 现有项目迁移测试 316 | 317 | ## 9. 结论 318 | 319 | ### 9.1 可行性评估 320 | ✅ **高度可行** 321 | 322 | **理由**: 323 | 1. APScheduler 功能完全覆盖现有需求 324 | 2. API 可以完全兼容 325 | 3. 项目已依赖 APScheduler 326 | 4. 重构收益明显(稳定性、功能、维护性) 327 | 328 | ### 9.2 推荐方案 329 | **采用方案一(完全封装)**,分阶段实施: 330 | 1. 第一阶段:基础重构,保持兼容 331 | 2. 第二阶段:功能增强(可选) 332 | 3. 第三阶段:优化和文档 333 | 334 | ### 9.3 预期收益 335 | - ✅ 减少代码量(约 200+ 行 → 100+ 行) 336 | - ✅ 提高稳定性(使用成熟库) 337 | - ✅ 增强功能(持久化、事件监听等) 338 | - ✅ 降低维护成本 339 | - ✅ 更好的错误处理 340 | 341 | ### 9.4 实施建议 342 | 1. **先创建新实现**,保留旧实现作为备份 343 | 2. **充分测试**后再替换 344 | 3. **逐步迁移**,可以先支持两种实现并存 345 | 4. **文档更新**,说明新特性 346 | 347 | ## 10. 参考资源 348 | 349 | - APScheduler 官方文档:https://apscheduler.readthedocs.io/ 350 | - APScheduler GitHub:https://github.com/agronholm/apscheduler 351 | - Cron 表达式参考:https://en.wikipedia.org/wiki/Cron 352 | 353 | -------------------------------------------------------------------------------- /myboot/core/decorators.py: -------------------------------------------------------------------------------- 1 | """ 2 | 装饰器模块 3 | 4 | 提供约定优于配置的装饰器,用于自动注册组件 5 | """ 6 | 7 | import re 8 | from functools import wraps 9 | from typing import Any, Dict, List, Optional, Union, Callable 10 | 11 | 12 | def _camel_to_snake(name: str) -> str: 13 | """ 14 | 将驼峰命名转换为下划线分隔的小写形式 15 | 16 | Args: 17 | name: 类名(驼峰命名) 18 | 19 | Returns: 20 | 下划线分隔的小写字符串 21 | 22 | Examples: 23 | UserService -> user_service 24 | EmailService -> email_service 25 | DatabaseClient -> database_client 26 | RedisClient -> redis_client 27 | """ 28 | # 在大写字母前插入下划线(除了第一个字符) 29 | s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) 30 | # 处理连续大写字母的情况(如 HTTPClient) 31 | s2 = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1) 32 | # 转换为小写 33 | return s2.lower() 34 | 35 | 36 | def route(path: str, methods: List[str] = None, **kwargs): 37 | """ 38 | 路由装饰器 39 | 40 | Args: 41 | path: 路由路径 42 | methods: HTTP 方法列表 43 | **kwargs: 其他路由参数 44 | """ 45 | if methods is None: 46 | methods = ['GET'] 47 | 48 | def decorator(func): 49 | func.__myboot_route__ = { 50 | 'path': path, 51 | 'methods': methods, 52 | 'kwargs': kwargs 53 | } 54 | return func 55 | return decorator 56 | 57 | 58 | def get(path: str, **kwargs): 59 | """GET 路由装饰器""" 60 | return route(path, methods=['GET'], **kwargs) 61 | 62 | 63 | def post(path: str, **kwargs): 64 | """POST 路由装饰器""" 65 | return route(path, methods=['POST'], **kwargs) 66 | 67 | 68 | def put(path: str, **kwargs): 69 | """PUT 路由装饰器""" 70 | return route(path, methods=['PUT'], **kwargs) 71 | 72 | 73 | def delete(path: str, **kwargs): 74 | """DELETE 路由装饰器""" 75 | return route(path, methods=['DELETE'], **kwargs) 76 | 77 | 78 | def patch(path: str, **kwargs): 79 | """PATCH 路由装饰器""" 80 | return route(path, methods=['PATCH'], **kwargs) 81 | 82 | 83 | def cron(cron_expression: str, enabled: Optional[bool] = None, **kwargs): 84 | """ 85 | Cron 任务装饰器 86 | 87 | 支持在函数和类方法中使用: 88 | - 函数:@cron("0 0 * * *") 或 @cron("0 0 * * * *") 89 | - 类方法:在类的方法上使用 @cron("0 0 * * *") 90 | 91 | Args: 92 | cron_expression: Cron 表达式 93 | - 标准5位格式:分 时 日 月 周(如 "0 0 * * *" 表示每小时) 94 | - 6位格式:秒 分 时 日 月 周(如 "0 0 * * * *" 表示每小时,兼容旧格式) 95 | enabled: 是否启用任务,如果为 None 则默认启用 96 | 可以通过手动获取配置来传递,例如: 97 | from myboot.core.config import get_config 98 | enabled = get_config('jobs.heartbeat.enabled', True) 99 | **kwargs: 其他任务参数 100 | """ 101 | def decorator(func): 102 | func.__myboot_job__ = { 103 | 'type': 'cron', 104 | 'cron': cron_expression, 105 | 'enabled': enabled, 106 | 'kwargs': kwargs 107 | } 108 | return func 109 | return decorator 110 | 111 | 112 | def interval(seconds: int = None, minutes: int = None, hours: int = None, enabled: Optional[bool] = None, **kwargs): 113 | """ 114 | 间隔任务装饰器 115 | 116 | 支持在函数和类方法中使用: 117 | - 函数:@interval(seconds=60) 118 | - 类方法:在类的方法上使用 @interval(seconds=60) 119 | 120 | Args: 121 | seconds: 秒数 122 | minutes: 分钟数 123 | hours: 小时数 124 | enabled: 是否启用任务,如果为 None 则默认启用 125 | 可以通过手动获取配置来传递,例如: 126 | from myboot.core.config import get_config 127 | enabled = get_config('jobs.heartbeat.enabled', True) 128 | **kwargs: 其他任务参数 129 | """ 130 | def decorator(func): 131 | interval_value = seconds or (minutes * 60) or (hours * 3600) 132 | func.__myboot_job__ = { 133 | 'type': 'interval', 134 | 'interval': interval_value, 135 | 'enabled': enabled, 136 | 'kwargs': kwargs 137 | } 138 | return func 139 | return decorator 140 | 141 | 142 | def once(run_date: str = None, enabled: Optional[bool] = None, **kwargs): 143 | """ 144 | 一次性任务装饰器 145 | 146 | 支持在函数和类方法中使用: 147 | - 函数:@once('2025-12-31 23:59:59') 148 | - 类方法:在类的方法上使用 @once('2025-12-31 23:59:59') 149 | 150 | Args: 151 | run_date: 运行日期 152 | enabled: 是否启用任务,如果为 None 则默认启用 153 | 可以通过手动获取配置来传递,例如: 154 | from myboot.core.config import get_config 155 | enabled = get_config('jobs.heartbeat.enabled', True) 156 | **kwargs: 其他任务参数 157 | """ 158 | def decorator(func): 159 | func.__myboot_job__ = { 160 | 'type': 'once', 161 | 'run_date': run_date, 162 | 'enabled': enabled, 163 | 'kwargs': kwargs 164 | } 165 | return func 166 | return decorator 167 | 168 | 169 | def service(name: str = None, **kwargs): 170 | """ 171 | 服务装饰器 172 | 173 | Args: 174 | name: 服务名称 175 | **kwargs: 其他服务参数 176 | """ 177 | def decorator(cls): 178 | cls.__myboot_service__ = { 179 | 'name': name or _camel_to_snake(cls.__name__), 180 | 'kwargs': kwargs 181 | } 182 | return cls 183 | return decorator 184 | 185 | 186 | def model(name: str = None, **kwargs): 187 | """ 188 | 模型装饰器 189 | 190 | Args: 191 | name: 模型名称 192 | **kwargs: 其他模型参数 193 | """ 194 | def decorator(cls): 195 | cls.__myboot_model__ = { 196 | 'name': name or _camel_to_snake(cls.__name__), 197 | 'kwargs': kwargs 198 | } 199 | return cls 200 | return decorator 201 | 202 | 203 | def client(name: str = None, **kwargs): 204 | """ 205 | 客户端装饰器 206 | 207 | Args: 208 | name: 客户端名称 209 | **kwargs: 其他客户端参数 210 | """ 211 | def decorator(cls): 212 | cls.__myboot_client__ = { 213 | 'name': name or _camel_to_snake(cls.__name__), 214 | 'kwargs': kwargs 215 | } 216 | return cls 217 | return decorator 218 | 219 | 220 | def component( 221 | name: str = None, 222 | scope: str = 'singleton', 223 | primary: bool = False, 224 | lazy: bool = False, 225 | **kwargs 226 | ): 227 | """ 228 | 组件装饰器 229 | 230 | 用于将类注册为通用组件,支持依赖注入。 231 | 可用于任意需要托管的类(Job 实例、工具类、配置类等)。 232 | 233 | Args: 234 | name: 组件名称,默认使用类名的 snake_case 形式 235 | scope: 生命周期范围 236 | - 'singleton': 单例模式(默认),整个应用生命周期内只创建一个实例 237 | - 'prototype': 原型模式,每次获取时创建新实例 238 | primary: 当按类型获取有多个匹配时,是否为首选 239 | lazy: 是否懒加载,True 时在首次使用时才创建实例 240 | **kwargs: 其他组件参数 241 | 242 | Examples: 243 | # 基本用法 244 | @component() 245 | class EmailHelper: 246 | def send(self, to: str, content: str): 247 | pass 248 | 249 | # 带依赖注入 250 | @component(name='redis_cache') 251 | class RedisCache: 252 | def __init__(self, redis_client: RedisClient): 253 | self.redis = redis_client 254 | 255 | # 包含定时任务的组件 256 | @component() 257 | class DataSyncJobs: 258 | def __init__(self, data_service: DataService): 259 | self.data_service = data_service 260 | 261 | @cron("0 2 * * *") 262 | def sync_daily(self): 263 | self.data_service.sync() 264 | """ 265 | def decorator(cls): 266 | cls.__myboot_component__ = { 267 | 'name': name or _camel_to_snake(cls.__name__), 268 | 'scope': scope, 269 | 'primary': primary, 270 | 'lazy': lazy, 271 | 'kwargs': kwargs 272 | } 273 | return cls 274 | return decorator 275 | 276 | 277 | def middleware( 278 | name: str = None, 279 | order: int = 0, 280 | path_filter: Union[str, List[str]] = None, 281 | methods: List[str] = None, 282 | condition: Callable = None, 283 | **kwargs 284 | ): 285 | """ 286 | 中间件装饰器 287 | 288 | Args: 289 | name: 中间件名称 290 | order: 执行顺序,数字越小越先执行(默认 0) 291 | path_filter: 路径过滤,支持字符串、字符串列表或正则表达式模式 292 | 例如: '/api/*', ['/api/*', '/admin/*'] 293 | methods: HTTP 方法过滤,如 ['GET', 'POST'](默认 None,处理所有方法) 294 | condition: 条件函数,接收 request 对象,返回 bool 决定是否执行中间件 295 | **kwargs: 其他中间件参数 296 | 297 | Examples: 298 | @middleware(order=1, path_filter='/api/*') 299 | def api_middleware(request, next_handler): 300 | return next_handler(request) 301 | 302 | @middleware(order=2, methods=['POST', 'PUT']) 303 | def post_middleware(request, next_handler): 304 | return next_handler(request) 305 | """ 306 | def decorator(func): 307 | func.__myboot_middleware__ = { 308 | 'name': name or func.__name__, 309 | 'order': order, 310 | 'path_filter': path_filter, 311 | 'methods': methods, 312 | 'condition': condition, 313 | 'kwargs': kwargs 314 | } 315 | return func 316 | return decorator 317 | 318 | 319 | def rest_controller(base_path: str, **kwargs): 320 | """ 321 | REST 控制器装饰器 322 | 323 | 用于标记 REST 控制器类,为类中的方法提供基础路径。 324 | 类中的方法需要显式使用 @get、@post、@put、@delete、@patch 等装饰器才会生成路由。 325 | 326 | 路径合并规则: 327 | - 方法路径以 // 开头:作为绝对路径使用(去掉一个 /) 328 | - 方法路径以 / 开头:去掉开头的 / 后追加到基础路径 329 | - 方法路径不以 / 开头:直接追加到基础路径 330 | 331 | 示例: 332 | @rest_controller('/api/reports') 333 | class ReportController: 334 | @post('/generate') # 最终路径: POST /api/reports/generate 335 | def create_report(self, report_type: str): 336 | return {"message": "报告生成任务已创建"} 337 | 338 | @get('/status/{job_id}') # 最终路径: GET /api/reports/status/{job_id} 339 | def get_status(self, job_id: str): 340 | return {"status": "completed"} 341 | 342 | Args: 343 | base_path: 基础路径 344 | **kwargs: 其他路由参数 345 | """ 346 | def decorator(cls): 347 | cls.__myboot_rest_controller__ = { 348 | 'base_path': base_path.rstrip('/'), 349 | 'kwargs': kwargs 350 | } 351 | return cls 352 | return decorator 353 | -------------------------------------------------------------------------------- /myboot/core/logger.py: -------------------------------------------------------------------------------- 1 | """ 2 | 日志管理模块 3 | 4 | 基于 loguru 的日志管理,提供初始化配置功能 5 | 所有代码可以直接使用: from loguru import logger 6 | """ 7 | 8 | import logging 9 | import os 10 | import sys 11 | from typing import Optional, Union 12 | 13 | from loguru import logger as loguru_logger 14 | from dynaconf import Dynaconf 15 | 16 | from .config import get_settings 17 | 18 | logger = loguru_logger 19 | 20 | 21 | def _get_worker_info() -> str: 22 | """ 23 | 获取当前 worker 信息 24 | 25 | Returns: 26 | worker 标识字符串,格式为 "Worker-1/4" 或 "Main" (主进程) 27 | """ 28 | worker_id = os.environ.get("MYBOOT_WORKER_ID") 29 | worker_count = os.environ.get("MYBOOT_WORKER_COUNT") 30 | 31 | if worker_id and worker_count: 32 | return f"Worker-{worker_id}/{worker_count}" 33 | return "Main" # Main process 34 | 35 | 36 | def _get_log_format_with_worker() -> str: 37 | """ 38 | 获取带 worker 标识的日志格式 39 | 40 | Returns: 41 | 日志格式字符串 42 | """ 43 | return ( 44 | "{time:YYYY-MM-DD HH:mm:ss} | " 45 | "{extra[worker]} | " 46 | "{level: <8} | " 47 | "{name}:{function}:{line} - " 48 | "{message}" 49 | ) 50 | 51 | 52 | def _get_log_format_simple() -> str: 53 | """ 54 | 获取简单日志格式(无 worker 标识) 55 | 56 | Returns: 57 | 日志格式字符串 58 | """ 59 | return ( 60 | "{time:YYYY-MM-DD HH:mm:ss} | " 61 | "{level: <8} | " 62 | "{name}:{function}:{line} - " 63 | "{message}" 64 | ) 65 | 66 | 67 | def _parse_json_config(value) -> bool: 68 | """解析 JSON 配置值为布尔值""" 69 | if isinstance(value, bool): 70 | return value 71 | if isinstance(value, str): 72 | return value.lower() in ("true", "1", "yes", "on") 73 | return bool(value) 74 | 75 | 76 | def _convert_logging_format_to_loguru(user_format: str) -> str: 77 | """ 78 | 将标准 logging 格式转换为 loguru 格式 79 | 80 | Args: 81 | user_format: 用户提供的日志格式字符串 82 | 83 | Returns: 84 | 转换后的 loguru 格式字符串 85 | """ 86 | format_mapping = { 87 | "%(asctime)s": "{time:YYYY-MM-DD HH:mm:ss}", 88 | "%(name)s": "{name}", 89 | "%(levelname)s": "{level: <8}", 90 | "%(message)s": "{message}", 91 | "%(filename)s": "{file.name}", 92 | "%(funcName)s": "{function}", 93 | "%(lineno)d": "{line}", 94 | } 95 | 96 | result = user_format 97 | for old, new in format_mapping.items(): 98 | result = result.replace(old, new) 99 | return result 100 | 101 | 102 | def _build_json_handler_kwargs(log_level: str) -> tuple[dict, dict]: 103 | """ 104 | 构建 JSON 格式的 handler 参数 105 | 106 | Returns: 107 | (console_kwargs, file_kwargs) 元组 108 | """ 109 | console_kwargs = { 110 | "sink": sys.stdout, 111 | "serialize": True, 112 | "level": log_level, 113 | "backtrace": True, 114 | "diagnose": True, 115 | } 116 | file_kwargs = { 117 | "serialize": True, 118 | "level": log_level, 119 | "rotation": "10 MB", 120 | "retention": "7 days", 121 | "compression": "zip", 122 | "backtrace": True, 123 | "diagnose": True, 124 | } 125 | return console_kwargs, file_kwargs 126 | 127 | 128 | def _build_text_handler_kwargs(log_format: str, log_level: str) -> tuple[dict, dict]: 129 | """ 130 | 构建文本格式的 handler 参数 131 | 132 | Returns: 133 | (console_kwargs, file_kwargs) 元组 134 | """ 135 | console_kwargs = { 136 | "sink": sys.stdout, 137 | "format": log_format, 138 | "level": log_level, 139 | "colorize": True, 140 | "backtrace": True, 141 | "diagnose": True, 142 | } 143 | file_kwargs = { 144 | "format": log_format, 145 | "level": log_level, 146 | "rotation": "10 MB", 147 | "retention": "7 days", 148 | "compression": "zip", 149 | "backtrace": True, 150 | "diagnose": True, 151 | } 152 | return console_kwargs, file_kwargs 153 | 154 | 155 | def _get_third_party_config(config_obj) -> dict: 156 | """获取第三方库日志配置""" 157 | try: 158 | return config_obj.logging.third_party 159 | except (AttributeError, KeyError): 160 | pass 161 | 162 | try: 163 | return config_obj.get("logging.third_party", {}) 164 | except (AttributeError, KeyError): 165 | return {} 166 | 167 | 168 | def _configure_third_party_loggers(third_party_config: dict) -> None: 169 | """配置第三方库的日志级别""" 170 | if not isinstance(third_party_config, dict): 171 | return 172 | 173 | for logger_name, level_name in third_party_config.items(): 174 | if isinstance(level_name, str): 175 | std_logger = logging.getLogger(logger_name) 176 | level = getattr(logging, level_name.upper(), logging.INFO) 177 | std_logger.setLevel(level) 178 | 179 | 180 | def _add_file_handler(log_file: str, file_kwargs: dict) -> None: 181 | """添加文件日志 handler""" 182 | log_dir = os.path.dirname(log_file) 183 | if log_dir: 184 | os.makedirs(log_dir, exist_ok=True) 185 | loguru_logger.add(log_file, **file_kwargs) 186 | 187 | 188 | def setup_logging(config: Optional[Union[str, Dynaconf]] = None, enable_worker_info: bool = True) -> None: 189 | """ 190 | 根据配置文件或配置对象初始化 loguru 日志系统 191 | 192 | Args: 193 | config: 配置文件路径或配置对象(Dynaconf),如果为 None 则使用默认配置 194 | enable_worker_info: 是否启用 worker 信息显示(多 worker 模式下自动启用) 195 | """ 196 | # 获取配置对象 197 | config_obj = config if isinstance(config, Dynaconf) else get_settings(config) 198 | 199 | # 移除默认的 handler 并配置 worker 上下文 200 | loguru_logger.remove() 201 | loguru_logger.configure(extra={"worker": _get_worker_info()}) 202 | 203 | # 获取日志级别 204 | log_level = config_obj.get("logging.level", "INFO").upper() 205 | 206 | # 构建 handler 参数 207 | use_json = _parse_json_config(config_obj.get("logging.json", False)) 208 | if use_json: 209 | console_kwargs, file_kwargs = _build_json_handler_kwargs(log_level) 210 | else: 211 | # 确定日志格式 212 | user_format = config_obj.get("logging.format", None) 213 | if user_format: 214 | log_format = _convert_logging_format_to_loguru(user_format) 215 | else: 216 | worker_count = os.environ.get("MYBOOT_WORKER_COUNT") 217 | is_multi_worker = worker_count and int(worker_count) > 1 218 | log_format = _get_log_format_with_worker() if (is_multi_worker and enable_worker_info) else _get_log_format_simple() 219 | 220 | console_kwargs, file_kwargs = _build_text_handler_kwargs(log_format, log_level) 221 | 222 | # 添加 handlers 223 | loguru_logger.add(**console_kwargs) 224 | 225 | log_file = config_obj.get("logging.file") 226 | if log_file: 227 | _add_file_handler(log_file, file_kwargs) 228 | 229 | # 配置第三方库日志 230 | _configure_third_party_loggers(_get_third_party_config(config_obj)) 231 | 232 | 233 | def configure_worker_logger(worker_id: int, total_workers: int) -> None: 234 | """ 235 | 配置 worker 进程的日志上下文 236 | 237 | 在多 worker 模式下,每个 worker 进程启动时调用此函数, 238 | 以便在日志中显示 worker 标识 239 | 240 | Args: 241 | worker_id: Worker 进程 ID (从 1 开始) 242 | total_workers: 总 worker 数量 243 | """ 244 | worker_info = f"Worker-{worker_id}/{total_workers}" 245 | loguru_logger.configure(extra={"worker": worker_info}) 246 | 247 | 248 | def setup_worker_logging(worker_id: int, total_workers: int) -> None: 249 | """ 250 | 为 worker 进程重新初始化日志系统 251 | 252 | 在多 worker 模式下,每个 worker 进程启动后调用此函数, 253 | 重新配置日志格式以显示 worker 标识 254 | 255 | Args: 256 | worker_id: Worker 进程 ID (从 1 开始) 257 | total_workers: 总 worker 数量 258 | """ 259 | # 移除现有的 handler 260 | loguru_logger.remove() 261 | 262 | # 设置 worker 上下文 263 | worker_info = f"Worker-{worker_id}/{total_workers}" 264 | loguru_logger.configure(extra={"worker": worker_info}) 265 | 266 | # 使用带 worker 标识的日志格式 267 | log_format = _get_log_format_with_worker() 268 | 269 | # 添加控制台输出 handler 270 | loguru_logger.add( 271 | sys.stdout, 272 | format=log_format, 273 | level="DEBUG", # 使用 DEBUG,让应用配置控制实际级别 274 | colorize=True, 275 | backtrace=True, 276 | diagnose=True, 277 | ) 278 | 279 | 280 | # 为了向后兼容,提供 get_logger 函数 281 | # 但实际上直接使用 loguru 的 logger 即可 282 | def get_logger(name: str = "app"): 283 | """ 284 | 获取日志器实例(向后兼容) 285 | 286 | 注意:建议直接使用 `from loguru import logger` 287 | 288 | Args: 289 | name: 日志器名称(loguru 中用于标识,可通过 bind 方法绑定) 290 | 291 | Returns: 292 | loguru Logger 实例 293 | """ 294 | return loguru_logger.bind(name=name) 295 | 296 | 297 | # 为了向后兼容,提供 Logger 类 298 | class Logger: 299 | """ 300 | 日志器类(向后兼容) 301 | 302 | 注意:建议直接使用 `from loguru import logger` 303 | """ 304 | 305 | def __init__(self, name: str = "app"): 306 | """ 307 | 初始化日志器 308 | 309 | Args: 310 | name: 日志器名称 311 | """ 312 | self.name = name 313 | self._logger = loguru_logger.bind(name=name) 314 | 315 | def debug(self, message: str, *args, **kwargs) -> None: 316 | """记录调试日志""" 317 | self._logger.debug(message, *args, **kwargs) 318 | 319 | def info(self, message: str, *args, **kwargs) -> None: 320 | """记录信息日志""" 321 | self._logger.info(message, *args, **kwargs) 322 | 323 | def warning(self, message: str, *args, **kwargs) -> None: 324 | """记录警告日志""" 325 | self._logger.warning(message, *args, **kwargs) 326 | 327 | def error(self, message: str, *args, **kwargs) -> None: 328 | """记录错误日志""" 329 | self._logger.error(message, *args, **kwargs) 330 | 331 | def critical(self, message: str, *args, **kwargs) -> None: 332 | """记录严重错误日志""" 333 | self._logger.critical(message, *args, **kwargs) 334 | 335 | def exception(self, message: str, *args, **kwargs) -> None: 336 | """记录异常日志""" 337 | self._logger.exception(message, *args, **kwargs) 338 | -------------------------------------------------------------------------------- /myboot/web/middleware.py: -------------------------------------------------------------------------------- 1 | """ 2 | Web 中间件模块 3 | 4 | 提供类似 Spring Boot 的中间件功能 5 | """ 6 | 7 | import asyncio 8 | import json 9 | import time 10 | from typing import Callable, List, Optional, Union, Pattern 11 | import re 12 | 13 | from fastapi import Request, Response 14 | from fastapi.responses import JSONResponse 15 | from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint 16 | from starlette.responses import StreamingResponse 17 | 18 | 19 | class Middleware: 20 | """中间件基类""" 21 | 22 | def __init__(self, middleware_class: type, **kwargs): 23 | """ 24 | 初始化中间件 25 | 26 | Args: 27 | middleware_class: 中间件类 28 | **kwargs: 中间件参数 29 | """ 30 | self.middleware_class = middleware_class 31 | self.kwargs = kwargs 32 | 33 | 34 | class FunctionMiddleware(BaseHTTPMiddleware): 35 | """函数中间件包装器 36 | 37 | 将函数式中间件转换为 FastAPI 的 BaseHTTPMiddleware 类 38 | """ 39 | 40 | def __init__( 41 | self, 42 | app, 43 | middleware_func: Callable, 44 | path_filter: Optional[Union[str, Pattern, List[str]]] = None, 45 | methods: Optional[List[str]] = None, 46 | condition: Optional[Callable] = None, 47 | order: int = 0, 48 | **kwargs 49 | ): 50 | """ 51 | 初始化函数中间件 52 | 53 | Args: 54 | app: FastAPI 应用实例 55 | middleware_func: 中间件函数,接收 (request, next_handler) 参数 56 | path_filter: 路径过滤,支持字符串、正则表达式或字符串列表 57 | methods: HTTP 方法过滤,如 ['GET', 'POST'] 58 | condition: 条件函数,接收 request,返回 bool 决定是否执行中间件 59 | order: 执行顺序,数字越小越先执行 60 | **kwargs: 其他参数 61 | """ 62 | super().__init__(app) 63 | self.middleware_func = middleware_func 64 | self.path_filter = path_filter 65 | self.methods = [m.upper() for m in methods] if methods else None 66 | self.condition = condition 67 | self.order = order 68 | self.kwargs = kwargs 69 | 70 | # 编译路径过滤正则表达式 71 | if path_filter: 72 | if isinstance(path_filter, str): 73 | # 将通配符转换为正则表达式 74 | pattern = path_filter.replace('*', '.*').replace('?', '.') 75 | self.path_pattern = re.compile(f'^{pattern}$') 76 | elif isinstance(path_filter, list): 77 | patterns = [p.replace('*', '.*').replace('?', '.') for p in path_filter] 78 | self.path_pattern = re.compile(f"^({'|'.join(patterns)})$") 79 | elif isinstance(path_filter, Pattern): 80 | self.path_pattern = path_filter 81 | else: 82 | self.path_pattern = None 83 | else: 84 | self.path_pattern = None 85 | 86 | def _should_process(self, request: Request) -> bool: 87 | """判断是否应该处理该请求""" 88 | # 检查路径过滤 89 | if self.path_pattern: 90 | if not self.path_pattern.match(request.url.path): 91 | return False 92 | 93 | # 检查 HTTP 方法过滤 94 | if self.methods: 95 | if request.method not in self.methods: 96 | return False 97 | 98 | # 检查条件函数 99 | if self.condition: 100 | try: 101 | if not self.condition(request): 102 | return False 103 | except Exception: 104 | # 条件函数执行失败时跳过该中间件 105 | return False 106 | 107 | return True 108 | 109 | async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: 110 | """执行中间件逻辑""" 111 | # 如果不符合过滤条件,直接跳过 112 | if not self._should_process(request): 113 | return await call_next(request) 114 | 115 | # 创建异步 next_handler 包装器 116 | async def async_next_handler(req: Request) -> Response: 117 | return await call_next(req) 118 | 119 | # 检查中间件函数是否是异步函数 120 | if asyncio.iscoroutinefunction(self.middleware_func): 121 | # 异步中间件(推荐方式) 122 | response = await self.middleware_func(request, async_next_handler) 123 | else: 124 | # 同步中间件支持(为了向后兼容) 125 | # 注意:同步中间件接收的 next_handler 会返回协程,需要手动处理 126 | # 建议使用异步中间件以获得更好的性能 127 | def sync_wrapper(req: Request): 128 | """同步包装器:返回协程对象,需要调用者手动 await""" 129 | return call_next(req) 130 | 131 | # 调用同步中间件函数 132 | result = self.middleware_func(request, sync_wrapper) 133 | 134 | # 处理返回值 135 | if asyncio.iscoroutine(result): 136 | # 如果返回协程,等待它 137 | response = await result 138 | elif isinstance(result, (Response, StreamingResponse)): 139 | # 如果已经是 Response,直接使用 140 | response = result 141 | else: 142 | # 其他情况,尝试转换 143 | from fastapi.responses import JSONResponse 144 | response = JSONResponse(content=result) 145 | 146 | # 确保返回 Response 对象 147 | if not isinstance(response, (Response, StreamingResponse)): 148 | # 如果返回的不是 Response,尝试转换为 Response 149 | from fastapi.responses import JSONResponse 150 | response = JSONResponse(content=response) 151 | 152 | return response 153 | 154 | 155 | class ResponseFormatterMiddleware(BaseHTTPMiddleware): 156 | """响应格式化中间件 157 | 158 | 自动将路由返回的数据包装为统一的 REST API 格式: 159 | { 160 | "success": true, 161 | "code": 200, 162 | "message": "操作成功", 163 | "data": {...} 164 | } 165 | """ 166 | 167 | def __init__( 168 | self, 169 | app, 170 | exclude_paths: Optional[List[str]] = None, 171 | auto_wrap: bool = True 172 | ): 173 | """ 174 | 初始化响应格式化中间件 175 | 176 | Args: 177 | app: FastAPI 应用实例 178 | exclude_paths: 排除的路径列表(这些路径不进行格式化) 179 | auto_wrap: 是否自动包装响应 180 | """ 181 | super().__init__(app) 182 | self.exclude_paths = exclude_paths or [] 183 | self.auto_wrap = auto_wrap 184 | 185 | # 默认排除的路径(系统路径和文档路径) 186 | default_excludes = [ 187 | "/docs", 188 | "/openapi.json", 189 | "/redoc", 190 | "/health", 191 | "/health/ready", 192 | "/health/live" 193 | ] 194 | 195 | # 合并排除路径 196 | self.exclude_paths = list(set(self.exclude_paths + default_excludes)) 197 | 198 | def _should_format(self, request: Request) -> bool: 199 | """判断是否应该格式化响应""" 200 | if not self.auto_wrap: 201 | return False 202 | 203 | path = request.url.path 204 | 205 | # 检查是否在排除列表中 206 | for exclude_path in self.exclude_paths: 207 | if path == exclude_path or path.startswith(exclude_path + "/"): 208 | return False 209 | 210 | return True 211 | 212 | def _is_formatted(self, content: dict) -> bool: 213 | """检查响应是否已经是统一格式""" 214 | if not isinstance(content, dict): 215 | return False 216 | 217 | # 检查是否包含统一格式的必须字段(message 和 data 可选) 218 | required_fields = {"success", "code"} 219 | return all(field in content for field in required_fields) 220 | 221 | async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: 222 | """处理请求并格式化响应""" 223 | response = await call_next(request) 224 | 225 | # 如果不需要格式化,直接返回 226 | if not self._should_format(request): 227 | return response 228 | 229 | # 检查响应类型 230 | # FastAPI 可能返回 JSONResponse、StreamingResponse 或 _StreamingResponse 231 | # 我们需要处理所有可能包含 JSON 内容的响应 232 | 233 | # 检查是否是 JSONResponse 234 | is_json_response = isinstance(response, JSONResponse) 235 | 236 | # 如果不是 JSONResponse,检查是否可能是 JSON 响应 237 | if not is_json_response: 238 | # 检查 media_type 是否为 JSON 239 | if hasattr(response, 'media_type') and response.media_type: 240 | media_type_lower = response.media_type.lower() 241 | if 'json' in media_type_lower: 242 | is_json_response = True 243 | else: 244 | return response 245 | elif isinstance(response, (StreamingResponse, Response)): 246 | # StreamingResponse 或 Response 如果没有设置 media_type,尝试读取并判断 247 | # 对于未知类型的响应,我们尝试读取内容并判断是否为 JSON 248 | # 允许继续处理,尝试读取响应体 249 | pass 250 | else: 251 | # 如果既不是 JSONResponse 也没有 media_type,跳过 252 | return response 253 | 254 | # 检查响应是否有 body_iterator(所有响应都应该有) 255 | if not hasattr(response, 'body_iterator'): 256 | return response 257 | 258 | # 获取响应内容 259 | try: 260 | # 读取响应体 - 使用正确的方式处理流式响应 261 | body = b"" 262 | async for chunk in response.body_iterator: 263 | body += chunk 264 | 265 | # 如果没有响应体,直接返回空响应 266 | if not body: 267 | return response 268 | 269 | # 解析 JSON 270 | try: 271 | content = json.loads(body.decode('utf-8')) 272 | except json.JSONDecodeError: 273 | # 如果不是有效的 JSON,返回原始响应 274 | return response 275 | 276 | # 如果已经是统一格式,重新构建响应并返回 277 | if self._is_formatted(content): 278 | # 移除 Content-Length,让服务器重新计算 279 | headers = dict(response.headers) 280 | headers.pop('content-length', None) 281 | return JSONResponse( 282 | content=content, 283 | status_code=response.status_code, 284 | headers=headers 285 | ) 286 | 287 | # 根据状态码判断成功或失败 288 | status_code = response.status_code 289 | is_success = 200 <= status_code < 400 290 | 291 | # 自动包装响应 292 | from myboot.web.response import ResponseWrapper 293 | 294 | if is_success: 295 | # 成功响应 296 | formatted_content = ResponseWrapper.success( 297 | data=content, 298 | message="操作成功", 299 | code=status_code 300 | ) 301 | else: 302 | # 错误响应(通常由异常处理器处理,但以防万一) 303 | message = content.get("message", "操作失败") if isinstance(content, dict) else "操作失败" 304 | formatted_content = ResponseWrapper.error( 305 | message=message, 306 | code=status_code, 307 | data=content if isinstance(content, dict) else {"error": str(content)} 308 | ) 309 | 310 | # 移除 Content-Length,让服务器重新计算新的响应长度 311 | headers = dict(response.headers) 312 | headers.pop('content-length', None) 313 | 314 | return JSONResponse( 315 | content=formatted_content, 316 | status_code=status_code, 317 | headers=headers 318 | ) 319 | 320 | except (AttributeError, UnicodeDecodeError, StopAsyncIteration) as e: 321 | # 如果无法解析,返回原始响应 322 | # 这里可以记录日志,但为了不影响正常响应,静默处理 323 | return response 324 | except Exception as e: 325 | # 捕获所有其他异常,避免中间件崩溃 326 | # 在生产环境中可以记录日志 327 | return response 328 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /myboot/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | MyBoot CLI 工具 5 | 6 | 提供项目模板初始化功能 7 | """ 8 | 9 | import click 10 | import sys 11 | from pathlib import Path 12 | 13 | 14 | @click.group() 15 | def cli(): 16 | """MyBoot 命令行工具 - 项目模板初始化""" 17 | pass 18 | 19 | 20 | @cli.command() 21 | @click.option('--name', prompt='项目名称', help='项目名称') 22 | @click.option('--dir', default='.', help='项目目录(默认为当前目录)') 23 | @click.option('--template', type=click.Choice(['basic', 'api', 'full']), default='basic', 24 | help='项目模板: basic(基础), api(API项目), full(完整项目)') 25 | @click.option('--force', is_flag=True, help='如果目录已存在则覆盖') 26 | def init(name: str, dir: str, template: str, force: bool): 27 | """初始化新的 MyBoot 项目""" 28 | 29 | project_dir = Path(dir) / name 30 | 31 | # 检查目录是否存在 32 | if project_dir.exists() and not force: 33 | click.echo(f"❌ 错误: 目录 '{project_dir}' 已存在", err=True) 34 | click.echo(" 使用 --force 选项可以覆盖现有目录") 35 | sys.exit(1) 36 | 37 | click.echo(f"📦 正在初始化项目: {name}") 38 | click.echo(f" 模板: {template}") 39 | click.echo(f" 目录: {project_dir}") 40 | click.echo() 41 | 42 | try: 43 | # 创建目录结构 44 | dirs = ['app', 'app/api', 'app/service', 'app/model', 'app/jobs', 'app/client', 'conf', 'tests'] 45 | for d in dirs: 46 | (project_dir / d).mkdir(parents=True, exist_ok=True) 47 | # 创建 __init__.py 48 | init_file = project_dir / d / '__init__.py' 49 | if not init_file.exists(): 50 | init_file.write_text('', encoding='utf-8') 51 | 52 | click.echo("✓ 创建目录结构") 53 | 54 | # 创建配置文件 55 | config_content = f"""# {name} 配置文件 56 | 57 | # 应用配置 58 | app: 59 | name: "{name}" 60 | version: "0.1.0" 61 | 62 | # 服务器配置 63 | server: 64 | port: 8000 65 | reload: true 66 | workers: 1 67 | keep_alive_timeout: 5 68 | graceful_timeout: 30 69 | response_format: 70 | enabled: true 71 | exclude_paths: 72 | - "/docs" 73 | 74 | # 日志配置 75 | logging: 76 | level: "INFO" 77 | 78 | # 任务调度配置 79 | scheduler: 80 | enabled: true 81 | timezone: "UTC" 82 | max_workers: 10 83 | """ 84 | config_file = project_dir / 'conf' / 'config.yaml' 85 | config_file.write_text(config_content, encoding='utf-8') 86 | click.echo("✓ 创建配置文件: conf/config.yaml") 87 | 88 | # 创建主应用文件(放在根目录) 89 | app_content = f'''"""主应用文件""" 90 | from myboot.core.application import create_app 91 | 92 | app = create_app(name="{name}") 93 | 94 | if __name__ == "__main__": 95 | app.run() 96 | ''' 97 | main_file = project_dir / 'main.py' 98 | main_file.write_text(app_content, encoding='utf-8') 99 | click.echo("✓ 创建主应用文件: main.py") 100 | 101 | # 根据模板创建不同的文件 102 | if template in ['api', 'full']: 103 | # 创建示例路由 104 | api_content = '''"""API 路由示例""" 105 | from myboot.core.application import app 106 | 107 | @app.get("/") 108 | def hello(): 109 | """Hello World 接口""" 110 | return {"message": "Hello, MyBoot!", "status": "success"} 111 | 112 | @app.get("/health") 113 | def health(): 114 | """健康检查接口""" 115 | return {"status": "healthy", "service": "running"} 116 | ''' 117 | routes_file = project_dir / 'app' / 'api' / 'routes.py' 118 | routes_file.write_text(api_content, encoding='utf-8') 119 | click.echo("✓ 创建示例路由: app/api/routes.py") 120 | 121 | if template == 'full': 122 | # 创建示例服务 123 | service_content = '''"""服务层示例""" 124 | from typing import Dict, Any 125 | 126 | 127 | class UserService: 128 | """用户服务示例""" 129 | 130 | def get_user(self, user_id: int) -> Dict[str, Any]: 131 | """获取用户信息""" 132 | return { 133 | "id": user_id, 134 | "name": "示例用户", 135 | "email": "user@example.com" 136 | } 137 | 138 | def create_user(self, name: str, email: str) -> Dict[str, Any]: 139 | """创建用户""" 140 | return { 141 | "id": 1, 142 | "name": name, 143 | "email": email, 144 | "status": "created" 145 | } 146 | ''' 147 | service_file = project_dir / 'app' / 'service' / 'user_service.py' 148 | service_file.write_text(service_content, encoding='utf-8') 149 | click.echo("✓ 创建示例服务: app/service/user_service.py") 150 | 151 | # 创建示例模型 152 | model_content = '''"""数据模型示例""" 153 | from pydantic import BaseModel, EmailStr 154 | from typing import Optional 155 | 156 | 157 | class User(BaseModel): 158 | """用户模型""" 159 | id: Optional[int] = None 160 | name: str 161 | email: EmailStr 162 | status: str = "active" 163 | 164 | class Config: 165 | json_schema_extra = { 166 | "example": { 167 | "name": "张三", 168 | "email": "zhangsan@example.com" 169 | } 170 | } 171 | ''' 172 | model_file = project_dir / 'app' / 'model' / 'user.py' 173 | model_file.write_text(model_content, encoding='utf-8') 174 | click.echo("✓ 创建示例模型: app/model/user.py") 175 | 176 | # 创建示例客户端 177 | client_content = '''"""客户端示例""" 178 | from typing import Dict, Any 179 | import requests 180 | 181 | 182 | class ApiClient: 183 | """API 客户端示例""" 184 | 185 | def __init__(self, base_url: str): 186 | """初始化客户端""" 187 | self.base_url = base_url 188 | 189 | def get(self, endpoint: str) -> Dict[str, Any]: 190 | """GET 请求""" 191 | response = requests.get(f"{self.base_url}/{endpoint}") 192 | response.raise_for_status() 193 | return response.json() 194 | 195 | def post(self, endpoint: str, data: Dict[str, Any]) -> Dict[str, Any]: 196 | """POST 请求""" 197 | response = requests.post(f"{self.base_url}/{endpoint}", json=data) 198 | response.raise_for_status() 199 | return response.json() 200 | ''' 201 | client_file = project_dir / 'app' / 'client' / 'api_client.py' 202 | client_file.write_text(client_content, encoding='utf-8') 203 | click.echo("✓ 创建示例客户端: app/client/api_client.py") 204 | 205 | # 创建示例定时任务 206 | job_content = '''"""定时任务示例""" 207 | from myboot.core.decorators import cron 208 | 209 | 210 | @cron("0 */5 * * * *") # 每5分钟执行一次 211 | def cleanup_task(): 212 | """清理任务示例""" 213 | print("执行清理任务...") 214 | 215 | 216 | @cron("0 0 * * * *") # 每小时执行一次 217 | def hourly_task(): 218 | """每小时任务示例""" 219 | print("执行每小时任务...") 220 | ''' 221 | job_file = project_dir / 'app' / 'jobs' / 'tasks.py' 222 | job_file.write_text(job_content, encoding='utf-8') 223 | click.echo("✓ 创建示例任务: app/jobs/tasks.py") 224 | 225 | # 创建 README 226 | readme_content = f"""# {name} 227 | 228 | MyBoot 项目 229 | 230 | ## 快速开始 231 | 232 | ### 安装依赖 233 | 234 | ```bash 235 | pip install myboot 236 | ``` 237 | 238 | ### 运行应用 239 | 240 | ```bash 241 | # 使用默认设置启动 242 | python main.py 243 | 244 | # 或者使用 myboot 命令(如果已安装) 245 | myboot run 246 | ``` 247 | 248 | ### 开发模式 249 | 250 | ```bash 251 | # 启用自动重载 252 | python main.py --reload 253 | 254 | # 或者 255 | myboot dev --reload 256 | ``` 257 | 258 | ## 项目结构 259 | 260 | ``` 261 | {name}/ 262 | ├── main.py # 应用入口 263 | ├── pyproject.toml # 项目配置文件 264 | ├── .gitignore # Git 忽略文件 265 | ├── app/ # 应用代码 266 | │ ├── api/ # API 路由 267 | │ ├── service/ # 业务逻辑层 268 | │ ├── model/ # 数据模型 269 | │ ├── jobs/ # 定时任务 270 | │ └── client/ # 客户端(第三方API调用等) 271 | ├── conf/ # 配置文件 272 | │ └── config.yaml # 主配置文件 273 | └── tests/ # 测试代码 274 | ``` 275 | 276 | ## 配置说明 277 | 278 | 配置文件位于 `conf/config.yaml`,支持以下配置: 279 | 280 | - **app**: 应用配置(名称、版本等) 281 | - **server**: 服务器配置(端口、工作进程等) 282 | - **logging**: 日志配置 283 | - **scheduler**: 任务调度配置 284 | 285 | ## 更多信息 286 | 287 | - [MyBoot 文档](https://github.com/your-org/myboot) 288 | - [快速开始指南](https://github.com/your-org/myboot/docs) 289 | """ 290 | readme_file = project_dir / 'README.md' 291 | readme_file.write_text(readme_content, encoding='utf-8') 292 | click.echo("✓ 创建 README: README.md") 293 | 294 | # 创建 .gitignore 295 | gitignore_content = """# Python 296 | __pycache__/ 297 | *.py[cod] 298 | *$py.class 299 | *.so 300 | .Python 301 | build/ 302 | develop-eggs/ 303 | dist/ 304 | downloads/ 305 | eggs/ 306 | .eggs/ 307 | lib/ 308 | lib64/ 309 | parts/ 310 | sdist/ 311 | var/ 312 | wheels/ 313 | *.egg-info/ 314 | .installed.cfg 315 | *.egg 316 | 317 | # Virtual Environment 318 | venv/ 319 | ENV/ 320 | env/ 321 | .venv 322 | 323 | # IDE 324 | .vscode/ 325 | .idea/ 326 | *.swp 327 | *.swo 328 | *~ 329 | 330 | # Logs 331 | *.log 332 | logs/ 333 | 334 | # OS 335 | .DS_Store 336 | Thumbs.db 337 | 338 | # MyBoot 339 | .pytest_cache/ 340 | .coverage 341 | htmlcov/ 342 | """ 343 | gitignore_file = project_dir / '.gitignore' 344 | gitignore_file.write_text(gitignore_content, encoding='utf-8') 345 | click.echo("✓ 创建 .gitignore") 346 | 347 | # 创建 pyproject.toml 348 | project_name = name.lower().replace(' ', '-').replace('_', '-') 349 | pyproject_content = f"""[project] 350 | name = "{project_name}" 351 | version = "0.1.0" 352 | description = "{name} - MyBoot 项目" 353 | authors = [ 354 | {{name = "Your Name", email = "your.email@example.com"}} 355 | ] 356 | readme = "README.md" 357 | license = {{text = "MIT"}} 358 | requires-python = ">=3.9" 359 | keywords = ["myboot", "web", "api"] 360 | classifiers = [ 361 | "Development Status :: 3 - Alpha", 362 | "Intended Audience :: Developers", 363 | "License :: OSI Approved :: MIT License", 364 | "Programming Language :: Python :: 3", 365 | "Programming Language :: Python :: 3.9", 366 | "Programming Language :: Python :: 3.10", 367 | "Programming Language :: Python :: 3.11", 368 | "Programming Language :: Python :: 3.12", 369 | ] 370 | 371 | dependencies = [ 372 | "myboot>=0.1.0", 373 | ] 374 | 375 | [project.optional-dependencies] 376 | dev = [ 377 | "pytest>=7.0", 378 | "pytest-cov>=4.0", 379 | "pytest-asyncio>=0.21.0", 380 | "black>=23.0", 381 | "isort>=5.12", 382 | "flake8>=6.0", 383 | ] 384 | 385 | [build-system] 386 | requires = ["hatchling"] 387 | build-backend = "hatchling.build" 388 | 389 | [tool.hatch.build.targets.wheel] 390 | packages = ["app"] 391 | 392 | [tool.black] 393 | line-length = 88 394 | target-version = ['py39', 'py310', 'py311', 'py312'] 395 | include = '\\.pyi?$' 396 | 397 | [tool.isort] 398 | profile = "black" 399 | line_length = 88 400 | known_first_party = ["app"] 401 | 402 | [tool.pytest.ini_options] 403 | testpaths = ["tests"] 404 | python_files = ["test_*.py", "*_test.py"] 405 | python_classes = ["Test*"] 406 | python_functions = ["test_*"] 407 | addopts = [ 408 | "--strict-markers", 409 | "--verbose", 410 | ] 411 | """ 412 | pyproject_file = project_dir / 'pyproject.toml' 413 | pyproject_file.write_text(pyproject_content, encoding='utf-8') 414 | click.echo("✓ 创建 pyproject.toml") 415 | 416 | click.echo() 417 | click.echo(f"✅ 项目 '{name}' 初始化完成!") 418 | click.echo() 419 | click.echo("下一步:") 420 | click.echo(f" cd {name}") 421 | click.echo(" python main.py") 422 | click.echo() 423 | 424 | except Exception as e: 425 | click.echo(f"❌ 初始化失败: {e}", err=True) 426 | import traceback 427 | if '--debug' in sys.argv: 428 | traceback.print_exc() 429 | sys.exit(1) 430 | 431 | 432 | @cli.command() 433 | def info(): 434 | """显示 MyBoot 信息""" 435 | click.echo("🎯 MyBoot - 类似 Spring Boot 的企业级Python快速开发框架") 436 | click.echo() 437 | click.echo("✨ 主要特性:") 438 | click.echo(" • 快速启动和自动配置") 439 | click.echo(" • 约定优于配置") 440 | click.echo(" • 高性能 Hypercorn 服务器") 441 | click.echo(" • Web API 开发") 442 | click.echo(" • 定时任务调度") 443 | click.echo(" • 日志管理") 444 | click.echo(" • 配置管理") 445 | click.echo() 446 | click.echo("🚀 快速开始:") 447 | click.echo(" myboot init # 初始化新项目") 448 | click.echo(" myboot init --template api # 使用 API 模板") 449 | click.echo(" myboot init --template full # 使用完整模板") 450 | click.echo() 451 | 452 | 453 | if __name__ == '__main__': 454 | cli() 455 | -------------------------------------------------------------------------------- /myboot/core/server.py: -------------------------------------------------------------------------------- 1 | """ 2 | 服务器管理器 3 | 4 | 默认使用 Hypercorn 作为 ASGI 服务器,支持多 workers 模式 5 | """ 6 | 7 | import asyncio 8 | import multiprocessing 9 | import os 10 | import signal 11 | import sys 12 | from typing import Any, Dict, List, Optional 13 | 14 | from loguru import logger 15 | 16 | from ..utils import get_local_ip 17 | 18 | 19 | def _resolve_app_from_path(app_path: str): 20 | """ 21 | 从模块路径解析 ASGI 应用 22 | 23 | 支持的格式: 24 | - "module.path:app" -> 获取 module.path 模块的 app 属性 25 | - "module.path:app.get_fastapi_app()" -> 获取 app 对象后调用 get_fastapi_app() 26 | - "module.path:create_app()" -> 调用 module.path 模块的 create_app() 函数 27 | 28 | Args: 29 | app_path: 应用模块路径 30 | 31 | Returns: 32 | ASGI 应用实例 33 | """ 34 | import importlib 35 | import re 36 | 37 | # 解析模块路径和应用名 38 | if ":" in app_path: 39 | module_path, app_expr = app_path.rsplit(":", 1) 40 | else: 41 | module_path, app_expr = app_path, "app" 42 | 43 | # 导入模块 44 | # 如果请求的是 main 模块且 __main__ 已存在,直接使用 __main__ 45 | if module_path == "main" and "__main__" in sys.modules: 46 | module = sys.modules["__main__"] 47 | else: 48 | module = importlib.import_module(module_path) 49 | 50 | # 解析表达式,支持链式调用如 "app.get_fastapi_app()" 51 | # 使用简单的解析器处理 attr 和 method() 调用 52 | parts = re.split(r'\.(?![^(]*\))', app_expr) # 按 . 分割,但不分割括号内的点 53 | 54 | result = module 55 | for part in parts: 56 | part = part.strip() 57 | if not part: 58 | continue 59 | 60 | # 检查是否是方法调用 (以 () 结尾) 61 | if part.endswith("()"): 62 | method_name = part[:-2] 63 | result = getattr(result, method_name)() 64 | else: 65 | result = getattr(result, part) 66 | 67 | return result 68 | 69 | 70 | def _worker_serve(app_path: str, config_dict: dict, worker_id: int, total_workers: int): 71 | """ 72 | Worker 进程入口函数 73 | 74 | Args: 75 | app_path: 应用模块路径,格式为 "module.path:app_name" 76 | config_dict: Hypercorn 配置字典 77 | worker_id: Worker 进程 ID (从 1 开始) 78 | total_workers: 总 worker 数量 79 | """ 80 | # 设置环境变量,供 Application 读取 81 | os.environ["MYBOOT_WORKER_ID"] = str(worker_id) 82 | os.environ["MYBOOT_WORKER_COUNT"] = str(total_workers) 83 | os.environ["MYBOOT_IS_PRIMARY_WORKER"] = "1" if worker_id == 1 else "0" 84 | 85 | # 重新初始化 worker 日志(设置环境变量后才能正确检测多 worker 模式) 86 | from .logger import setup_worker_logging 87 | setup_worker_logging(worker_id, total_workers) 88 | 89 | try: 90 | import hypercorn.asyncio 91 | from hypercorn.config import Config 92 | except ImportError: 93 | raise ImportError("Hypercorn 未安装,请运行: pip install hypercorn") 94 | 95 | # 从模块路径加载 app 96 | app = _resolve_app_from_path(app_path) 97 | 98 | # 重建配置 99 | config = Config() 100 | for key, value in config_dict.items(): 101 | if hasattr(config, key): 102 | setattr(config, key, value) 103 | 104 | logger.info(f"Worker-{worker_id}/{total_workers} 启动中... (primary={worker_id == 1})") 105 | 106 | # 运行服务器 107 | asyncio.run(hypercorn.asyncio.serve(app, config)) 108 | 109 | 110 | class HypercornServer: 111 | """Hypercorn 服务器,支持多 workers 模式""" 112 | 113 | def __init__(self, app, host: str = "0.0.0.0", port: int = 8000, **kwargs): 114 | self.app = app 115 | self.host = host 116 | self.port = port 117 | self.kwargs = kwargs 118 | self._running = False 119 | self._workers: List[multiprocessing.Process] = [] 120 | self._import_hypercorn() 121 | 122 | def _import_hypercorn(self): 123 | """动态导入 Hypercorn""" 124 | try: 125 | import hypercorn.asyncio 126 | from hypercorn.config import Config 127 | self.hypercorn = hypercorn 128 | self._config_class = Config 129 | except ImportError: 130 | raise ImportError("Hypercorn 未安装,请运行: pip install hypercorn") 131 | 132 | def _build_config(self) -> Any: 133 | """构建 Hypercorn 配置""" 134 | config = self._config_class() 135 | config.bind = [f"{self.host}:{self.port}"] 136 | config.use_reloader = self.kwargs.get('reload', False) 137 | config.workers = self.kwargs.get('workers', 1) 138 | config.keep_alive_timeout = self.kwargs.get('keep_alive_timeout', 5) 139 | config.graceful_timeout = self.kwargs.get('graceful_timeout', 30) 140 | config.max_incomplete_request_size = self.kwargs.get('max_incomplete_request_size', 16 * 1024) 141 | config.websocket_max_size = self.kwargs.get('websocket_max_size', 16 * 1024 * 1024) 142 | 143 | # 应用其他配置 144 | for key, value in self.kwargs.items(): 145 | if hasattr(config, key) and key not in ['reload', 'workers']: 146 | setattr(config, key, value) 147 | 148 | return config 149 | 150 | def _config_to_dict(self, config) -> dict: 151 | """将配置转换为可序列化的字典""" 152 | return { 153 | 'bind': config.bind, 154 | 'use_reloader': config.use_reloader, 155 | 'keep_alive_timeout': config.keep_alive_timeout, 156 | 'graceful_timeout': config.graceful_timeout, 157 | 'max_incomplete_request_size': getattr(config, 'max_incomplete_request_size', 16 * 1024), 158 | 'websocket_max_size': getattr(config, 'websocket_max_size', 16 * 1024 * 1024), 159 | } 160 | 161 | def start(self, app_path: Optional[str] = None) -> None: 162 | """ 163 | 启动 Hypercorn 服务器 164 | 165 | Args: 166 | app_path: 应用模块路径(多 workers 模式必需),格式为 "module.path:app_name" 167 | 例如: "myapp.main:app" 或 "myapp.main:create_app()" 168 | """ 169 | try: 170 | config = self._build_config() 171 | workers = self.kwargs.get('workers', 1) 172 | 173 | self._running = True 174 | 175 | if workers > 1: 176 | # 多 workers 模式 177 | if not app_path: 178 | logger.warning( 179 | "多 workers 模式需要提供 app_path 参数(如 'myapp.main:app')," 180 | "当前回退到单进程模式" 181 | ) 182 | asyncio.run(self.hypercorn.asyncio.serve(self.app, config)) 183 | else: 184 | self._start_multiple_workers(app_path, config, workers) 185 | else: 186 | # 单 worker 模式:直接运行 187 | asyncio.run(self.hypercorn.asyncio.serve(self.app, config)) 188 | 189 | except Exception as e: 190 | logger.error(f"Hypercorn 服务器启动失败: {e}") 191 | raise 192 | finally: 193 | self._running = False 194 | self._cleanup_workers() 195 | 196 | def _start_multiple_workers(self, app_path: str, config, workers: int) -> None: 197 | """ 198 | 启动多个 worker 进程 199 | 200 | Args: 201 | app_path: 应用模块路径 202 | config: Hypercorn 配置 203 | workers: worker 数量 204 | """ 205 | # Windows 需要使用 spawn 206 | if sys.platform == 'win32': 207 | multiprocessing.set_start_method('spawn', force=True) 208 | 209 | config_dict = self._config_to_dict(config) 210 | 211 | logger.info(f"启动 {workers} 个 worker 进程...") 212 | 213 | # 创建并启动 worker 进程 214 | for i in range(workers): 215 | process = multiprocessing.Process( 216 | target=_worker_serve, 217 | args=(app_path, config_dict, i + 1, workers), 218 | name=f"hypercorn-worker-{i + 1}" 219 | ) 220 | process.start() 221 | self._workers.append(process) 222 | logger.info(f"Worker-{i + 1}/{workers} 已启动 (PID: {process.pid})") 223 | 224 | # 主进程等待所有 worker 225 | try: 226 | for process in self._workers: 227 | process.join() 228 | except KeyboardInterrupt: 229 | logger.info("收到中断信号,正在关闭所有 workers...") 230 | self._cleanup_workers() 231 | 232 | def _cleanup_workers(self) -> None: 233 | """清理所有 worker 进程""" 234 | for process in self._workers: 235 | if process.is_alive(): 236 | logger.info(f"终止 Worker (PID: {process.pid})...") 237 | process.terminate() 238 | process.join(timeout=5) 239 | if process.is_alive(): 240 | process.kill() 241 | self._workers.clear() 242 | 243 | def stop(self) -> None: 244 | """停止 Hypercorn 服务器""" 245 | self._running = False 246 | 247 | @property 248 | def is_running(self) -> bool: 249 | """检查服务器是否运行中""" 250 | return self._running 251 | 252 | def get_url(self) -> str: 253 | """获取服务器 URL""" 254 | # 使用真实 IP 地址显示(如果 host 是 0.0.0.0) 255 | display_host = get_local_ip() if self.host == "0.0.0.0" else self.host 256 | return f"http://{display_host}:{self.port}" 257 | 258 | 259 | class ServerManager: 260 | """简化的服务器管理器""" 261 | 262 | def __init__(self): 263 | self._current_server: Optional[HypercornServer] = None 264 | 265 | def start_server( 266 | self, 267 | app, 268 | host: str = "0.0.0.0", 269 | port: int = 8000, 270 | app_path: Optional[str] = None, 271 | **kwargs 272 | ) -> None: 273 | """ 274 | 启动 Hypercorn 服务器 275 | 276 | Args: 277 | app: ASGI 应用 278 | host: 主机地址 279 | port: 端口号 280 | app_path: 应用模块路径(多 workers 模式必需),格式为 "module.path:app_name" 281 | 例如: "myapp.main:app" 282 | **kwargs: 其他配置参数,包括: 283 | - workers: worker 进程数量,默认 1 284 | - reload: 是否启用热重载 285 | - keep_alive_timeout: keep-alive 超时时间 286 | - graceful_timeout: 优雅关闭超时时间 287 | """ 288 | if self._current_server and self._current_server.is_running: 289 | logger.warning("服务器已在运行中,请先停止当前服务器") 290 | return 291 | 292 | self._current_server = HypercornServer(app, host, port, **kwargs) 293 | 294 | # 注册信号处理器 295 | self._register_signal_handlers() 296 | 297 | try: 298 | self._current_server.start(app_path=app_path) 299 | except KeyboardInterrupt: 300 | logger.info("收到中断信号,正在关闭服务器...") 301 | finally: 302 | self.stop_server() 303 | 304 | def stop_server(self) -> None: 305 | """停止当前服务器""" 306 | if self._current_server: 307 | self._current_server.stop() 308 | self._current_server = None 309 | 310 | def _register_signal_handlers(self) -> None: 311 | """注册信号处理器""" 312 | def signal_handler(signum, frame): 313 | logger.info(f"收到信号 {signum},正在关闭服务器...") 314 | self.stop_server() 315 | sys.exit(0) 316 | 317 | signal.signal(signal.SIGINT, signal_handler) 318 | signal.signal(signal.SIGTERM, signal_handler) 319 | 320 | def get_server_info(self) -> Dict[str, Any]: 321 | """获取服务器信息""" 322 | if not self._current_server: 323 | return {"status": "not_running"} 324 | 325 | return { 326 | "status": "running" if self._current_server.is_running else "stopped", 327 | "url": self._current_server.get_url(), 328 | "host": self._current_server.host, 329 | "port": self._current_server.port, 330 | } 331 | 332 | 333 | # 全局服务器管理器实例 334 | server_manager = ServerManager() 335 | 336 | 337 | def start_server( 338 | app, 339 | host: str = "0.0.0.0", 340 | port: int = 8000, 341 | app_path: Optional[str] = None, 342 | **kwargs 343 | ) -> None: 344 | """ 345 | 启动服务器的便捷函数 346 | 347 | Args: 348 | app: ASGI 应用 349 | host: 主机地址 350 | port: 端口号 351 | app_path: 应用模块路径(多 workers 模式必需),格式为 "module.path:app_name" 352 | **kwargs: 其他配置参数 353 | 354 | Example: 355 | # 单 worker 模式 356 | start_server(app, host="0.0.0.0", port=8000) 357 | 358 | # 多 workers 模式(4个进程) 359 | start_server( 360 | app, 361 | host="0.0.0.0", 362 | port=8000, 363 | app_path="myapp.main:app", 364 | workers=4 365 | ) 366 | """ 367 | server_manager.start_server(app, host, port, app_path=app_path, **kwargs) -------------------------------------------------------------------------------- /examples/convention_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | 约定优于配置示例应用 5 | 6 | 展示 MyBoot 框架的约定优于配置特性 7 | """ 8 | 9 | import sys 10 | import threading 11 | from pathlib import Path 12 | 13 | from myboot.core.application import Application 14 | from myboot.core.config import get_config 15 | from myboot.core.decorators import ( 16 | get, post, put, delete, 17 | cron, interval, once, 18 | service, client, middleware, 19 | rest_controller, component 20 | ) 21 | from myboot.jobs.scheduled_job import ScheduledJob 22 | from myboot.core.scheduler import get_scheduler 23 | 24 | # 添加项目根目录到 Python 路径 25 | project_root = Path(__file__).parent.parent 26 | sys.path.insert(0, str(project_root)) 27 | 28 | # 创建应用实例 29 | app = Application( 30 | name="约定优于配置示例", 31 | auto_configuration=True, # 启用自动配置 32 | auto_discover_package="examples" # 自动发现当前包 33 | ) 34 | 35 | 36 | # ==================== 服务层 ==================== 37 | 38 | @service() 39 | class UserService: 40 | """用户服务 - 自动注册为 'user_service'""" 41 | 42 | def __init__(self): 43 | self.users = {} 44 | print("✅ UserService 已初始化") 45 | 46 | def get_user(self, user_id: int): 47 | """获取用户""" 48 | return self.users.get(user_id, {"id": user_id, "name": f"用户{user_id}"}) 49 | 50 | def create_user(self, name: str, email: str): 51 | """创建用户""" 52 | user_id = len(self.users) + 1 53 | user = {"id": user_id, "name": name, "email": email} 54 | self.users[user_id] = user 55 | return user 56 | 57 | def update_user(self, user_id: int, **kwargs): 58 | """更新用户""" 59 | if user_id in self.users: 60 | self.users[user_id].update(kwargs) 61 | return self.users[user_id] 62 | return None 63 | 64 | def delete_user(self, user_id: int): 65 | """删除用户""" 66 | return self.users.pop(user_id, None) 67 | 68 | 69 | @service('email_service') 70 | class EmailService: 71 | """邮件服务 - 注册为 'email_service'""" 72 | 73 | def __init__(self): 74 | print("✅ EmailService 已初始化") 75 | 76 | def send_email(self, to: str, subject: str, body: str): 77 | """发送邮件""" 78 | print(f"📧 发送邮件到 {to}: {subject}") 79 | return {"status": "sent", "to": to, "subject": subject} 80 | 81 | 82 | # ==================== 客户端层 ==================== 83 | 84 | @client() 85 | class DatabaseClient: 86 | """数据库客户端 - 自动注册为 'database_client'""" 87 | 88 | def __init__(self): 89 | print("✅ DatabaseClient 已初始化") 90 | 91 | def connect(self): 92 | """连接数据库""" 93 | print("🔗 连接数据库") 94 | return True 95 | 96 | def query(self, sql: str): 97 | """执行查询""" 98 | print(f"📊 执行查询: {sql}") 99 | return [] 100 | 101 | 102 | @client('redis_client') 103 | class RedisClient: 104 | """Redis 客户端 - 注册为 'redis_client'""" 105 | 106 | def __init__(self): 107 | print("✅ RedisClient 已初始化") 108 | self.value = None 109 | 110 | def get(self, key: str): 111 | """获取缓存""" 112 | print(f"📦 获取缓存: {key}") 113 | return self.value 114 | 115 | def set(self, key: str, value: str): 116 | """设置缓存""" 117 | print(f"💾 设置缓存: {key} = {value}") 118 | self.value = value 119 | 120 | 121 | # ==================== 中间件 ==================== 122 | 123 | @middleware(order=1) 124 | async def logging_middleware(request, next_handler): 125 | """日志中间件 - 自动注册,处理所有请求""" 126 | print(f"📝 请求日志: {request.method} {request.url}") 127 | response = await next_handler(request) 128 | print(f"📝 响应日志: {response.status_code}") 129 | return response 130 | 131 | 132 | @middleware(order=2) 133 | async def timing_middleware(request, next_handler): 134 | """计时中间件 - 自动注册,处理所有请求""" 135 | import time 136 | start_time = time.time() 137 | response = await next_handler(request) 138 | end_time = time.time() 139 | elapsed = end_time - start_time 140 | print(f"⏱️ 请求耗时: {elapsed:.3f}s - {request.method} {request.url.path}") 141 | return response 142 | 143 | 144 | @middleware(order=3, path_filter='/api/*') 145 | async def api_middleware(request, next_handler): 146 | """API 中间件 - 只处理 /api/* 路径的请求""" 147 | print(f"🔌 API 请求: {request.method} {request.url.path}") 148 | # 可以在这里添加 API 特定的逻辑,如认证、限流等 149 | response = await next_handler(request) 150 | return response 151 | 152 | 153 | @middleware(order=4, methods=['POST', 'PUT', 'PATCH']) 154 | async def modify_middleware(request, next_handler): 155 | """修改操作中间件 - 只处理 POST、PUT、PATCH 请求""" 156 | print(f"✏️ 修改操作: {request.method} {request.url.path}") 157 | response = await next_handler(request) 158 | return response 159 | 160 | 161 | @middleware( 162 | order=5, 163 | path_filter=['/users/*', '/api/products/*'], 164 | condition=lambda req: req.headers.get('user-agent', '').startswith('Mozilla') 165 | ) 166 | async def browser_only_middleware(request, next_handler): 167 | """浏览器专用中间件 - 只处理浏览器请求且匹配特定路径""" 168 | print(f"🌐 浏览器请求: {request.headers.get('user-agent', 'Unknown')}") 169 | response = await next_handler(request) 170 | return response 171 | 172 | 173 | # ==================== 定时任务组件 ==================== 174 | # 注意:定时任务必须在 @component 装饰的类中定义,支持依赖注入 175 | 176 | 177 | @component() 178 | class ScheduledJobs: 179 | """定时任务组件 - 使用 @component 装饰器,支持依赖注入""" 180 | 181 | def __init__(self): 182 | print("✅ ScheduledJobs 已初始化") 183 | 184 | @cron('0 */1 * * * *', enabled=True) # 每分钟执行 185 | def heartbeat(self): 186 | """心跳任务""" 187 | print("💓 心跳检测 - 系统运行正常") 188 | 189 | @interval(minutes=10, enabled=get_config('jobs.cleanup_task.enabled', True)) 190 | def cleanup_task(self): 191 | """清理任务 - 从配置文件读取 enabled 状态""" 192 | print("🧹 执行清理任务") 193 | 194 | @once('2025-12-31 23:59:59') 195 | def new_year_task(self): 196 | """新年任务""" 197 | print("🎉 新年任务执行") 198 | 199 | 200 | # ==================== REST 控制器 ==================== 201 | # 注意:路由必须在 @rest_controller 装饰的类中定义 202 | 203 | @rest_controller('/') 204 | class HomeController: 205 | """首页控制器""" 206 | 207 | @get('/') 208 | def home(self): 209 | """首页 - GET /""" 210 | return { 211 | "message": "欢迎使用 MyBoot 约定优于配置示例", 212 | "features": [ 213 | "自动发现和注册组件", 214 | "约定优于配置", 215 | "零配置启动", 216 | "REST 控制器", 217 | "依赖注入", 218 | "定时任务" 219 | ] 220 | } 221 | 222 | 223 | @rest_controller('/api/users') 224 | class UserController: 225 | """用户控制器 - 使用依赖注入""" 226 | 227 | def __init__(self, user_service: UserService, email_service: EmailService): 228 | self.user_service = user_service 229 | self.email_service = email_service 230 | 231 | @get('/') 232 | def list_users(self): 233 | """获取用户列表 - GET /api/users""" 234 | return {"users": list(self.user_service.users.values())} 235 | 236 | @get('/{user_id}') 237 | def get_user(self, user_id: int): 238 | """获取单个用户 - GET /api/users/{user_id}""" 239 | return self.user_service.get_user(user_id) 240 | 241 | @post('/') 242 | def create_user(self, name: str, email: str): 243 | """创建用户 - POST /api/users""" 244 | user = self.user_service.create_user(name, email) 245 | self.email_service.send_email(email, "欢迎注册", f"欢迎 {name} 注册我们的服务!") 246 | return {"message": "用户创建成功", "user": user} 247 | 248 | @put('/{user_id}') 249 | def update_user(self, user_id: int, name: str = None, email: str = None): 250 | """更新用户 - PUT /api/users/{user_id}""" 251 | update_data = {} 252 | if name: 253 | update_data['name'] = name 254 | if email: 255 | update_data['email'] = email 256 | 257 | user = self.user_service.update_user(user_id, **update_data) 258 | if user: 259 | return {"message": "用户更新成功", "user": user} 260 | return {"error": "用户不存在"} 261 | 262 | @delete('/{user_id}') 263 | def delete_user(self, user_id: int): 264 | """删除用户 - DELETE /api/users/{user_id}""" 265 | user = self.user_service.delete_user(user_id) 266 | if user: 267 | return {"message": "用户删除成功", "user": user} 268 | return {"error": "用户不存在"} 269 | 270 | 271 | @rest_controller('/api/products') 272 | class ProductController: 273 | """产品控制器 - 使用依赖注入""" 274 | 275 | def __init__(self, redis_client: RedisClient): 276 | self.redis_client = redis_client 277 | self.products = { 278 | 1: {"id": 1, "name": "产品1", "price": 100}, 279 | 2: {"id": 2, "name": "产品2", "price": 200} 280 | } 281 | 282 | @get('/') 283 | def list_products(self): 284 | """获取产品列表 - GET /api/products""" 285 | if self.redis_client: 286 | print(self.redis_client.get('app_status')) 287 | return {"products": list(self.products.values())} 288 | 289 | @get('/{product_id}') 290 | def get_product(self, product_id: int): 291 | """获取单个产品 - GET /api/products/{product_id}""" 292 | return self.products.get(product_id, {"error": "产品不存在"}) 293 | 294 | @post('/') 295 | def create_product(self, name: str, price: float): 296 | """创建产品 - POST /api/products""" 297 | product_id = max(self.products.keys()) + 1 298 | product = {"id": product_id, "name": name, "price": price} 299 | self.products[product_id] = product 300 | return {"message": "产品创建成功", "product": product} 301 | 302 | @put('/{product_id}') 303 | def update_product(self, product_id: int, name: str = None, price: float = None): 304 | """更新产品 - PUT /api/products/{product_id}""" 305 | if product_id not in self.products: 306 | return {"error": "产品不存在"} 307 | 308 | if name: 309 | self.products[product_id]['name'] = name 310 | if price: 311 | self.products[product_id]['price'] = price 312 | 313 | return {"message": "产品更新成功", "product": self.products[product_id]} 314 | 315 | @delete('/{product_id}') 316 | def delete_product(self, product_id: int): 317 | """删除产品 - DELETE /api/products/{product_id}""" 318 | if product_id in self.products: 319 | product = self.products.pop(product_id) 320 | return {"message": "产品删除成功", "product": product} 321 | return {"error": "产品不存在"} 322 | 323 | 324 | def generate_report(report_type: str): 325 | """生成报告任务""" 326 | import time 327 | print(f"开始生成 {report_type} 报告") 328 | time.sleep(10) # 模拟报告生成 329 | return {"type": report_type, "status": "completed"} 330 | 331 | 332 | @rest_controller('/api/reports') 333 | class ReportController: 334 | """报告控制器""" 335 | 336 | def __init__(self): 337 | self.scheduler = get_scheduler() 338 | 339 | @post('/generate') 340 | def create_report(self, report_type: str): 341 | """创建报告生成任务""" 342 | # 创建自定义 ScheduledJob 343 | class ReportJob(ScheduledJob): 344 | def __init__(self, report_type: str): 345 | super().__init__( 346 | name=f"生成{report_type}报告", 347 | timeout=300 # 5分钟超时 348 | ) 349 | self.report_type = report_type 350 | 351 | def run(self, *args, **kwargs): 352 | return generate_report(self.report_type) 353 | 354 | # 创建任务实例 355 | job = ReportJob(report_type) 356 | 357 | # 添加到调度器并执行 358 | job_id = self.scheduler.add_scheduled_job(job) 359 | thread = threading.Thread(target=job.execute) 360 | thread.daemon = True 361 | thread.start() 362 | 363 | return {"message": "报告生成任务已创建", "job_id": job_id} 364 | 365 | @get('/status/{job_id}') 366 | def get_status(self, job_id: str): 367 | """查询任务状态""" 368 | job = self.scheduler.get_scheduled_job(job_id) 369 | if job: 370 | return job.get_info() 371 | return {"error": "任务不存在"} 372 | 373 | 374 | # ==================== 启动钩子 ==================== 375 | @app.add_startup_hook 376 | def startup_hook(): 377 | """启动钩子""" 378 | print("🚀 应用启动钩子执行") 379 | 380 | from myboot.core.application import get_client 381 | 382 | # 初始化数据库连接 383 | db_client = get_client('database_client') 384 | if db_client: 385 | db_client.connect() 386 | 387 | # 初始化 Redis 连接 388 | redis_client = get_client('redis_client') 389 | if redis_client: 390 | redis_client.set('app_status', 'running') 391 | 392 | 393 | @app.add_shutdown_hook 394 | def shutdown_hook(): 395 | """关闭钩子""" 396 | print("🛑 应用关闭钩子执行") 397 | 398 | 399 | # ==================== 主程序 ==================== 400 | 401 | if __name__ == "__main__": 402 | print("=" * 60) 403 | print("🎯 MyBoot 约定优于配置示例") 404 | print("=" * 60) 405 | print() 406 | print("✨ 特性展示:") 407 | print(" • 自动发现和注册组件") 408 | print(" • 约定优于配置") 409 | print(" • 零配置启动") 410 | print(" • 自动服务注入") 411 | print(" • 自动路由注册") 412 | print(" • 自动任务调度") 413 | print() 414 | print("🌐 访问地址:") 415 | print(" • 应用: http://localhost:8000") 416 | print(" • API 文档: http://localhost:8000/docs") 417 | print(" • 健康检查: http://localhost:8000/health") 418 | print() 419 | print("📚 API 端点(通过 @rest_controller 定义):") 420 | print(" • GET / - 首页") 421 | print(" • GET /api/users - 用户列表") 422 | print(" • GET /api/users/{id} - 获取用户") 423 | print(" • POST /api/users - 创建用户") 424 | print(" • PUT /api/users/{id} - 更新用户") 425 | print(" • DELETE /api/users/{id} - 删除用户") 426 | print(" • GET /api/products - 产品列表") 427 | print(" • GET /api/products/{id} - 获取产品") 428 | print(" • POST /api/products - 创建产品") 429 | print(" • PUT /api/products/{id} - 更新产品") 430 | print(" • DELETE /api/products/{id} - 删除产品") 431 | print() 432 | print("⏰ 定时任务(通过 @component 组件定义):") 433 | print(" • 心跳检测 (每分钟)") 434 | print(" • 清理任务 (每10分钟)") 435 | print(" • 新年任务 (2025-12-31 23:59:59)") 436 | print() 437 | print("=" * 60) 438 | 439 | # 启动应用 440 | app.run(host="0.0.0.0", port=8000, reload=False) 441 | -------------------------------------------------------------------------------- /docs/dependency-injection.md: -------------------------------------------------------------------------------- 1 | # 依赖注入使用指南 2 | 3 | MyBoot 框架提供了基于 `dependency_injector` 的自动依赖注入功能,让您可以轻松管理服务之间的依赖关系,无需手动获取和传递依赖。 4 | 5 | ## 目录 6 | 7 | - [快速开始](#快速开始) 8 | - [基本用法](#基本用法) 9 | - [声明依赖](#1-声明依赖) 10 | - [服务命名规则](#2-服务命名规则) 11 | - [多级依赖](#3-多级依赖) 12 | - [可选依赖](#4-可选依赖) 13 | - [Client 依赖注入](#5-client-依赖注入) 14 | - [Component 组件](#6-component-组件) 15 | - [高级特性](#高级特性) 16 | - [最佳实践](#最佳实践) 17 | - [常见问题](#常见问题) 18 | 19 | ## 快速开始 20 | 21 | ### 安装依赖 22 | 23 | 确保已安装 `dependency_injector`: 24 | 25 | ```bash 26 | pip install dependency-injector 27 | ``` 28 | 29 | ### 基本示例 30 | 31 | ```python 32 | from myboot.core.decorators import service 33 | 34 | @service() 35 | class UserService: 36 | """用户服务""" 37 | def __init__(self): 38 | self.users = {} 39 | 40 | def get_user(self, user_id: int): 41 | return self.users.get(user_id) 42 | 43 | @service() 44 | class EmailService: 45 | """邮件服务""" 46 | def send_email(self, to: str, subject: str): 47 | print(f"发送邮件到 {to}: {subject}") 48 | 49 | @service() 50 | class OrderService: 51 | """订单服务 - 自动注入 UserService 和 EmailService""" 52 | def __init__(self, user_service: UserService, email_service: EmailService): 53 | self.user_service = user_service 54 | self.email_service = email_service 55 | 56 | def create_order(self, user_id: int, product: str): 57 | user = self.user_service.get_user(user_id) 58 | self.email_service.send_email(user['email'], "订单创建", f"您的订单 {product} 已创建") 59 | ``` 60 | 61 | 框架会自动: 62 | 63 | 1. 检测 `OrderService` 的依赖(`UserService` 和 `EmailService`) 64 | 2. 按正确的顺序初始化服务 65 | 3. 自动注入依赖到 `OrderService` 的构造函数 66 | 67 | ## 基本用法 68 | 69 | ### 1. 声明依赖 70 | 71 | 通过类型注解声明依赖是最简单的方式: 72 | 73 | ```python 74 | @service() 75 | class ProductService: 76 | def __init__(self, user_service: UserService, cache_service: CacheService): 77 | self.user_service = user_service 78 | self.cache_service = cache_service 79 | ``` 80 | 81 | 框架会自动: 82 | 83 | - 从类型注解中识别依赖的服务类 84 | - 将类名转换为服务名(如 `UserService` → `user_service`) 85 | - 自动注入对应的服务实例 86 | 87 | ### 2. 服务命名规则 88 | 89 | 服务名称遵循以下规则: 90 | 91 | - **默认命名**:类名自动转换为下划线分隔的小写形式 92 | 93 | - `UserService` → `user_service` 94 | - `EmailService` → `email_service` 95 | - `DatabaseClient` → `database_client` 96 | 97 | - **自定义命名**:通过装饰器参数指定 98 | ```python 99 | @service('custom_user_service') 100 | class UserService: 101 | pass 102 | ``` 103 | 104 | ### 3. 多级依赖 105 | 106 | 支持多级依赖,框架会自动处理依赖顺序: 107 | 108 | ```python 109 | @service() 110 | class DatabaseClient: 111 | def __init__(self): 112 | self.connection = None 113 | 114 | @service() 115 | class UserRepository: 116 | def __init__(self, db: DatabaseClient): 117 | self.db = db 118 | 119 | @service() 120 | class UserService: 121 | def __init__(self, user_repo: UserRepository): 122 | self.user_repo = user_repo 123 | ``` 124 | 125 | 依赖顺序:`DatabaseClient` → `UserRepository` → `UserService` 126 | 127 | ### 4. 可选依赖 128 | 129 | 使用 `Optional` 类型注解声明可选依赖: 130 | 131 | ```python 132 | from typing import Optional 133 | 134 | @service() 135 | class CacheService: 136 | pass 137 | 138 | @service() 139 | class ProductService: 140 | # cache_service 是可选的,如果不存在则为 None 141 | def __init__(self, cache_service: Optional[CacheService] = None): 142 | self.cache_service = cache_service 143 | if self.cache_service: 144 | # 使用缓存服务 145 | pass 146 | ``` 147 | 148 | ### 5. Client 依赖注入 149 | 150 | 除了 Service 之间的依赖注入,框架还支持将 Client 注入到 Controller 或 Service 中: 151 | 152 | ```python 153 | from myboot.core.decorators import client, service, rest_controller, get 154 | 155 | @client() 156 | class HttpClient: 157 | """HTTP 客户端""" 158 | def request(self, url: str): 159 | return {"url": url} 160 | 161 | @client(name="redis_client") # 自定义名称 162 | class RedisClient: 163 | """Redis 客户端""" 164 | def get(self, key: str): 165 | return None 166 | 167 | @service() 168 | class UserService: 169 | """注入 Client 到 Service""" 170 | def __init__(self, http_client: HttpClient): 171 | self.http_client = http_client 172 | 173 | @rest_controller("/api") 174 | class UserController: 175 | """注入 Client 和 Service 到 Controller""" 176 | def __init__(self, user_service: UserService, redis_client: RedisClient): 177 | self.user_service = user_service 178 | self.redis_client = redis_client 179 | 180 | @get("/users") 181 | def list_users(self): 182 | return [] 183 | ``` 184 | 185 | #### Client 命名规则 186 | 187 | - **默认命名**:类名自动转换为下划线形式 188 | 189 | - `HttpClient` → `http_client` 190 | - `RedisClient` → `redis_client` 191 | 192 | - **自定义命名**:通过装饰器参数指定 193 | ```python 194 | @client(name="my_redis") 195 | class RedisClient: 196 | pass 197 | ``` 198 | 199 | #### Client 查找方式 200 | 201 | 框架支持多种方式查找 Client 依赖: 202 | 203 | ```python 204 | @client(name="my_http") # 自定义名称 205 | class HttpClient: 206 | pass 207 | 208 | @rest_controller("/api") 209 | class MyController: 210 | # 以下方式都可以成功注入: 211 | 212 | # 方式1:按自定义名称(参数名匹配) 213 | def __init__(self, my_http: HttpClient): 214 | pass 215 | 216 | # 方式2:按自动转换名称 217 | def __init__(self, http_client: HttpClient): 218 | pass 219 | 220 | # 方式3:按类型匹配(参数名任意) 221 | def __init__(self, client: HttpClient): 222 | pass 223 | 224 | # 方式4:显式指定名称 225 | def __init__(self, x: Provide['my_http']): 226 | pass 227 | ``` 228 | 229 | ### 6. Component 组件 230 | 231 | `@component` 装饰器用于注册通用组件,支持依赖注入。它可用于任意需要托管的类(工具类、配置类、包含定时任务的类等)。 232 | 233 | #### 基本用法 234 | 235 | ```python 236 | from myboot.core.decorators import component 237 | 238 | @component() 239 | class EmailHelper: 240 | """邮件工具类""" 241 | def send(self, to: str, content: str): 242 | print(f"发送邮件到 {to}: {content}") 243 | ``` 244 | 245 | #### 带依赖注入 246 | 247 | ```python 248 | from myboot.core.decorators import component, client 249 | 250 | @client() 251 | class SmtpClient: 252 | def send_mail(self, to: str, subject: str, body: str): 253 | pass 254 | 255 | @component(name='email_helper') 256 | class EmailHelper: 257 | """带依赖注入的组件""" 258 | def __init__(self, smtp_client: SmtpClient): 259 | self.smtp = smtp_client 260 | 261 | def send(self, to: str, subject: str, body: str): 262 | self.smtp.send_mail(to, subject, body) 263 | ``` 264 | 265 | #### 包含定时任务的组件 266 | 267 | **重要**:定时任务(`@cron`、`@interval`、`@once`)**必须**在 `@component` 装饰的类中定义。这是定义定时任务的唯一方式,不再支持模块级函数或 `@service` 类中的定时任务。 268 | 269 | ```python 270 | from myboot.core.decorators import component, service, cron, interval 271 | 272 | @service() 273 | class DataService: 274 | def sync(self): 275 | print("同步数据...") 276 | 277 | def health_check(self): 278 | print("健康检查...") 279 | 280 | @component() 281 | class DataSyncJobs: 282 | """数据同步任务集合 - 自动注入 DataService""" 283 | 284 | def __init__(self, data_service: DataService): 285 | self.data_service = data_service 286 | 287 | @cron("0 2 * * *") # 每天凌晨 2 点 288 | def sync_daily_data(self): 289 | """每日数据同步""" 290 | self.data_service.sync() 291 | 292 | @interval(hours=1) # 每小时 293 | def check_data_health(self): 294 | """数据健康检查""" 295 | self.data_service.health_check() 296 | ``` 297 | 298 | **注意**: 299 | - 定时任务方法会在组件注册时自动扫描并注册到调度器 300 | - 组件支持依赖注入,可以在构造函数中注入所需的服务 301 | 302 | #### Component 配置选项 303 | 304 | ```python 305 | @component( 306 | name='my_component', # 组件名称,默认使用类名的 snake_case 307 | scope='singleton', # 生命周期:'singleton'(默认)或 'prototype' 308 | lazy=False, # 是否懒加载 309 | primary=False # 当按类型获取有多个匹配时,是否为首选 310 | ) 311 | class MyComponent: 312 | pass 313 | ``` 314 | 315 | #### 从容器获取组件 316 | 317 | ```python 318 | from myboot.core.application import app 319 | 320 | # 方式1:通过 container 获取 321 | email_helper = app().container.get('email_helper') 322 | 323 | # 方式2:通过 Application 直接获取 324 | email_helper = app().get_component('email_helper') 325 | 326 | # 方式3:依赖注入(推荐) 327 | @component() 328 | class NotificationService: 329 | def __init__(self, email_helper: EmailHelper): 330 | self.email_helper = email_helper 331 | ``` 332 | 333 | ## 高级特性 334 | 335 | ### 1. 显式指定服务名 336 | 337 | 如果服务名与类名转换规则不匹配,可以使用 `Provide` 类型提示: 338 | 339 | ```python 340 | from myboot.core.di import Provide 341 | 342 | @service('custom_user_service') 343 | class UserService: 344 | pass 345 | 346 | @service() 347 | class OrderService: 348 | def __init__(self, user_service: Provide['custom_user_service']): 349 | self.user_service = user_service 350 | ``` 351 | 352 | ### 2. 服务生命周期 353 | 354 | 通过 `scope` 参数控制服务的生命周期: 355 | 356 | ```python 357 | # 单例模式(默认) 358 | @service(scope='singleton') 359 | class UserService: 360 | pass 361 | 362 | # 工厂模式(每次创建新实例) 363 | @service(scope='factory') 364 | class TaskService: 365 | pass 366 | ``` 367 | 368 | ### 3. 循环依赖检测 369 | 370 | 框架会自动检测循环依赖并抛出清晰的错误: 371 | 372 | ```python 373 | @service() 374 | class ServiceA: 375 | def __init__(self, service_b: ServiceB): 376 | pass 377 | 378 | @service() 379 | class ServiceB: 380 | def __init__(self, service_a: ServiceA): 381 | pass 382 | ``` 383 | 384 | 错误信息: 385 | 386 | ``` 387 | ValueError: 检测到循环依赖: service_a -> service_b -> service_a。 388 | 请重构代码以消除循环依赖。 389 | ``` 390 | 391 | ### 4. 获取服务实例 392 | 393 | 在路由或其他地方获取服务实例: 394 | 395 | ```python 396 | from myboot.core.application import get_service 397 | 398 | @get('/users/{user_id}') 399 | def get_user(user_id: int): 400 | user_service = get_service('user_service') 401 | return user_service.get_user(user_id) 402 | ``` 403 | 404 | ## 最佳实践 405 | 406 | ### 1. 使用类型注解 407 | 408 | 推荐使用类型注解声明依赖,代码更清晰: 409 | 410 | ```python 411 | # ✅ 推荐 412 | @service() 413 | class OrderService: 414 | def __init__(self, user_service: UserService, email_service: EmailService): 415 | self.user_service = user_service 416 | self.email_service = email_service 417 | 418 | # ❌ 不推荐(需要手动获取) 419 | @service() 420 | class OrderService: 421 | def __init__(self): 422 | from myboot.core.application import get_service 423 | self.user_service = get_service('user_service') 424 | self.email_service = get_service('email_service') 425 | ``` 426 | 427 | ### 2. 避免循环依赖 428 | 429 | 设计服务时避免循环依赖: 430 | 431 | ```python 432 | # ✅ 好的设计 433 | @service() 434 | class UserService: 435 | def __init__(self, user_repo: UserRepository): 436 | self.user_repo = user_repo 437 | 438 | @service() 439 | class OrderService: 440 | def __init__(self, user_service: UserService, order_repo: OrderRepository): 441 | self.user_service = user_service 442 | self.order_repo = order_repo 443 | 444 | # ❌ 避免循环依赖 445 | @service() 446 | class ServiceA: 447 | def __init__(self, service_b: ServiceB): 448 | pass 449 | 450 | @service() 451 | class ServiceB: 452 | def __init__(self, service_a: ServiceA): 453 | pass 454 | ``` 455 | 456 | ### 3. 使用接口而非具体实现 457 | 458 | 虽然 Python 没有接口,但可以通过抽象基类或协议定义接口: 459 | 460 | ```python 461 | from abc import ABC, abstractmethod 462 | 463 | class IUserRepository(ABC): 464 | @abstractmethod 465 | def get_user(self, user_id: int): 466 | pass 467 | 468 | @service() 469 | class UserRepository(IUserRepository): 470 | def get_user(self, user_id: int): 471 | return {"id": user_id} 472 | 473 | @service() 474 | class UserService: 475 | def __init__(self, user_repo: IUserRepository): 476 | self.user_repo = user_repo 477 | ``` 478 | 479 | ### 4. 合理使用可选依赖 480 | 481 | 对于非必需的依赖,使用 `Optional`: 482 | 483 | ```python 484 | from typing import Optional 485 | 486 | @service() 487 | class ProductService: 488 | def __init__( 489 | self, 490 | db: DatabaseClient, # 必需依赖 491 | cache: Optional[CacheService] = None # 可选依赖 492 | ): 493 | self.db = db 494 | self.cache = cache 495 | ``` 496 | 497 | ## 常见问题 498 | 499 | ### Q1: 依赖注入失败怎么办? 500 | 501 | 如果依赖注入失败,框架会自动回退到传统方式(直接实例化)。检查日志中的错误信息: 502 | 503 | 1. **依赖的服务未注册**:确保依赖的服务已使用 `@service()` 装饰器 504 | 2. **服务名不匹配**:检查服务名是否正确(类名转下划线命名) 505 | 3. **循环依赖**:重构代码消除循环依赖 506 | 507 | ### Q2: 如何调试依赖关系? 508 | 509 | 框架会在日志中输出依赖关系信息: 510 | 511 | ``` 512 | 已注册服务提供者: user_service (依赖: set()) 513 | 已注册服务提供者: order_service (依赖: {'user_service', 'email_service'}) 514 | ``` 515 | 516 | ### Q3: 可以在运行时动态获取服务吗? 517 | 518 | 可以,使用 `get_service()` 函数: 519 | 520 | ```python 521 | from myboot.core.application import get_service 522 | 523 | def some_function(): 524 | user_service = get_service('user_service') 525 | if user_service: 526 | # 使用服务 527 | pass 528 | ``` 529 | 530 | ### Q4: 支持异步服务吗? 531 | 532 | 目前依赖注入主要支持同步服务。对于异步服务,建议在服务内部处理异步逻辑。 533 | 534 | ### Q5: 如何测试带依赖的服务? 535 | 536 | 在测试中,可以手动创建服务实例并注入 mock 对象: 537 | 538 | ```python 539 | def test_order_service(): 540 | # 创建 mock 依赖 541 | mock_user_service = MockUserService() 542 | mock_email_service = MockEmailService() 543 | 544 | # 创建服务实例 545 | order_service = OrderService(mock_user_service, mock_email_service) 546 | 547 | # 测试 548 | assert order_service is not None 549 | ``` 550 | 551 | ## 完整示例 552 | 553 | ```python 554 | from myboot.core.decorators import service, get 555 | from myboot.core.application import get_service 556 | from typing import Optional 557 | 558 | # 基础服务 559 | @service() 560 | class DatabaseClient: 561 | def __init__(self): 562 | self.connection = "connected" 563 | print("✅ DatabaseClient 已初始化") 564 | 565 | @service() 566 | class CacheService: 567 | def __init__(self): 568 | self.cache = {} 569 | print("✅ CacheService 已初始化") 570 | 571 | # 仓储层 572 | @service() 573 | class UserRepository: 574 | def __init__(self, db: DatabaseClient): 575 | self.db = db 576 | print("✅ UserRepository 已初始化(依赖: DatabaseClient)") 577 | 578 | def find_by_id(self, user_id: int): 579 | return {"id": user_id, "name": f"用户{user_id}"} 580 | 581 | # 服务层 582 | @service() 583 | class UserService: 584 | def __init__( 585 | self, 586 | user_repo: UserRepository, 587 | cache: Optional[CacheService] = None 588 | ): 589 | self.user_repo = user_repo 590 | self.cache = cache 591 | print("✅ UserService 已初始化(依赖: UserRepository, CacheService)") 592 | 593 | def get_user(self, user_id: int): 594 | # 尝试从缓存获取 595 | if self.cache and user_id in self.cache.cache: 596 | return self.cache.cache[user_id] 597 | 598 | # 从数据库获取 599 | user = self.user_repo.find_by_id(user_id) 600 | 601 | # 存入缓存 602 | if self.cache: 603 | self.cache.cache[user_id] = user 604 | 605 | return user 606 | 607 | # 路由层 608 | @get('/users/{user_id}') 609 | def get_user(user_id: int): 610 | user_service = get_service('user_service') 611 | return user_service.get_user(user_id) 612 | ``` 613 | 614 | ## 总结 615 | 616 | 依赖注入功能让您能够: 617 | 618 | - ✅ 自动管理服务依赖关系 619 | - ✅ 无需手动获取和传递依赖 620 | - ✅ 支持多级依赖和可选依赖 621 | - ✅ 自动检测循环依赖 622 | - ✅ 支持 Client 注入到 Service 和 Controller 623 | - ✅ 支持 Component 组件,可包含定时任务 624 | - ✅ 支持多种依赖查找方式(名称、类型) 625 | - ✅ 保持向后兼容,现有代码无需修改 626 | 627 | 开始使用依赖注入,让代码更加清晰和可维护! 628 | --------------------------------------------------------------------------------