socar-api 0.6.0__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.
Files changed (49) hide show
  1. socar_api-0.6.0/PKG-INFO +177 -0
  2. socar_api-0.6.0/README.md +151 -0
  3. socar_api-0.6.0/pyproject.toml +86 -0
  4. socar_api-0.6.0/setup.cfg +4 -0
  5. socar_api-0.6.0/src/api_frame/__init__.py +16 -0
  6. socar_api-0.6.0/src/api_frame/atomic.py +212 -0
  7. socar_api-0.6.0/src/api_frame/auth.py +243 -0
  8. socar_api-0.6.0/src/api_frame/cache.py +258 -0
  9. socar_api-0.6.0/src/api_frame/exception.py +192 -0
  10. socar_api-0.6.0/src/api_frame/field.py +68 -0
  11. socar_api-0.6.0/src/api_frame/filebase.py +377 -0
  12. socar_api-0.6.0/src/api_frame/filter.py +166 -0
  13. socar_api-0.6.0/src/api_frame/handlers.py +309 -0
  14. socar_api-0.6.0/src/api_frame/jsonapi.py +317 -0
  15. socar_api-0.6.0/src/api_frame/meta.py +22 -0
  16. socar_api-0.6.0/src/api_frame/query.py +660 -0
  17. socar_api-0.6.0/src/api_frame/resource.py +1250 -0
  18. socar_api-0.6.0/src/api_frame/responses.py +27 -0
  19. socar_api-0.6.0/src/api_frame/router.py +232 -0
  20. socar_api-0.6.0/src/api_frame/schema.py +686 -0
  21. socar_api-0.6.0/src/api_frame/scope.py +127 -0
  22. socar_api-0.6.0/src/api_frame/serializer.py +336 -0
  23. socar_api-0.6.0/src/api_frame/url_parse.py +128 -0
  24. socar_api-0.6.0/src/api_frame/util.py +253 -0
  25. socar_api-0.6.0/src/socar_api.egg-info/PKG-INFO +177 -0
  26. socar_api-0.6.0/src/socar_api.egg-info/SOURCES.txt +47 -0
  27. socar_api-0.6.0/src/socar_api.egg-info/dependency_links.txt +1 -0
  28. socar_api-0.6.0/src/socar_api.egg-info/requires.txt +18 -0
  29. socar_api-0.6.0/src/socar_api.egg-info/top_level.txt +1 -0
  30. socar_api-0.6.0/tests/test_atomic.py +166 -0
  31. socar_api-0.6.0/tests/test_auth.py +94 -0
  32. socar_api-0.6.0/tests/test_cache.py +277 -0
  33. socar_api-0.6.0/tests/test_exception.py +175 -0
  34. socar_api-0.6.0/tests/test_field.py +109 -0
  35. socar_api-0.6.0/tests/test_filebase.py +510 -0
  36. socar_api-0.6.0/tests/test_filter.py +186 -0
  37. socar_api-0.6.0/tests/test_inferinfo.py +24 -0
  38. socar_api-0.6.0/tests/test_integration.py +661 -0
  39. socar_api-0.6.0/tests/test_jsonapi.py +196 -0
  40. socar_api-0.6.0/tests/test_jsonapi_compliance.py +890 -0
  41. socar_api-0.6.0/tests/test_jsonapi_v11.py +215 -0
  42. socar_api-0.6.0/tests/test_meta.py +21 -0
  43. socar_api-0.6.0/tests/test_query.py +273 -0
  44. socar_api-0.6.0/tests/test_responses.py +37 -0
  45. socar_api-0.6.0/tests/test_schema.py +268 -0
  46. socar_api-0.6.0/tests/test_scope.py +195 -0
  47. socar_api-0.6.0/tests/test_serializer.py +353 -0
  48. socar_api-0.6.0/tests/test_url_parse.py +105 -0
  49. socar_api-0.6.0/tests/test_util.py +92 -0
