├── 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 |
--------------------------------------------------------------------------------