fastapi-augment 0.1.5__tar.gz → 0.1.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/PKG-INFO +200 -23
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/README.md +199 -22
- fastapi_augment-0.1.6/VERSION +1 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/pyproject.toml +10 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/common/__init__.py +27 -4
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/common/app_discovery.py +25 -24
- fastapi_augment-0.1.6/src/fastapi_augment/common/asgi_types.py +89 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/common/exception_handlers.py +1 -1
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/common/utils/strings.py +4 -3
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/config/base_settings.py +10 -10
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/config/database_settings.py +3 -3
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/db/sqlalchemy/engine.py +16 -10
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/db/sqlalchemy/repository_base.py +306 -12
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/factory.py +1 -1
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/health/router.py +2 -1
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/lifespan.py +3 -20
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/logger/setup.py +2 -1
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/openapi.py +4 -4
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/schemas/__init__.py +4 -1
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/schemas/request.py +3 -5
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/schemas/response.py +3 -3
- fastapi_augment-0.1.6/src/fastapi_augment/schemas/types.py +46 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment.egg-info/PKG-INFO +200 -23
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment.egg-info/SOURCES.txt +4 -1
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_app_discovery.py +72 -22
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_db_repository.py +2 -1
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_exception_handlers.py +1 -1
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_factory.py +3 -3
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_lifespan.py +1 -1
- fastapi_augment-0.1.6/tests/test_soft_delete.py +353 -0
- fastapi_augment-0.1.6/tests/test_strings.py +153 -0
- fastapi_augment-0.1.5/VERSION +0 -1
- fastapi_augment-0.1.5/src/fastapi_augment/schemas/types.py +0 -11
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/setup.cfg +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/__init__.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/common/constants.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/common/exceptions.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/common/utils/__init__.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/common/utils/paths.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/config/__init__.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/db/__init__.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/db/sqlalchemy/__init__.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/db/sqlalchemy/alembic/README +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/db/sqlalchemy/alembic/__init__.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/db/sqlalchemy/alembic/env.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/db/sqlalchemy/alembic/script.py.mako +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/db/sqlalchemy/base.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/db/sqlalchemy/migrate.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/db/sqlalchemy/mixins/__init__.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/db/sqlalchemy/mixins/audit.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/db/sqlalchemy/mixins/soft_delete.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/db/sqlalchemy/mixins/timestamp.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/db/sqlalchemy/model_base.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/db/sqlalchemy/query_parser.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/db/sqlalchemy/session.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/health/__init__.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/health/checker.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/health/checkers.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/logger/__init__.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/logger/filters.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/logger/handlers.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/logger/record_factory.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/middlewares/__init__.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/middlewares/base.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/middlewares/request_id.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/py.typed +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/schemas/base.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment/schemas/pagination.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment.egg-info/dependency_links.txt +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment.egg-info/entry_points.txt +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment.egg-info/requires.txt +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/src/fastapi_augment.egg-info/top_level.txt +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_config.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_constants.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_database_settings.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_db_engine.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_db_session_models.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_exceptions.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_health.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_log.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_middlewares.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_migrate.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_model_base.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_openapi.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_paths.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_query_parser.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_schemas.py +0 -0
- {fastapi_augment-0.1.5 → fastapi_augment-0.1.6}/tests/test_settings.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-augment
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: FastAPI 通用代码工具包,跨项目复用
|
|
5
5
|
Author-email: zarkhan <hanguangzheng@qq.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -54,7 +54,7 @@ Requires-Dist: fastapi-augment[config,orjson,sqlalchemy,uvicorn]; extra == "stan
|
|
|
54
54
|
- **健康检查** — 可扩展的检查器模式,内置应用状态与数据库连通性检查,一行开关
|
|
55
55
|
- **配置管理** — 基于 pydantic-settings,支持 `.env` 文件、环境变量前缀、嵌套配置
|
|
56
56
|
- **数据库迁移 CLI** — 一行命令生成/执行迁移,自动发现用户模型
|
|
57
|
-
- **应用发现** — 自动发现子包 `__all__` 导出的 FastAPI
|
|
57
|
+
- **应用发现** — 自动发现子包 `__all__` 导出的 ASGI 应用(不限于 FastAPI),支持排除与导入串校验
|
|
58
58
|
|
|
59
59
|
## 安装
|
|
60
60
|
|
|
@@ -121,6 +121,19 @@ app = create_app(
|
|
|
121
121
|
)
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
+
### 完整可跑示例 — `examples/quickstart`
|
|
125
|
+
|
|
126
|
+
上面的最小示例浓缩了核心 API;想看到**完整工程形态**(三段式配置、模块级日志、组合根装配、生命周期建表、软删除与聚合、统一响应、测试),直接运行示例工程:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
cd examples/quickstart
|
|
130
|
+
uv sync --all-groups # 安装依赖(需 uv,Python >= 3.11)
|
|
131
|
+
uv run python src/main.py # 启动:http://127.0.0.1:8000/docs
|
|
132
|
+
uv run --group dev pytest -q # 跑测试(临时 SQLite,不落盘)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
示例工程沉淀自真实业务项目(browser-proxy)的工程模式,与库文档各章节一一对应(配置组合示例 ↔ `src/config/`,数据库层 ↔ `src/core/database.py`,泛型仓储 ↔ `apps/api/router.py`),详见 `examples/quickstart/README.md`。
|
|
136
|
+
|
|
124
137
|
## 核心模块
|
|
125
138
|
|
|
126
139
|
### 应用工厂 — `create_app()`
|
|
@@ -304,6 +317,37 @@ async with sessions.transaction() as session:
|
|
|
304
317
|
count = await user_repo.delete_where(session, is_active=False)
|
|
305
318
|
```
|
|
306
319
|
|
|
320
|
+
**软删除感知(模型混入 `SoftDeleteMixin` 时自动生效,非软删模型行为不变):**
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
# 查询默认排除已软删行;include_deleted=True 可放开
|
|
324
|
+
user = await user_repo.get(session, id_='01HXK...') # 已软删 → None
|
|
325
|
+
users = await user_repo.list(session, include_deleted=True) # 包含已软删行
|
|
326
|
+
total = await user_repo.count(session, include_deleted=True)
|
|
327
|
+
|
|
328
|
+
# 删除自动转软删(写 is_deleted=True + deleted_at);非软删模型仍为物理删除
|
|
329
|
+
await user_repo.delete(session, user)
|
|
330
|
+
deleted = await user_repo.delete_by_id(session, id_='01HXK...')
|
|
331
|
+
count = await user_repo.delete_where(session, is_active=False)
|
|
332
|
+
|
|
333
|
+
# 需要真正物理删除时显式调用 hard_delete_*
|
|
334
|
+
await user_repo.hard_delete_by_id(session, id_='01HXK...')
|
|
335
|
+
count = await user_repo.hard_delete_where(session, is_active=False)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**聚合与批量更新:**
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
# 数值列聚合(默认排除已软删行,支持过滤条件;无匹配行返回 None)
|
|
342
|
+
total_amount = await user_repo.sum(session, 'amount', role='admin')
|
|
343
|
+
avg_amount = await user_repo.avg(session, 'amount')
|
|
344
|
+
min_amount = await user_repo.min(session, 'amount')
|
|
345
|
+
max_amount = await user_repo.max(session, 'amount')
|
|
346
|
+
|
|
347
|
+
# 批量更新(单条 UPDATE,返回受影响行数;默认跳过已软删行)
|
|
348
|
+
affected = await user_repo.update_where(session, {'role': 'admin'}, name='alice')
|
|
349
|
+
```
|
|
350
|
+
|
|
307
351
|
**过滤语法:**
|
|
308
352
|
|
|
309
353
|
```python
|
|
@@ -718,6 +762,118 @@ cfg = Settings.from_toml('config.toml')
|
|
|
718
762
|
支持 `SettingsConfigDict` 的所有参数(`env_prefix`、`secrets_dir`、`yaml_file` 等),
|
|
719
763
|
与模型字段值自动区分,无需关心分类。
|
|
720
764
|
|
|
765
|
+
#### 项目配置组合示例
|
|
766
|
+
|
|
767
|
+
实际项目中通常组合多个配置类——全局(日志)、项目信息、Uvicorn 运行参数。以下为推荐模式:
|
|
768
|
+
只定义字段与加载方式,具体值由 `.env` 与环境变量注入;库仅提供 `AugmentBaseSettings` 基类。
|
|
769
|
+
|
|
770
|
+
```python
|
|
771
|
+
# src/config/settings.py —— 全局配置(日志 + 根目录)
|
|
772
|
+
from pathlib import Path
|
|
773
|
+
|
|
774
|
+
from fastapi_augment.common.utils import find_project_root, get_root_dir
|
|
775
|
+
from fastapi_augment.config import AugmentBaseSettings
|
|
776
|
+
|
|
777
|
+
__VERSION__ = '0.1.0'
|
|
778
|
+
_ENV_PREFIX = 'MY_SERVICE'
|
|
779
|
+
|
|
780
|
+
try:
|
|
781
|
+
_root_dir = find_project_root()
|
|
782
|
+
except Exception:
|
|
783
|
+
_root_dir = get_root_dir(__file__, 2)
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
class Settings(AugmentBaseSettings):
|
|
787
|
+
"""项目全局配置,字段通过 MY_SERVICE_ 前缀环境变量或 .env 覆盖"""
|
|
788
|
+
|
|
789
|
+
# 日志配置(对应 fastapi_augment.logger.setup_logger 参数)
|
|
790
|
+
logs_dir: str | Path | None = _root_dir / 'logs'
|
|
791
|
+
logs_level: str | None = 'INFO'
|
|
792
|
+
logs_filename: str = 'app.log'
|
|
793
|
+
logs_rotation: str = 'hour'
|
|
794
|
+
logs_backup_count: int = 30
|
|
795
|
+
logs_encoding: str = 'utf-8'
|
|
796
|
+
logs_enable_console: bool = True
|
|
797
|
+
|
|
798
|
+
@property
|
|
799
|
+
def root_dir(self) -> Path:
|
|
800
|
+
return _root_dir
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
settings = Settings.from_env(
|
|
804
|
+
env_file=_root_dir / '.env',
|
|
805
|
+
env_prefix=f'{_ENV_PREFIX}_',
|
|
806
|
+
env_nested_delimiter='__',
|
|
807
|
+
env_parse_none_str='null',
|
|
808
|
+
)
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
```python
|
|
812
|
+
# src/config/project_settings.py —— 项目元信息(debug / title / version)
|
|
813
|
+
from fastapi_augment.config import AugmentBaseSettings
|
|
814
|
+
|
|
815
|
+
from .settings import __VERSION__, _root_dir, _ENV_PREFIX
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
class ProjectSettings(AugmentBaseSettings):
|
|
819
|
+
"""项目基础配置,字段通过 MY_SERVICE_PROJECT_ 前缀覆盖"""
|
|
820
|
+
|
|
821
|
+
debug: bool = False
|
|
822
|
+
title: str = 'my-service'
|
|
823
|
+
summary: str = ''
|
|
824
|
+
|
|
825
|
+
@property
|
|
826
|
+
def version(self) -> str:
|
|
827
|
+
return __VERSION__
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
project_settings = ProjectSettings.from_env(
|
|
831
|
+
env_file=_root_dir / '.env',
|
|
832
|
+
env_prefix=f'{_ENV_PREFIX}_PROJECT_',
|
|
833
|
+
env_nested_delimiter='__',
|
|
834
|
+
env_parse_none_str='null',
|
|
835
|
+
)
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
```python
|
|
839
|
+
# src/config/uvicorn_settings.py —— Uvicorn 运行参数(不使用 uvicorn 时无需定义)
|
|
840
|
+
from fastapi_augment.config import AugmentBaseSettings
|
|
841
|
+
|
|
842
|
+
from .project_settings import project_settings
|
|
843
|
+
from .settings import _root_dir, _ENV_PREFIX
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
class UvicornSettings(AugmentBaseSettings):
|
|
847
|
+
"""Uvicorn 运行配置,字段通过 MY_SERVICE_UVICORN_ 前缀覆盖"""
|
|
848
|
+
|
|
849
|
+
host: str = '0.0.0.0'
|
|
850
|
+
port: int = 8000
|
|
851
|
+
workers: int = 2
|
|
852
|
+
access_log: bool = True
|
|
853
|
+
root_path: str = ''
|
|
854
|
+
asgi_app_ref: str = 'apps.main:app' # 启动入口,可用环境变量覆盖
|
|
855
|
+
|
|
856
|
+
@property
|
|
857
|
+
def reload(self) -> bool:
|
|
858
|
+
"""是否热重载,联动项目 debug 开关"""
|
|
859
|
+
return project_settings.debug
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
uvicorn_settings = UvicornSettings.from_env(
|
|
863
|
+
env_file=_root_dir / '.env',
|
|
864
|
+
env_prefix=f'{_ENV_PREFIX}_UVICORN_',
|
|
865
|
+
env_nested_delimiter='__',
|
|
866
|
+
env_parse_none_str='null',
|
|
867
|
+
)
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
**要点:**
|
|
871
|
+
|
|
872
|
+
- 配置类、环境变量前缀、默认值均为**使用方策略**,留在项目内,不进库
|
|
873
|
+
- 各配置类前缀独立(`MY_SERVICE_` / `MY_SERVICE_PROJECT_` / `MY_SERVICE_UVICORN_`),互不干扰
|
|
874
|
+
- 不使用 uvicorn 时无需定义 `UvicornSettings`,库不强制
|
|
875
|
+
- 环境变量优先级高于 `.env` 文件
|
|
876
|
+
|
|
721
877
|
### 中间件 — `middlewares`
|
|
722
878
|
|
|
723
879
|
#### `RequestIdMiddleware`
|
|
@@ -732,26 +888,29 @@ request_id = get_request_id()
|
|
|
732
888
|
|
|
733
889
|
### 应用发现 — `common.app_discovery`
|
|
734
890
|
|
|
735
|
-
递归发现 `apps` 包下所有子包通过 `__all__` 导出的 FastAPI
|
|
891
|
+
递归发现 `apps` 包下所有子包通过 `__all__` 导出的 ASGI 应用(不限于 FastAPI),用于多应用聚合部署与启动前校验。
|
|
736
892
|
|
|
737
|
-
**约定:** 每个业务子包(如 `apps.platform`)在 `__init__.py` 的 `__all__` 中导出自己创建的
|
|
893
|
+
**约定:** 每个业务子包(如 `apps.platform`)在 `__init__.py` 的 `__all__` 中导出自己创建的 ASGI 应用(如 `platform_app`,不限于 FastAPI 实例);只有出现在 `__all__` 且通过 `is_asgi_app` 判定的对象才被识别为"应用"(判定规则见下)。
|
|
738
894
|
|
|
739
895
|
```python
|
|
740
|
-
from fastapi_augment.common import
|
|
896
|
+
from fastapi_augment.common import ASGIAppSpec, discover_asgi_apps, validate_asgi_import
|
|
741
897
|
|
|
742
898
|
# 发现全部应用(排除主应用,获取主应用之外的其它应用)
|
|
743
|
-
apps: list[
|
|
899
|
+
apps: list[ASGIAppSpec] = discover_asgi_apps(root='apps', exclude='apps.platform:platform_app')
|
|
744
900
|
|
|
745
901
|
# 每个应用可直接启动(import_string 即 uvicorn 导入串)
|
|
746
902
|
for spec in apps:
|
|
747
903
|
uvicorn.run(spec.import_string) # 'apps.platform:platform_app'
|
|
748
904
|
|
|
749
|
-
#
|
|
905
|
+
# 启动前校验主应用导入串是否真实存在且为可调用的 ASGI 应用(uvicorn 可直接启动)
|
|
750
906
|
validate_asgi_import('apps.platform:platform_app')
|
|
751
907
|
```
|
|
752
908
|
|
|
753
909
|
- **`exclude` 支持三种标识** — 模块名(`apps.platform`)、导出名(`platform_app`)或 `module:name` 导入串(`apps.platform:platform_app`)
|
|
754
910
|
- 结果按模块名排序,顺序稳定
|
|
911
|
+
- **`validate_asgi_import` 不限于 FastAPI** — 只要是可调用的 ASGI 应用(Starlette / Flask 等或自定义 ASGI 函数,判定规则见下)均通过
|
|
912
|
+
|
|
913
|
+
**ASGI 类型与校验** — `common.asgi_types` 提供遵循 ASGI 规范的类型定义(`ASGIApplication` / `ASGI2Application` / `ASGI3Application` / `Scope` 等)与运行时近似判定 `is_asgi_app()`(返回 `TypeGuard[ASGIApplication]`,`if is_asgi_app(x)` 后类型检查器自动将 `x` 收窄为 ASGI 应用):callable 为硬门槛(与 uvicorn `Config.load` 一致),签名可解析时进一步检查 ASGI2(类,scope 单参)或 ASGI3(`scope, receive, send` 三参)结构,`*args` 包装器放行。签名判定为近似,权威判定以 uvicorn 实际启动为准。
|
|
755
914
|
|
|
756
915
|
## 开发与发布
|
|
757
916
|
|
|
@@ -767,42 +926,60 @@ validate_asgi_import('apps.platform:platform_app')
|
|
|
767
926
|
# lint(读取 pyproject.toml 的 [tool.ruff] 配置)
|
|
768
927
|
uvx ruff check src tests
|
|
769
928
|
|
|
770
|
-
#
|
|
929
|
+
# 类型检查(读取 pyproject.toml 的 [tool.mypy] 配置)
|
|
930
|
+
uv run mypy src tests
|
|
931
|
+
|
|
932
|
+
# 测试(覆盖率门禁 --cov-fail-under=90 在 pyproject.toml 的 pytest addopts 中配置)
|
|
771
933
|
uv run --frozen pytest -q
|
|
772
934
|
```
|
|
773
935
|
|
|
774
|
-
CI
|
|
936
|
+
CI(`.github/workflows/lint.yml`,即 **CI** workflow)已配置自动检查,PR / develop push 时运行:
|
|
937
|
+
|
|
938
|
+
- **Ruff** — `uvx ruff check src tests`
|
|
939
|
+
- **Mypy** — `uv run mypy src tests`(Python 3.11)
|
|
940
|
+
- **Pytest** — Python 3.11 / 3.12 / 3.13 矩阵并行,覆盖率不低于 90%(`--cov-fail-under=90`)
|
|
941
|
+
|
|
942
|
+
三个 Job 全部通过才允许合并到 `master`(分支保护所需状态检查为 `Ruff`、`Mypy` 与 `Pytest (Python x.y)`)。PR 阶段只做检查,不打包、不打 tag、不发布。
|
|
775
943
|
|
|
776
944
|
### 发布 Release
|
|
777
945
|
|
|
778
|
-
|
|
946
|
+
单个 workflow(`.github/workflows/release.yml`)串行完成 GitHub Release 与 PyPI 发布,两个 Job:
|
|
779
947
|
|
|
780
|
-
|
|
948
|
+
1. **release Job** — 版本守卫 → 代码门禁(pytest + ruff + mypy)→ 源码打包(zip / tar.gz)→ 上传 GitHub Release(tag 由 `gh release create` 自动创建,成功时 tag 必然存在)
|
|
949
|
+
2. **publish Job**(`needs: release`)— **仅当 GitHub Release 成功后才执行**:查 PyPI 是否已有该版本(无则上传)→ build → 版本一致性断言 → `twine check` 校验打包 → 上传 PyPI
|
|
950
|
+
|
|
951
|
+
触发方式:
|
|
952
|
+
|
|
953
|
+
- **推送到 `master`(自动)** — 版本守卫判定"需要发布"时自动执行;不需要时跳过
|
|
781
954
|
- **master 分支手动触发**(workflow_dispatch)— `release` 发布新版本 / `rebuild` 重新打包指定版本
|
|
782
955
|
|
|
783
|
-
|
|
956
|
+
版本守卫规则(release 与 publish 各自独立判断):
|
|
784
957
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
958
|
+
| 状态 | push 合并 | 手动触发 |
|
|
959
|
+
| ---- | --------- | -------- |
|
|
960
|
+
| tag 与 Release 均在且与代码版本一致 | 跳过 | 报错引导先更新 `VERSION` |
|
|
961
|
+
| 缺 tag 或缺 Release | 补齐发布 | 补齐发布 |
|
|
962
|
+
| rebuild 模式 | — | 直接放行 |
|
|
790
963
|
|
|
791
|
-
|
|
964
|
+
publish Job 以 PyPI 线上版本为准(查询 `pypi.org/pypi/<project>/<version>/json`,404 才上传),PyPI 版本不可覆盖,天然不重复。
|
|
792
965
|
|
|
793
|
-
|
|
966
|
+
发布类操作仅 master 分支可执行,任何一步失败都不会产生半成品:
|
|
794
967
|
|
|
795
|
-
|
|
968
|
+
1. **代码门禁** — 判定需要发布时先跑 pytest + ruff + mypy,任一失败即停止(防止绕过分支保护发布未验证代码)
|
|
969
|
+
2. **版本一致性校验** — 目标版本与代码版本不一致时终止并引导(master 受保护,需先在 `develop` 更新 `VERSION` 与 `factory.py` 版本,PR 合并后重试)
|
|
970
|
+
3. **打包** — 源码打包为 `fastapi_augment-<版本>.zip` / `.tar.gz`(排除 `.venv`、缓存、构建产物)
|
|
971
|
+
4. **Release** — 打包成功后才上传 GitHub Release;失败不留任何 tag/Release,重试不会跳版本
|
|
972
|
+
5. **PyPI** — Release 成功后才上传,上传前经 `twine check` 校验 sdist/wheel 元数据,使用 `PYPI_API_TOKEN`(GitHub Secrets)认证
|
|
796
973
|
|
|
797
|
-
|
|
798
|
-
- **rebuild** — 指定已有 tag(如 `v1.2.3`)重新打包上传,不修改代码版本
|
|
974
|
+
建议发布前先在 `develop` 分支完成版本号更新并 PR 合并到 `master`——合并触发自动发布,发布流程将直接复用代码版本。
|
|
799
975
|
|
|
800
976
|
## 项目结构
|
|
801
977
|
|
|
802
978
|
```
|
|
803
979
|
fastapi_augment/
|
|
804
980
|
├── common/
|
|
805
|
-
│ ├── app_discovery.py #
|
|
981
|
+
│ ├── app_discovery.py # ASGI 应用发现(__all__ 约定)
|
|
982
|
+
│ ├── asgi_types.py # ASGI 类型定义与 is_asgi_app 运行时判定(TypeGuard)
|
|
806
983
|
│ ├── constants.py # 全局常量与默认错误文案
|
|
807
984
|
│ ├── exceptions.py # 4xx HTTP 异常体系
|
|
808
985
|
│ ├── exception_handlers.py # 全局异常处理器
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
- **健康检查** — 可扩展的检查器模式,内置应用状态与数据库连通性检查,一行开关
|
|
18
18
|
- **配置管理** — 基于 pydantic-settings,支持 `.env` 文件、环境变量前缀、嵌套配置
|
|
19
19
|
- **数据库迁移 CLI** — 一行命令生成/执行迁移,自动发现用户模型
|
|
20
|
-
- **应用发现** — 自动发现子包 `__all__` 导出的 FastAPI
|
|
20
|
+
- **应用发现** — 自动发现子包 `__all__` 导出的 ASGI 应用(不限于 FastAPI),支持排除与导入串校验
|
|
21
21
|
|
|
22
22
|
## 安装
|
|
23
23
|
|
|
@@ -84,6 +84,19 @@ app = create_app(
|
|
|
84
84
|
)
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
### 完整可跑示例 — `examples/quickstart`
|
|
88
|
+
|
|
89
|
+
上面的最小示例浓缩了核心 API;想看到**完整工程形态**(三段式配置、模块级日志、组合根装配、生命周期建表、软删除与聚合、统一响应、测试),直接运行示例工程:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
cd examples/quickstart
|
|
93
|
+
uv sync --all-groups # 安装依赖(需 uv,Python >= 3.11)
|
|
94
|
+
uv run python src/main.py # 启动:http://127.0.0.1:8000/docs
|
|
95
|
+
uv run --group dev pytest -q # 跑测试(临时 SQLite,不落盘)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
示例工程沉淀自真实业务项目(browser-proxy)的工程模式,与库文档各章节一一对应(配置组合示例 ↔ `src/config/`,数据库层 ↔ `src/core/database.py`,泛型仓储 ↔ `apps/api/router.py`),详见 `examples/quickstart/README.md`。
|
|
99
|
+
|
|
87
100
|
## 核心模块
|
|
88
101
|
|
|
89
102
|
### 应用工厂 — `create_app()`
|
|
@@ -267,6 +280,37 @@ async with sessions.transaction() as session:
|
|
|
267
280
|
count = await user_repo.delete_where(session, is_active=False)
|
|
268
281
|
```
|
|
269
282
|
|
|
283
|
+
**软删除感知(模型混入 `SoftDeleteMixin` 时自动生效,非软删模型行为不变):**
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
# 查询默认排除已软删行;include_deleted=True 可放开
|
|
287
|
+
user = await user_repo.get(session, id_='01HXK...') # 已软删 → None
|
|
288
|
+
users = await user_repo.list(session, include_deleted=True) # 包含已软删行
|
|
289
|
+
total = await user_repo.count(session, include_deleted=True)
|
|
290
|
+
|
|
291
|
+
# 删除自动转软删(写 is_deleted=True + deleted_at);非软删模型仍为物理删除
|
|
292
|
+
await user_repo.delete(session, user)
|
|
293
|
+
deleted = await user_repo.delete_by_id(session, id_='01HXK...')
|
|
294
|
+
count = await user_repo.delete_where(session, is_active=False)
|
|
295
|
+
|
|
296
|
+
# 需要真正物理删除时显式调用 hard_delete_*
|
|
297
|
+
await user_repo.hard_delete_by_id(session, id_='01HXK...')
|
|
298
|
+
count = await user_repo.hard_delete_where(session, is_active=False)
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**聚合与批量更新:**
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
# 数值列聚合(默认排除已软删行,支持过滤条件;无匹配行返回 None)
|
|
305
|
+
total_amount = await user_repo.sum(session, 'amount', role='admin')
|
|
306
|
+
avg_amount = await user_repo.avg(session, 'amount')
|
|
307
|
+
min_amount = await user_repo.min(session, 'amount')
|
|
308
|
+
max_amount = await user_repo.max(session, 'amount')
|
|
309
|
+
|
|
310
|
+
# 批量更新(单条 UPDATE,返回受影响行数;默认跳过已软删行)
|
|
311
|
+
affected = await user_repo.update_where(session, {'role': 'admin'}, name='alice')
|
|
312
|
+
```
|
|
313
|
+
|
|
270
314
|
**过滤语法:**
|
|
271
315
|
|
|
272
316
|
```python
|
|
@@ -681,6 +725,118 @@ cfg = Settings.from_toml('config.toml')
|
|
|
681
725
|
支持 `SettingsConfigDict` 的所有参数(`env_prefix`、`secrets_dir`、`yaml_file` 等),
|
|
682
726
|
与模型字段值自动区分,无需关心分类。
|
|
683
727
|
|
|
728
|
+
#### 项目配置组合示例
|
|
729
|
+
|
|
730
|
+
实际项目中通常组合多个配置类——全局(日志)、项目信息、Uvicorn 运行参数。以下为推荐模式:
|
|
731
|
+
只定义字段与加载方式,具体值由 `.env` 与环境变量注入;库仅提供 `AugmentBaseSettings` 基类。
|
|
732
|
+
|
|
733
|
+
```python
|
|
734
|
+
# src/config/settings.py —— 全局配置(日志 + 根目录)
|
|
735
|
+
from pathlib import Path
|
|
736
|
+
|
|
737
|
+
from fastapi_augment.common.utils import find_project_root, get_root_dir
|
|
738
|
+
from fastapi_augment.config import AugmentBaseSettings
|
|
739
|
+
|
|
740
|
+
__VERSION__ = '0.1.0'
|
|
741
|
+
_ENV_PREFIX = 'MY_SERVICE'
|
|
742
|
+
|
|
743
|
+
try:
|
|
744
|
+
_root_dir = find_project_root()
|
|
745
|
+
except Exception:
|
|
746
|
+
_root_dir = get_root_dir(__file__, 2)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
class Settings(AugmentBaseSettings):
|
|
750
|
+
"""项目全局配置,字段通过 MY_SERVICE_ 前缀环境变量或 .env 覆盖"""
|
|
751
|
+
|
|
752
|
+
# 日志配置(对应 fastapi_augment.logger.setup_logger 参数)
|
|
753
|
+
logs_dir: str | Path | None = _root_dir / 'logs'
|
|
754
|
+
logs_level: str | None = 'INFO'
|
|
755
|
+
logs_filename: str = 'app.log'
|
|
756
|
+
logs_rotation: str = 'hour'
|
|
757
|
+
logs_backup_count: int = 30
|
|
758
|
+
logs_encoding: str = 'utf-8'
|
|
759
|
+
logs_enable_console: bool = True
|
|
760
|
+
|
|
761
|
+
@property
|
|
762
|
+
def root_dir(self) -> Path:
|
|
763
|
+
return _root_dir
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
settings = Settings.from_env(
|
|
767
|
+
env_file=_root_dir / '.env',
|
|
768
|
+
env_prefix=f'{_ENV_PREFIX}_',
|
|
769
|
+
env_nested_delimiter='__',
|
|
770
|
+
env_parse_none_str='null',
|
|
771
|
+
)
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
```python
|
|
775
|
+
# src/config/project_settings.py —— 项目元信息(debug / title / version)
|
|
776
|
+
from fastapi_augment.config import AugmentBaseSettings
|
|
777
|
+
|
|
778
|
+
from .settings import __VERSION__, _root_dir, _ENV_PREFIX
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
class ProjectSettings(AugmentBaseSettings):
|
|
782
|
+
"""项目基础配置,字段通过 MY_SERVICE_PROJECT_ 前缀覆盖"""
|
|
783
|
+
|
|
784
|
+
debug: bool = False
|
|
785
|
+
title: str = 'my-service'
|
|
786
|
+
summary: str = ''
|
|
787
|
+
|
|
788
|
+
@property
|
|
789
|
+
def version(self) -> str:
|
|
790
|
+
return __VERSION__
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
project_settings = ProjectSettings.from_env(
|
|
794
|
+
env_file=_root_dir / '.env',
|
|
795
|
+
env_prefix=f'{_ENV_PREFIX}_PROJECT_',
|
|
796
|
+
env_nested_delimiter='__',
|
|
797
|
+
env_parse_none_str='null',
|
|
798
|
+
)
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
```python
|
|
802
|
+
# src/config/uvicorn_settings.py —— Uvicorn 运行参数(不使用 uvicorn 时无需定义)
|
|
803
|
+
from fastapi_augment.config import AugmentBaseSettings
|
|
804
|
+
|
|
805
|
+
from .project_settings import project_settings
|
|
806
|
+
from .settings import _root_dir, _ENV_PREFIX
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
class UvicornSettings(AugmentBaseSettings):
|
|
810
|
+
"""Uvicorn 运行配置,字段通过 MY_SERVICE_UVICORN_ 前缀覆盖"""
|
|
811
|
+
|
|
812
|
+
host: str = '0.0.0.0'
|
|
813
|
+
port: int = 8000
|
|
814
|
+
workers: int = 2
|
|
815
|
+
access_log: bool = True
|
|
816
|
+
root_path: str = ''
|
|
817
|
+
asgi_app_ref: str = 'apps.main:app' # 启动入口,可用环境变量覆盖
|
|
818
|
+
|
|
819
|
+
@property
|
|
820
|
+
def reload(self) -> bool:
|
|
821
|
+
"""是否热重载,联动项目 debug 开关"""
|
|
822
|
+
return project_settings.debug
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
uvicorn_settings = UvicornSettings.from_env(
|
|
826
|
+
env_file=_root_dir / '.env',
|
|
827
|
+
env_prefix=f'{_ENV_PREFIX}_UVICORN_',
|
|
828
|
+
env_nested_delimiter='__',
|
|
829
|
+
env_parse_none_str='null',
|
|
830
|
+
)
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
**要点:**
|
|
834
|
+
|
|
835
|
+
- 配置类、环境变量前缀、默认值均为**使用方策略**,留在项目内,不进库
|
|
836
|
+
- 各配置类前缀独立(`MY_SERVICE_` / `MY_SERVICE_PROJECT_` / `MY_SERVICE_UVICORN_`),互不干扰
|
|
837
|
+
- 不使用 uvicorn 时无需定义 `UvicornSettings`,库不强制
|
|
838
|
+
- 环境变量优先级高于 `.env` 文件
|
|
839
|
+
|
|
684
840
|
### 中间件 — `middlewares`
|
|
685
841
|
|
|
686
842
|
#### `RequestIdMiddleware`
|
|
@@ -695,26 +851,29 @@ request_id = get_request_id()
|
|
|
695
851
|
|
|
696
852
|
### 应用发现 — `common.app_discovery`
|
|
697
853
|
|
|
698
|
-
递归发现 `apps` 包下所有子包通过 `__all__` 导出的 FastAPI
|
|
854
|
+
递归发现 `apps` 包下所有子包通过 `__all__` 导出的 ASGI 应用(不限于 FastAPI),用于多应用聚合部署与启动前校验。
|
|
699
855
|
|
|
700
|
-
**约定:** 每个业务子包(如 `apps.platform`)在 `__init__.py` 的 `__all__` 中导出自己创建的
|
|
856
|
+
**约定:** 每个业务子包(如 `apps.platform`)在 `__init__.py` 的 `__all__` 中导出自己创建的 ASGI 应用(如 `platform_app`,不限于 FastAPI 实例);只有出现在 `__all__` 且通过 `is_asgi_app` 判定的对象才被识别为"应用"(判定规则见下)。
|
|
701
857
|
|
|
702
858
|
```python
|
|
703
|
-
from fastapi_augment.common import
|
|
859
|
+
from fastapi_augment.common import ASGIAppSpec, discover_asgi_apps, validate_asgi_import
|
|
704
860
|
|
|
705
861
|
# 发现全部应用(排除主应用,获取主应用之外的其它应用)
|
|
706
|
-
apps: list[
|
|
862
|
+
apps: list[ASGIAppSpec] = discover_asgi_apps(root='apps', exclude='apps.platform:platform_app')
|
|
707
863
|
|
|
708
864
|
# 每个应用可直接启动(import_string 即 uvicorn 导入串)
|
|
709
865
|
for spec in apps:
|
|
710
866
|
uvicorn.run(spec.import_string) # 'apps.platform:platform_app'
|
|
711
867
|
|
|
712
|
-
#
|
|
868
|
+
# 启动前校验主应用导入串是否真实存在且为可调用的 ASGI 应用(uvicorn 可直接启动)
|
|
713
869
|
validate_asgi_import('apps.platform:platform_app')
|
|
714
870
|
```
|
|
715
871
|
|
|
716
872
|
- **`exclude` 支持三种标识** — 模块名(`apps.platform`)、导出名(`platform_app`)或 `module:name` 导入串(`apps.platform:platform_app`)
|
|
717
873
|
- 结果按模块名排序,顺序稳定
|
|
874
|
+
- **`validate_asgi_import` 不限于 FastAPI** — 只要是可调用的 ASGI 应用(Starlette / Flask 等或自定义 ASGI 函数,判定规则见下)均通过
|
|
875
|
+
|
|
876
|
+
**ASGI 类型与校验** — `common.asgi_types` 提供遵循 ASGI 规范的类型定义(`ASGIApplication` / `ASGI2Application` / `ASGI3Application` / `Scope` 等)与运行时近似判定 `is_asgi_app()`(返回 `TypeGuard[ASGIApplication]`,`if is_asgi_app(x)` 后类型检查器自动将 `x` 收窄为 ASGI 应用):callable 为硬门槛(与 uvicorn `Config.load` 一致),签名可解析时进一步检查 ASGI2(类,scope 单参)或 ASGI3(`scope, receive, send` 三参)结构,`*args` 包装器放行。签名判定为近似,权威判定以 uvicorn 实际启动为准。
|
|
718
877
|
|
|
719
878
|
## 开发与发布
|
|
720
879
|
|
|
@@ -730,42 +889,60 @@ validate_asgi_import('apps.platform:platform_app')
|
|
|
730
889
|
# lint(读取 pyproject.toml 的 [tool.ruff] 配置)
|
|
731
890
|
uvx ruff check src tests
|
|
732
891
|
|
|
733
|
-
#
|
|
892
|
+
# 类型检查(读取 pyproject.toml 的 [tool.mypy] 配置)
|
|
893
|
+
uv run mypy src tests
|
|
894
|
+
|
|
895
|
+
# 测试(覆盖率门禁 --cov-fail-under=90 在 pyproject.toml 的 pytest addopts 中配置)
|
|
734
896
|
uv run --frozen pytest -q
|
|
735
897
|
```
|
|
736
898
|
|
|
737
|
-
CI
|
|
899
|
+
CI(`.github/workflows/lint.yml`,即 **CI** workflow)已配置自动检查,PR / develop push 时运行:
|
|
900
|
+
|
|
901
|
+
- **Ruff** — `uvx ruff check src tests`
|
|
902
|
+
- **Mypy** — `uv run mypy src tests`(Python 3.11)
|
|
903
|
+
- **Pytest** — Python 3.11 / 3.12 / 3.13 矩阵并行,覆盖率不低于 90%(`--cov-fail-under=90`)
|
|
904
|
+
|
|
905
|
+
三个 Job 全部通过才允许合并到 `master`(分支保护所需状态检查为 `Ruff`、`Mypy` 与 `Pytest (Python x.y)`)。PR 阶段只做检查,不打包、不打 tag、不发布。
|
|
738
906
|
|
|
739
907
|
### 发布 Release
|
|
740
908
|
|
|
741
|
-
|
|
909
|
+
单个 workflow(`.github/workflows/release.yml`)串行完成 GitHub Release 与 PyPI 发布,两个 Job:
|
|
742
910
|
|
|
743
|
-
|
|
911
|
+
1. **release Job** — 版本守卫 → 代码门禁(pytest + ruff + mypy)→ 源码打包(zip / tar.gz)→ 上传 GitHub Release(tag 由 `gh release create` 自动创建,成功时 tag 必然存在)
|
|
912
|
+
2. **publish Job**(`needs: release`)— **仅当 GitHub Release 成功后才执行**:查 PyPI 是否已有该版本(无则上传)→ build → 版本一致性断言 → `twine check` 校验打包 → 上传 PyPI
|
|
913
|
+
|
|
914
|
+
触发方式:
|
|
915
|
+
|
|
916
|
+
- **推送到 `master`(自动)** — 版本守卫判定"需要发布"时自动执行;不需要时跳过
|
|
744
917
|
- **master 分支手动触发**(workflow_dispatch)— `release` 发布新版本 / `rebuild` 重新打包指定版本
|
|
745
918
|
|
|
746
|
-
|
|
919
|
+
版本守卫规则(release 与 publish 各自独立判断):
|
|
747
920
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
921
|
+
| 状态 | push 合并 | 手动触发 |
|
|
922
|
+
| ---- | --------- | -------- |
|
|
923
|
+
| tag 与 Release 均在且与代码版本一致 | 跳过 | 报错引导先更新 `VERSION` |
|
|
924
|
+
| 缺 tag 或缺 Release | 补齐发布 | 补齐发布 |
|
|
925
|
+
| rebuild 模式 | — | 直接放行 |
|
|
753
926
|
|
|
754
|
-
|
|
927
|
+
publish Job 以 PyPI 线上版本为准(查询 `pypi.org/pypi/<project>/<version>/json`,404 才上传),PyPI 版本不可覆盖,天然不重复。
|
|
755
928
|
|
|
756
|
-
|
|
929
|
+
发布类操作仅 master 分支可执行,任何一步失败都不会产生半成品:
|
|
757
930
|
|
|
758
|
-
|
|
931
|
+
1. **代码门禁** — 判定需要发布时先跑 pytest + ruff + mypy,任一失败即停止(防止绕过分支保护发布未验证代码)
|
|
932
|
+
2. **版本一致性校验** — 目标版本与代码版本不一致时终止并引导(master 受保护,需先在 `develop` 更新 `VERSION` 与 `factory.py` 版本,PR 合并后重试)
|
|
933
|
+
3. **打包** — 源码打包为 `fastapi_augment-<版本>.zip` / `.tar.gz`(排除 `.venv`、缓存、构建产物)
|
|
934
|
+
4. **Release** — 打包成功后才上传 GitHub Release;失败不留任何 tag/Release,重试不会跳版本
|
|
935
|
+
5. **PyPI** — Release 成功后才上传,上传前经 `twine check` 校验 sdist/wheel 元数据,使用 `PYPI_API_TOKEN`(GitHub Secrets)认证
|
|
759
936
|
|
|
760
|
-
|
|
761
|
-
- **rebuild** — 指定已有 tag(如 `v1.2.3`)重新打包上传,不修改代码版本
|
|
937
|
+
建议发布前先在 `develop` 分支完成版本号更新并 PR 合并到 `master`——合并触发自动发布,发布流程将直接复用代码版本。
|
|
762
938
|
|
|
763
939
|
## 项目结构
|
|
764
940
|
|
|
765
941
|
```
|
|
766
942
|
fastapi_augment/
|
|
767
943
|
├── common/
|
|
768
|
-
│ ├── app_discovery.py #
|
|
944
|
+
│ ├── app_discovery.py # ASGI 应用发现(__all__ 约定)
|
|
945
|
+
│ ├── asgi_types.py # ASGI 类型定义与 is_asgi_app 运行时判定(TypeGuard)
|
|
769
946
|
│ ├── constants.py # 全局常量与默认错误文案
|
|
770
947
|
│ ├── exceptions.py # 4xx HTTP 异常体系
|
|
771
948
|
│ ├── exception_handlers.py # 全局异常处理器
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.6
|
|
@@ -66,8 +66,10 @@ fastapi-augment-migrate = "fastapi_augment.db.sqlalchemy.migrate:cli_main"
|
|
|
66
66
|
dev = [
|
|
67
67
|
"aiosqlite>=0.21.0",
|
|
68
68
|
"httpx2>=2.12.0",
|
|
69
|
+
"mypy>=1.10",
|
|
69
70
|
"pytest>=8.0",
|
|
70
71
|
"pytest-asyncio>=0.25.0",
|
|
72
|
+
"pytest-cov>=5.0",
|
|
71
73
|
]
|
|
72
74
|
|
|
73
75
|
[tool.setuptools.packages.find]
|
|
@@ -88,6 +90,7 @@ version = { file = "VERSION" }
|
|
|
88
90
|
[tool.pytest.ini_options]
|
|
89
91
|
asyncio_mode = "auto"
|
|
90
92
|
testpaths = ["tests"]
|
|
93
|
+
addopts = "--cov=fastapi_augment --cov-report=term-missing --cov-fail-under=90"
|
|
91
94
|
|
|
92
95
|
[tool.ruff]
|
|
93
96
|
line-length = 120
|
|
@@ -113,3 +116,10 @@ ignore = [
|
|
|
113
116
|
|
|
114
117
|
[tool.ruff.lint.isort]
|
|
115
118
|
known-first-party = ["fastapi_augment"]
|
|
119
|
+
|
|
120
|
+
# ======== 类型检查(mypy) ========
|
|
121
|
+
[tool.mypy]
|
|
122
|
+
python_version = "3.11"
|
|
123
|
+
files = ["src", "tests"]
|
|
124
|
+
warn_unused_ignores = true
|
|
125
|
+
no_implicit_optional = true
|