@@ -0,0 +1,177 @@
1
+ Metadata-Version: 2.4
2
+ Name: socar-api
3
+ Version: 0.6.0
4
+ Summary: 基于 FastAPI + JSON:API 规范的 RESTful 接口框架
5
+ Author-email: "so.car" <socar@so.car>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: fastapi>=0.136.3
10
+ Requires-Dist: pydantic>=2.13.4
11
+ Requires-Dist: jquery-unparam>=2.0.0
12
+ Requires-Dist: pyyaml>=6.0.3
13
+ Requires-Dist: treelib>=1.8.0
14
+ Requires-Dist: bitarray>=3.8.1
15
+ Requires-Dist: python-multipart>=0.0.32
16
+ Requires-Dist: python-dateutil>=2.9.0.post0
17
+ Requires-Dist: typing-extensions>=4.15.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=9.0.3; extra == "dev"
20
+ Requires-Dist: pytest-asyncio>=1.4.0; extra == "dev"
21
+ Requires-Dist: httpx>=0.28.1; extra == "dev"
22
+ Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
23
+ Requires-Dist: ruff>=0.15.16; extra == "dev"
24
+ Requires-Dist: mypy>=2.1.0; extra == "dev"
25
+ Requires-Dist: pre-commit>=4.6.0; extra == "dev"
26
+
27
+ # api_frame
28
+
29
+ 基于 FastAPI + JSON:API 规范的 RESTful 接口框架。
30
+
31
+ [![version](https://img.shields.io/badge/version-0.6.0-blue.svg)](https://gitee.com/socar/api_frame)
32
+
33
+ ## 简介
34
+
35
+ api_frame 提供了从资源声明 → 路由生成 → 请求处理 → 响应序列化的完整链路,让开发者**专注业务逻辑**,无需重复实现接口规范。
36
+
37
+ 框架提供两种资源基类:
38
+ - **`BaseResource`** — 完整基类,支持 `connect_data()` session 管理和子资源嵌套
39
+ - **`Resource`**(推荐) — 简化基类,内置 JSON:API 序列化管线,可直接访问 `self.db`
40
+
41
+ ```python
42
+ from api_frame import Resource, SchemaBase, Field, Relationship
43
+
44
+ class ArticleModel(SchemaBase):
45
+ title: str = Field(None)
46
+ content: str = Field(None)
47
+ author_id: int = Field(None, isrel=True)
48
+
49
+ class Article(Resource):
50
+ model = ArticleModel
51
+
52
+ class Meta:
53
+ link = '/articles'
54
+ type_ = 'articles'
55
+
56
+ class RelResources:
57
+ author = Relationship(
58
+ rel_resource='Author',
59
+ mapping_field='author_id',
60
+ has_api=False,
61
+ )
62
+
63
+ async def get_many(self):
64
+ return self.db.query(ArticleModel).all()
65
+
66
+ async def post(self):
67
+ body = self.parse_body()
68
+ obj = ArticleModel(**body.model_dump())
69
+ self.db.add(obj)
70
+ self.db.commit()
71
+ return self.model.model_validate(obj)
72
+ ```
73
+
74
+ 一行 `Article.register_routes(app)` 即生成完整 JSON:API 接口。
75
+
76
+ ## 核心特性
77
+
78
+ - **JSON:API 规范** — 请求/响应格式、资源关系、稀疏字段、include 复合文档、分页
79
+ - **资源路由** — 类声明式路由,自动生成标准 CRUD 端点
80
+ - **关系管理** — 一对一/一对多关系,include 深层级联(最多支持 3 层)
81
+ - **权限系统** — 基于 scope 的 OAuth2 认证,API + 关系双层权限;支持 `@scope` 声明式配置
82
+ - **原子操作** — JSON:API Atomic 扩展支持,批量操作 + 事务回滚
83
+ - **文件上传/下载** — 增强的 `UploadFileResource` / `DownloadFileResource` 基类,文件类型验证、关系校验、可替换存储后端
84
+ - **响应缓存** — `@cached()` 装饰器一行启用接口缓存,Cache-Aside 模式,可替换后端
85
+ - **筛选过滤** — Deep Object 格式,40+ 操作符覆盖字符串/数值/列表/时间/布尔
86
+ - **自动模型** — 根据资源自动生成请求/响应模型
87
+ - **资源版本** — 内建 API 版本管理
88
+ - **自动文档** — 与 FastAPI OpenAPI 集成
89
+
90
+ ## 安装
91
+
92
+ > **要求**: Python 3.12+
93
+
94
+ ```bash
95
+ pip install git+https://gitee.com/socar/api_frame
96
+ ```
97
+
98
+ ## 快速开始
99
+
100
+ ```python
101
+ from fastapi import FastAPI
102
+ from api_frame import BaseResource, SchemaBase, Field
103
+ from api_frame.util import register_jsonapi_exception_handlers
104
+
105
+
106
+ class BookModel(SchemaBase):
107
+ id: str = Field(None)
108
+ title: str = Field(None)
109
+ author: str = Field(None)
110
+
111
+
112
+ class Book(BaseResource):
113
+ model = BookModel
114
+ methods = {'GET', 'GETS', 'POST', 'PATCH', 'DELETE'}
115
+
116
+ class Meta:
117
+ link = '/books'
118
+ type_ = 'books'
119
+
120
+
121
+ app = FastAPI()
122
+ register_jsonapi_exception_handlers(app)
123
+ Book.register_routes(app)
124
+ ```
125
+
126
+ 启动后自动生成:
127
+
128
+ | 方法 | 路径 | 说明 |
129
+ |------|------|------|
130
+ | `GET` | `/books` | 资源列表 |
131
+ | `GET` | `/books/{id}` | 单个资源 |
132
+ | `POST` | `/books` | 新增资源 |
133
+ | `PATCH` | `/books/{id}` | 更新资源 |
134
+ | `DELETE` | `/books/{id}` | 删除资源 |
135
+ | `POST` | `/atomic` | 原子操作(`register_routes()` 自动注册) |
136
+
137
+ ## 文档
138
+
139
+ - [框架概述 & 核心概念](docs/index.md)
140
+ - [快速开始指南](docs/guide.md)
141
+ - [API 参考](docs/api.md)
142
+ - [原子操作设计文档](docs/atomic.md)
143
+
144
+ ## 安装指定版本
145
+
146
+ ```bash
147
+ pip install git+https://gitee.com/socar/api_frame.git@v0.6.0
148
+ ```
149
+
150
+ ## 版本历史
151
+
152
+ | 版本 | 说明 |
153
+ |------|------|
154
+ | v0.6.0 | 声明式 scope 配置 (`@scope`)、增强文件上传/下载基类 (`filebase.py`)、统一缓存装饰器 (`@cached`)、SecurityConfig strict 模式。405 tests |
155
+ | v0.5.11 | `create_filter_model` 处理 `typing.Union`(`Optional[X]`);`page[limit]`/`page[offset]` 解析后转为 `int`。330 tests |
156
+ | v0.5.10 | `relapi=False` 时不生成 relationship links;`has_self_link` 控制 `data.links.self`;顶层 `links.self` 保留原始 request URL 的全部 query params;分页导航 `first/last/prev/next` 保留 `filter/sort/include` 等非分页参数;`get_host()` 不再硬编码 HTTPS,保留实际请求协议。325 tests |
157
+ | v0.5.9 | UploadFileBaseResource `examples` list 格式 + `e.raw_errors→e.errors()`(Pydantic v2 兼容);include 兄弟节点 `asyncio.gather` 并行;Schema cache double-lookup 优化(13 处);`list[tuple]→Union` 修复(Pydantic v2 homogeneous list 退化);`_build_response` / `_jsonapi` 单资源 `links.self` 含 ID(含 BaseResource 路径修复);NumberFilter/FloatFilter 增加 `aeq`;306 tests |
158
+ | v0.5.8 | JSON:API v1.1 合规全闭环(POST Location 头、PATCH body.id 409 校验、422 JSON Pointer `source.pointer`、顶层 `links.self` 含 ID、include 保留连字符、查询参数命名约定、分页 `links.first/last/prev/next` URL);`lid` 支持(ResourceIdentifier/RelationshipModel/ApiDataModelRequest);filter NumberFilter/FloatFilter 增加 `aeq` 操作符;query.py dict 类型检测兼容 Python 3.12(`__origin__ is dict`);`list[tuple(...)]` → Union(Pydantic v2 退化为 homogeneous list 修复);exception.py 空 parts JSON Pointer 边界保护;`_build_response` 单资源 `links.self` 正确提取 ID;303 tests |
159
+ | v0.5.7 | resource.py _jsonapi 新增 `many` 参数,空结果根据 many 返回 `[]` 或 `null`;handlers.py make_related_handler 对 to-one 404 自动转换为 `data: null`(修复 `/models/{id}/measure` 等 related 端点 500);292 tests |
160
+ | v0.5.6 | filter.py: UnionType (`int\|None`) + GenericAlias (`list[str]`) 支持;serializer.py: None to-one 关系产出 null 标识符;query.py: Filters/ArgsModel 的 get_field_value/get_filter/pop_and_filter 递归遍历 FilterOr;移除根目录残留 `__init__.py`;292 tests |
161
+ | v0.5.4 | JSON:API v1.1 合规收紧(ErrorModel.status str 强制 + field_validator 自动 int→str;ResourceIdentifier.id/type 必填不可空;ErrorResponse model_dump 默认 exclude_none);RefRes.id 统一 str;status 校验增加 bool 拒绝 + HTTP 100-599 区间;ResourceIdentifier validator mode=before + whitespace strip 自动规范化;新增边界测试覆盖空白 id、status 区间边界;292 tests |
162
+ | v0.5.3 | JSON:API v1.1 合规改进(ErrorModel/ErrorResponse/LinksSelfModel 扩展 v1.1 字段);related_cond/serializer request 传递 bug 修复;ApiDataModelResponse.id 类型收紧;get_field_value 类型注解修正 + ArgsModel 多值一致性;jsonapi.version → 1.1;LinksRelatedModel 补全 href/meta;OpenAPI summary 自然中文描述 + 资源类型前缀;清除废弃注释代码和构建残留;415/406 Content-Type/Accept 校验中间件 JSONAPIContentNegotiationMiddleware;移除废弃 Pydantic v1 兼容层(Meta.dict → model_dump、__fields__ → model_fields);RdentifierMeta → IdentifierMeta 正名;过期 warning 升级为 error;StrEnum / PEP 695 语法迁移;examples/ 全面清理;filterwarnings 移除;mypy 严格模式;requires-python ≥3.12;新增 31 合规补集测试(共 289 pass) |
163
+ | v0.5.0 | Pydantic v2 原生迁移;resource.py 拆分为 router/serializer/handlers/atomic 四模块;新增简化基类 Resource;Python 3.10+ 语法(`int | str` 替代 `Union`);FastAPI 0.136;移除 clean.py、build/;OpenAPI summary 优化 |
164
+ | v0.4.0 | 原子操作 Atomic 支持、权限三态模型(未配 scope = 放行)、docs 文档 |
165
+ | v0.3.30 | 兼容 map 类型的 get_field_value |
166
+ | v0.3.29 | 依赖版本更改 |
167
+ | v0.3.28 | str 支持,语义化搜索/向量搜索 |
168
+ | v0.3.27 | 支持 dict 类型过滤 |
169
+ | v0.3.26 | 权限配置缺失时提醒 |
170
+ | v0.3.25 | query 验证数组逻辑更改 |
171
+ | v0.3.24 | 权限验证绑定到关系的关系资源 |
172
+ | v0.3.23 | Filter 类增加 get_field_value 方法 |
173
+ | v0.3.22 | 判断接口是否有权限方法更改 |
174
+ | v0.3.21 | 验证关系权限前判断关系是否存在 |
175
+ | v0.3.20 | 更改 url 参数解析,使用 urllib.parse |
176
+ | v0.3.19 | route 命唯一名 |
177
+ | v0.3.0 | 调整 fastapi==0.92.0 |
@@ -0,0 +1,151 @@
1
+ # api_frame
2
+
3
+ 基于 FastAPI + JSON:API 规范的 RESTful 接口框架。
4
+
5
+ [![version](https://img.shields.io/badge/version-0.6.0-blue.svg)](https://gitee.com/socar/api_frame)
6
+
7
+ ## 简介
8
+
9
+ api_frame 提供了从资源声明 → 路由生成 → 请求处理 → 响应序列化的完整链路,让开发者**专注业务逻辑**,无需重复实现接口规范。
10
+
11
+ 框架提供两种资源基类:
12
+ - **`BaseResource`** — 完整基类,支持 `connect_data()` session 管理和子资源嵌套
13
+ - **`Resource`**(推荐) — 简化基类,内置 JSON:API 序列化管线,可直接访问 `self.db`
14
+
15
+ ```python
16
+ from api_frame import Resource, SchemaBase, Field, Relationship
17
+
18
+ class ArticleModel(SchemaBase):
19
+ title: str = Field(None)
20
+ content: str = Field(None)
21
+ author_id: int = Field(None, isrel=True)
22
+
23
+ class Article(Resource):
24
+ model = ArticleModel
25
+
26
+ class Meta:
27
+ link = '/articles'
28
+ type_ = 'articles'
29
+
30
+ class RelResources:
31
+ author = Relationship(
32
+ rel_resource='Author',
33
+ mapping_field='author_id',
34
+ has_api=False,
35
+ )
36
+
37
+ async def get_many(self):
38
+ return self.db.query(ArticleModel).all()
39
+
40
+ async def post(self):
41
+ body = self.parse_body()
42
+ obj = ArticleModel(**body.model_dump())
43
+ self.db.add(obj)
44
+ self.db.commit()
45
+ return self.model.model_validate(obj)
46
+ ```
47
+
48
+ 一行 `Article.register_routes(app)` 即生成完整 JSON:API 接口。
49
+
50
+ ## 核心特性
51
+
52
+ - **JSON:API 规范** — 请求/响应格式、资源关系、稀疏字段、include 复合文档、分页
53
+ - **资源路由** — 类声明式路由,自动生成标准 CRUD 端点
54
+ - **关系管理** — 一对一/一对多关系,include 深层级联(最多支持 3 层)
55
+ - **权限系统** — 基于 scope 的 OAuth2 认证,API + 关系双层权限;支持 `@scope` 声明式配置
56
+ - **原子操作** — JSON:API Atomic 扩展支持,批量操作 + 事务回滚
57
+ - **文件上传/下载** — 增强的 `UploadFileResource` / `DownloadFileResource` 基类,文件类型验证、关系校验、可替换存储后端
58
+ - **响应缓存** — `@cached()` 装饰器一行启用接口缓存,Cache-Aside 模式,可替换后端
59
+ - **筛选过滤** — Deep Object 格式,40+ 操作符覆盖字符串/数值/列表/时间/布尔
60
+ - **自动模型** — 根据资源自动生成请求/响应模型
61
+ - **资源版本** — 内建 API 版本管理
62
+ - **自动文档** — 与 FastAPI OpenAPI 集成
63
+
64
+ ## 安装
65
+
66
+ > **要求**: Python 3.12+
67
+
68
+ ```bash
69
+ pip install git+https://gitee.com/socar/api_frame
70
+ ```
71
+
72
+ ## 快速开始
73
+
74
+ ```python
75
+ from fastapi import FastAPI
76
+ from api_frame import BaseResource, SchemaBase, Field
77
+ from api_frame.util import register_jsonapi_exception_handlers
78
+
79
+
80
+ class BookModel(SchemaBase):
81
+ id: str = Field(None)
82
+ title: str = Field(None)
83
+ author: str = Field(None)
84
+
85
+
86
+ class Book(BaseResource):
87
+ model = BookModel
88
+ methods = {'GET', 'GETS', 'POST', 'PATCH', 'DELETE'}
89
+
90
+ class Meta:
91
+ link = '/books'
92
+ type_ = 'books'
93
+
94
+
95
+ app = FastAPI()
96
+ register_jsonapi_exception_handlers(app)
97
+ Book.register_routes(app)
98
+ ```
99
+
100
+ 启动后自动生成:
101
+
102
+ | 方法 | 路径 | 说明 |
103
+ |------|------|------|
104
+ | `GET` | `/books` | 资源列表 |
105
+ | `GET` | `/books/{id}` | 单个资源 |
106
+ | `POST` | `/books` | 新增资源 |
107
+ | `PATCH` | `/books/{id}` | 更新资源 |
108
+ | `DELETE` | `/books/{id}` | 删除资源 |
109
+ | `POST` | `/atomic` | 原子操作(`register_routes()` 自动注册) |
110
+
111
+ ## 文档
112
+
113
+ - [框架概述 & 核心概念](docs/index.md)
114
+ - [快速开始指南](docs/guide.md)
115
+ - [API 参考](docs/api.md)
116
+ - [原子操作设计文档](docs/atomic.md)
117
+
118
+ ## 安装指定版本
119
+
120
+ ```bash
121
+ pip install git+https://gitee.com/socar/api_frame.git@v0.6.0
122
+ ```
123
+
124
+ ## 版本历史
125
+
126
+ | 版本 | 说明 |
127
+ |------|------|
128
+ | v0.6.0 | 声明式 scope 配置 (`@scope`)、增强文件上传/下载基类 (`filebase.py`)、统一缓存装饰器 (`@cached`)、SecurityConfig strict 模式。405 tests |
129
+ | v0.5.11 | `create_filter_model` 处理 `typing.Union`(`Optional[X]`);`page[limit]`/`page[offset]` 解析后转为 `int`。330 tests |
130
+ | v0.5.10 | `relapi=False` 时不生成 relationship links;`has_self_link` 控制 `data.links.self`;顶层 `links.self` 保留原始 request URL 的全部 query params;分页导航 `first/last/prev/next` 保留 `filter/sort/include` 等非分页参数;`get_host()` 不再硬编码 HTTPS,保留实际请求协议。325 tests |
131
+ | v0.5.9 | UploadFileBaseResource `examples` list 格式 + `e.raw_errors→e.errors()`(Pydantic v2 兼容);include 兄弟节点 `asyncio.gather` 并行;Schema cache double-lookup 优化(13 处);`list[tuple]→Union` 修复(Pydantic v2 homogeneous list 退化);`_build_response` / `_jsonapi` 单资源 `links.self` 含 ID(含 BaseResource 路径修复);NumberFilter/FloatFilter 增加 `aeq`;306 tests |
132
+ | v0.5.8 | JSON:API v1.1 合规全闭环(POST Location 头、PATCH body.id 409 校验、422 JSON Pointer `source.pointer`、顶层 `links.self` 含 ID、include 保留连字符、查询参数命名约定、分页 `links.first/last/prev/next` URL);`lid` 支持(ResourceIdentifier/RelationshipModel/ApiDataModelRequest);filter NumberFilter/FloatFilter 增加 `aeq` 操作符;query.py dict 类型检测兼容 Python 3.12(`__origin__ is dict`);`list[tuple(...)]` → Union(Pydantic v2 退化为 homogeneous list 修复);exception.py 空 parts JSON Pointer 边界保护;`_build_response` 单资源 `links.self` 正确提取 ID;303 tests |
133
+ | v0.5.7 | resource.py _jsonapi 新增 `many` 参数,空结果根据 many 返回 `[]` 或 `null`;handlers.py make_related_handler 对 to-one 404 自动转换为 `data: null`(修复 `/models/{id}/measure` 等 related 端点 500);292 tests |
134
+ | v0.5.6 | filter.py: UnionType (`int\|None`) + GenericAlias (`list[str]`) 支持;serializer.py: None to-one 关系产出 null 标识符;query.py: Filters/ArgsModel 的 get_field_value/get_filter/pop_and_filter 递归遍历 FilterOr;移除根目录残留 `__init__.py`;292 tests |
135
+ | v0.5.4 | JSON:API v1.1 合规收紧(ErrorModel.status str 强制 + field_validator 自动 int→str;ResourceIdentifier.id/type 必填不可空;ErrorResponse model_dump 默认 exclude_none);RefRes.id 统一 str;status 校验增加 bool 拒绝 + HTTP 100-599 区间;ResourceIdentifier validator mode=before + whitespace strip 自动规范化;新增边界测试覆盖空白 id、status 区间边界;292 tests |
136
+ | v0.5.3 | JSON:API v1.1 合规改进(ErrorModel/ErrorResponse/LinksSelfModel 扩展 v1.1 字段);related_cond/serializer request 传递 bug 修复;ApiDataModelResponse.id 类型收紧;get_field_value 类型注解修正 + ArgsModel 多值一致性;jsonapi.version → 1.1;LinksRelatedModel 补全 href/meta;OpenAPI summary 自然中文描述 + 资源类型前缀;清除废弃注释代码和构建残留;415/406 Content-Type/Accept 校验中间件 JSONAPIContentNegotiationMiddleware;移除废弃 Pydantic v1 兼容层(Meta.dict → model_dump、__fields__ → model_fields);RdentifierMeta → IdentifierMeta 正名;过期 warning 升级为 error;StrEnum / PEP 695 语法迁移;examples/ 全面清理;filterwarnings 移除;mypy 严格模式;requires-python ≥3.12;新增 31 合规补集测试(共 289 pass) |
137
+ | v0.5.0 | Pydantic v2 原生迁移;resource.py 拆分为 router/serializer/handlers/atomic 四模块;新增简化基类 Resource;Python 3.10+ 语法(`int | str` 替代 `Union`);FastAPI 0.136;移除 clean.py、build/;OpenAPI summary 优化 |
138
+ | v0.4.0 | 原子操作 Atomic 支持、权限三态模型(未配 scope = 放行)、docs 文档 |
139
+ | v0.3.30 | 兼容 map 类型的 get_field_value |
140
+ | v0.3.29 | 依赖版本更改 |
141
+ | v0.3.28 | str 支持,语义化搜索/向量搜索 |
142
+ | v0.3.27 | 支持 dict 类型过滤 |
143
+ | v0.3.26 | 权限配置缺失时提醒 |
144
+ | v0.3.25 | query 验证数组逻辑更改 |
145
+ | v0.3.24 | 权限验证绑定到关系的关系资源 |
146
+ | v0.3.23 | Filter 类增加 get_field_value 方法 |
147
+ | v0.3.22 | 判断接口是否有权限方法更改 |
148
+ | v0.3.21 | 验证关系权限前判断关系是否存在 |
149
+ | v0.3.20 | 更改 url 参数解析,使用 urllib.parse |
150
+ | v0.3.19 | route 命唯一名 |
151
+ | v0.3.0 | 调整 fastapi==0.92.0 |
@@ -0,0 +1,86 @@
1
+ [build-system]
2
+ requires = ["setuptools>=82.0", "wheel>=0.47"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "socar-api"
7
+ version = "0.6.0"
8
+ description = "基于 FastAPI + JSON:API 规范的 RESTful 接口框架"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "so.car", email = "socar@so.car" },
14
+ ]
15
+
16
+ dependencies = [
17
+ "fastapi>=0.136.3",
18
+ "pydantic>=2.13.4",
19
+ "jquery-unparam>=2.0.0",
20
+ "pyyaml>=6.0.3",
21
+ "treelib>=1.8.0",
22
+ "bitarray>=3.8.1",
23
+ "python-multipart>=0.0.32",
24
+ "python-dateutil>=2.9.0.post0",
25
+ "typing-extensions>=4.15.0",
26
+ ]
27
+
28
+ [tool.setuptools.packages.find]
29
+ include = ["api_frame*", "examples*"]
30
+ where = ["src"]
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "pytest>=9.0.3",
35
+ "pytest-asyncio>=1.4.0",
36
+ "httpx>=0.28.1",
37
+ "pytest-cov>=7.1.0",
38
+ "ruff>=0.15.16",
39
+ "mypy>=2.1.0",
40
+ "pre-commit>=4.6.0",
41
+ ]
42
+
43
+ [tool.pytest.ini_options]
44
+ testpaths = ["tests"]
45
+ asyncio_mode = "auto"
46
+
47
+ [tool.ruff]
48
+ target-version = "py312"
49
+ line-length = 120
50
+
51
+ [tool.ruff.lint]
52
+ select = ["E", "F", "W", "I", "N", "UP"]
53
+
54
+ [tool.ruff.lint.per-file-ignores]
55
+ "src/api_frame/schema.py" = ["E501"]
56
+ "src/api_frame/*" = ["E402"]
57
+ "examples/*" = ["E501"]
58
+ "tests/test_serializer.py" = ["E402"]
59
+ [tool.mypy]
60
+ python_version = "3.12"
61
+ ignore_missing_imports = true
62
+ disallow_untyped_defs = true
63
+ no_implicit_optional = true
64
+
65
+ [[tool.mypy.overrides]]
66
+ module = [
67
+ "api_frame.schema",
68
+ "api_frame.filter",
69
+ "api_frame.query",
70
+ "api_frame.jsonapi",
71
+ "api_frame.resource",
72
+ "api_frame.serializer",
73
+ "api_frame.handlers",
74
+ "api_frame.field",
75
+ "api_frame.auth",
76
+ "api_frame.exception",
77
+ "api_frame.atomic",
78
+ "api_frame.router",
79
+ "api_frame.url_parse",
80
+ "api_frame.meta",
81
+ "api_frame.util",
82
+ "api_frame.cache",
83
+ "api_frame.filebase",
84
+ "api_frame.scope",
85
+ ]
86
+ ignore_errors = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,16 @@
1
+ __version__ = "0.6.0"
2
+
3
+ from api_frame.auth import Auth # noqa: F401
4
+ from api_frame.cache import MemoryBackend, cached, set_default_backend, set_version_hook # noqa: F401
5
+ from api_frame.exception import ( # noqa: F401
6
+ AuthError,
7
+ JsonapiException,
8
+ QureyError,
9
+ ResourceDuplicate,
10
+ ResourceNotFound,
11
+ )
12
+ from api_frame.filebase import DownloadFileResource, UploadFileResource # noqa: F401
13
+ from api_frame.query import ArgParse, Filter, FilterAnd, FilterOr # noqa: F401
14
+ from api_frame.resource import BaseResource, Resource, UploadFileBaseResource # noqa: F401
15
+ from api_frame.schema import Relationship, SchemaBase, field_mapping # noqa: F401
16
+ from api_frame.scope import scope # noqa: F401
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env python3
2
+ """Atomic operations module extracted from resource.py.
3
+
4
+ Provides AtomicOperation dataclass and AtomicExecutor class for
5
+ processing JSON:API atomic operations without mutating the request.
6
+ """
7
+
8
+ import logging
9
+ from dataclasses import dataclass
10
+ from typing import Any, Literal
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ from fastapi import Request
15
+
16
+ from api_frame.exception import AuthError, JsonapiException, serialize_error
17
+ from api_frame.meta import type_registered_resources
18
+
19
+
20
+ @dataclass
21
+ class AtomicOperation:
22
+ """Represents a single atomic operation.
23
+
24
+ Attributes:
25
+ op: The operation type ('add', 'update', 'remove')
26
+ target_cls: The target resource class
27
+ data: Operation data dict (from 'data' field)
28
+ ref_id: Optional reference id (for 'update' and 'remove')
29
+ """
30
+
31
+ op: Literal["add", "update", "remove"]
32
+ target_cls: type[Any]
33
+ data: dict
34
+ ref_id: str | None = None
35
+
36
+
37
+ class AtomicExecutor:
38
+ """Executes JSON:API atomic operations.
39
+
40
+ Processes a list of atomic operations by creating resource instances
41
+ directly and calling their post/patch/delete methods, without
42
+ mutating request.scope or request._json on the original request.
43
+ """
44
+
45
+ def __init__(self, request: Request):
46
+ self.request = request
47
+
48
+ async def run(self, operations: list[dict]) -> dict:
49
+ """Execute a list of atomic operations.
50
+
51
+ Args:
52
+ operations: List of operation dicts from 'atomic:operations'
53
+
54
+ Returns:
55
+ dict with 'atomic:results' key containing operation results
56
+
57
+ Raises:
58
+ JsonapiException: On validation errors
59
+ AuthError: On permission errors
60
+ """
61
+ if not operations:
62
+ raise JsonapiException(status_code=400, detail="atomic:operations 不能为空")
63
+
64
+ # 阶段1:解析并验证所有操作
65
+ resolved = await self._parse_operations(operations)
66
+
67
+ # 阶段2:在共享事务中执行所有操作
68
+ return await self._execute_operations(resolved)
69
+
70
+ async def _parse_operations(self, operations: list[dict]) -> list[tuple]:
71
+ """Parse and validate all operations.
72
+
73
+ Returns list of (op, target_cls, op_data, ref_id) tuples.
74
+ """
75
+ op_to_method = {"add": "POST", "update": "PATCH", "remove": "DELETE"}
76
+ resolved = []
77
+
78
+ for op_data in operations:
79
+ op = op_data.get("op")
80
+ if op not in ("add", "update", "remove"):
81
+ raise JsonapiException(status_code=400, detail=f"不支持的操作符: {op}")
82
+
83
+ if op == "add":
84
+ target_type = op_data.get("data", {}).get("type")
85
+ ref_id = None
86
+ else:
87
+ ref = op_data.get("ref", {})
88
+ target_type = ref.get("type")
89
+ ref_id = ref.get("id")
90
+ if not ref_id:
91
+ raise JsonapiException(status_code=400, detail="缺少资源 id")
92
+
93
+ if not target_type:
94
+ raise JsonapiException(status_code=400, detail="缺少资源类型 type")
95
+
96
+ target_cls = type_registered_resources.get(target_type)
97
+ if not target_cls:
98
+ raise JsonapiException(status_code=400, detail=f"资源类型不存在: {target_type}")
99
+
100
+ # 检查权限
101
+ if target_cls.Auth:
102
+ scope = target_cls.Auth.get_scopes(api_url=target_cls.Meta.link, method=op_to_method[op])
103
+ if scope is not None:
104
+ has_perm = await target_cls.Auth.api_auth(api_url=target_cls.Meta.link, method=op_to_method[op])
105
+ if not has_perm:
106
+ raise AuthError(detail=f"没有 {op_to_method[op]} {target_type} 的权限")
107
+
108
+ resolved.append((op, target_cls, op_data, ref_id))
109
+
110
+ return resolved
111
+
112
+ async def _execute_operations(self, resolved: list[tuple]) -> dict:
113
+ """Execute validated operations with shared transaction management."""
114
+ shared_db = None
115
+ last_target_cls = None # 用于异常时的错误处理回退
116
+ try:
117
+ # 获取 session 数据源
118
+ session_source = next((tc for _, tc, _, _ in resolved if tc.session is not None), None)
119
+
120
+ if session_source and session_source.session:
121
+ shared_db = session_source.session.get()
122
+ try:
123
+ shared_db.rollback()
124
+ except BaseException:
125
+ pass
126
+
127
+ results = []
128
+ for op, target_cls, op_data, ref_id in resolved:
129
+ last_target_cls = target_cls
130
+ result = await self._execute_single(op, target_cls, op_data, ref_id, shared_db)
131
+ results.append(
132
+ {
133
+ "data": {
134
+ "type": target_cls.Meta.type_,
135
+ "id": result.id if result and hasattr(result, "id") else None,
136
+ }
137
+ }
138
+ )
139
+
140
+ if shared_db:
141
+ shared_db.commit()
142
+
143
+ return {"atomic:results": results}
144
+
145
+ except BaseException as e:
146
+ if shared_db:
147
+ shared_db.rollback()
148
+ if last_target_cls is not None:
149
+ return await last_target_cls.handle_error(request=self.request, exc=e)
150
+ return await self._fallback_error(e)
151
+ finally:
152
+ if shared_db:
153
+ shared_db.close()
154
+
155
+ async def _execute_single(self, op: str, target_cls: type, op_data: dict, ref_id: str | None, shared_db):
156
+ """Execute a single atomic operation.
157
+
158
+ Creates a resource instance and calls the appropriate handler,
159
+ without mutating request.scope or request._json.
160
+ """
161
+ if op == "add":
162
+ body = op_data.get("data", {}).copy()
163
+ resource = target_cls(request=self.request, request_context={"request_body": {"data": body}})
164
+ # 标记为新增操作
165
+ resource._atomic_op = "add"
166
+ resource._atomic_body = body
167
+ if shared_db:
168
+ resource.db = shared_db
169
+ result = await resource.post()
170
+ if shared_db and resource.db is not shared_db:
171
+ logger.warning(
172
+ "原子操作: %s.post() 内部调用了 connect_data,已自动恢复共享事务", target_cls.Meta.type_
173
+ )
174
+ resource.db = shared_db
175
+
176
+ elif op == "update":
177
+ body = op_data.get("data", {}).copy()
178
+ body["id"] = ref_id
179
+ body.setdefault("type", target_cls.Meta.type_)
180
+ resource = target_cls(request=self.request, request_context={"request_body": {"data": body}})
181
+ # 标记为更新操作
182
+ resource._atomic_op = "update"
183
+ resource._atomic_body = body
184
+ resource._atomic_id = ref_id
185
+ if shared_db:
186
+ resource.db = shared_db
187
+ result = await resource.patch()
188
+ if shared_db and resource.db is not shared_db:
189
+ logger.warning(
190
+ "原子操作: %s.patch() 内部调用了 connect_data,已自动恢复共享事务", target_cls.Meta.type_
191
+ )
192
+ resource.db = shared_db
193
+
194
+ else: # remove
195
+ resource = target_cls(request=self.request)
196
+ resource._atomic_op = "remove"
197
+ resource._atomic_id = ref_id
198
+ if shared_db:
199
+ resource.db = shared_db
200
+ result = await resource.delete()
201
+ if shared_db and resource.db is not shared_db:
202
+ logger.warning(
203
+ "原子操作: %s.delete() 内部调用了 connect_data,已自动恢复共享事务", target_cls.Meta.type_
204
+ )
205
+ resource.db = shared_db
206
+
207
+ return result
208
+
209
+ async def _fallback_error(self, exc: BaseException) -> dict:
210
+ """Fallback error handler when target_cls is not available."""
211
+
212
+ return serialize_error(request=self.request, exc=exc)