flask-frame 1.2.2__tar.gz → 1.3.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 (59) hide show
  1. {flask_frame-1.2.2/src/flask_frame.egg-info → flask_frame-1.3.0}/PKG-INFO +90 -4
  2. {flask_frame-1.2.2 → flask_frame-1.3.0}/README.md +89 -1
  3. {flask_frame-1.2.2 → flask_frame-1.3.0}/pyproject.toml +15 -4
  4. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/__init__.py +3 -0
  5. flask_frame-1.3.0/src/flask_frame/openapi.py +383 -0
  6. {flask_frame-1.2.2 → flask_frame-1.3.0/src/flask_frame.egg-info}/PKG-INFO +90 -4
  7. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame.egg-info/SOURCES.txt +1 -0
  8. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame.egg-info/requires.txt +0 -2
  9. {flask_frame-1.2.2 → flask_frame-1.3.0}/LICENSE +0 -0
  10. {flask_frame-1.2.2 → flask_frame-1.3.0}/setup.cfg +0 -0
  11. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/algorithm/__init__.py +0 -0
  12. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/algorithm/text.py +0 -0
  13. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/annotation/__init__.py +0 -0
  14. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/api/__init__.py +0 -0
  15. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/api/exception.py +0 -0
  16. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/api/request.py +0 -0
  17. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/api/response.py +0 -0
  18. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/app.py +0 -0
  19. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/config.py +0 -0
  20. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/__init__.py +0 -0
  21. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/api_log/__init__.py +0 -0
  22. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/api_log/model.py +0 -0
  23. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/celery.py +0 -0
  24. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/consul.py +0 -0
  25. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/database/__init__.py +0 -0
  26. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/database/model.py +0 -0
  27. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/lock.py +0 -0
  28. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/loguru/__init__.py +0 -0
  29. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/loguru/compress.py +0 -0
  30. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/loguru/macro.py +0 -0
  31. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/marshmallow.py +0 -0
  32. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/minio.py +0 -0
  33. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/permission.py +0 -0
  34. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/postgrest/__init__.py +0 -0
  35. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/postgrest/sql_generator.py +0 -0
  36. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/postgrest/util.py +0 -0
  37. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/redis.py +0 -0
  38. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/sentry.py +0 -0
  39. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/permission_context.py +0 -0
  40. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/py.typed +0 -0
  41. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/schema/__init__.py +0 -0
  42. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/thread/__init__.py +0 -0
  43. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/thread/dk_thread_pool.py +0 -0
  44. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/thread/my_synchronized.py +0 -0
  45. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/__init__.py +0 -0
  46. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/com_tool.py +0 -0
  47. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/db.py +0 -0
  48. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/enum.py +0 -0
  49. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/fernet_tool.py +0 -0
  50. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/file.py +0 -0
  51. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/json.py +0 -0
  52. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/lock.py +0 -0
  53. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/model.py +0 -0
  54. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/param_tool.py +0 -0
  55. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/py_utils.py +0 -0
  56. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/rsa_tool.py +0 -0
  57. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/sql_tool.py +0 -0
  58. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame.egg-info/dependency_links.txt +0 -0
  59. {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flask_frame
3
- Version: 1.2.2
3
+ Version: 1.3.0
4
4
  Summary: 基于FLASK快速开发REST接口框架
5
5
  Author-email: wuhanchu <whcwuhanchu@gmail.com>
6
6
  License-Expression: MIT
@@ -32,8 +32,6 @@ Requires-Dist: psycopg2-binary==2.9.10
32
32
  Requires-Dist: rsa==4.9
33
33
  Requires-Dist: pycryptodome==3.23.0
34
34
  Requires-Dist: cryptography==46.0.5
35
- Requires-Dist: gunicorn==23.0.0
36
- Requires-Dist: gevent==24.11.1
37
35
  Requires-Dist: tenacity==9.1.2
38
36
  Requires-Dist: loguru==0.7.3
39
37
  Requires-Dist: pytz~=2025.2
@@ -51,7 +49,7 @@ Dynamic: license-file
51
49
  ### 本地开发
52
50
 
53
51
  ```bash
54
- pip install -e .
52
+ pip3 install -e .
55
53
  ```
56
54
 
57
55
  ### 发布到 PyPI
@@ -85,6 +83,93 @@ nohup docker build . --force-rm=true -f docker/Dockerfile.alpine_continue -t wuh
85
83
  - `GET /debug-sentry` - 触发错误(调试 Sentry)
86
84
  - 任意接口加 `?profile` 参数可获取性能分析报告
87
85
 
86
+ ## OpenAPI 文档生成
87
+
88
+ 从 Flask 路由自动生成 OpenAPI 3.0 JSON,支持导入 Apifox 或通过 API 自动推送(增量同步)。
89
+
90
+ ```python
91
+ from flask_frame import create_app, generate_openapi, sync_to_apifox
92
+
93
+ app = create_app(config)
94
+
95
+ # 生成 JSON 文件
96
+ generate_openapi(app, title="My API", output_dir="./api_json")
97
+
98
+ # 自动同步到 Apifox(支持增量)
99
+ sync_to_apifox(app, token="afxp_xxx", project_id="xxx", title="My API")
100
+ ```
101
+
102
+ 获取凭据:Apifox 项目设置 → API 认证获取 Token,项目设置 → 基本信息获取项目 ID。
103
+
104
+ ### VS Code Task 配置
105
+
106
+ 在业务项目中创建 `.vscode/tasks.json`:
107
+
108
+ ```json
109
+ {
110
+ "version": "2.0.0",
111
+ "tasks": [
112
+ {
113
+ "label": "sync_to_apifox",
114
+ "type": "shell",
115
+ "command": "/usr/bin/python3.12",
116
+ "args": [
117
+ "script/sync_to_apifox.py",
118
+ "--token", "afxp_xxx",
119
+ "--project-id", "xxx"
120
+ ],
121
+ "group": "build",
122
+ "presentation": {
123
+ "echo": true,
124
+ "reveal": "always",
125
+ "focus": true,
126
+ "panel": "dedicated",
127
+ "showReuseMessage": false,
128
+ "clear": false
129
+ },
130
+ "problemMatcher": []
131
+ }
132
+ ]
133
+ }
134
+ ```
135
+
136
+ 对应的 `script/sync_to_apifox.py`:
137
+
138
+ ```python
139
+ import argparse
140
+ from flask_frame import create_app, sync_to_apifox
141
+ from config import config
142
+ import module, context
143
+
144
+ app = create_app(config)
145
+ module.init_app(app)
146
+ context.init_app(app)
147
+
148
+ parser = argparse.ArgumentParser()
149
+ parser.add_argument("--token", required=True)
150
+ parser.add_argument("--project-id", required=True)
151
+ parser.add_argument("--force", action="store_true")
152
+ args = parser.parse_args()
153
+
154
+ sync_to_apifox(app, token=args.token, project_id=args.project_id, title="你的API名称", force=args.force)
155
+ ```
156
+
157
+ 使用 `Ctrl+Shift+B` 选择 `sync_to_apifox` 即可推送到 Apifox。
158
+
159
+ 视图函数的 Google 风格 docstring 会自动解析为 API 描述和参数说明:
160
+
161
+ ```python
162
+ @app.route("/api/users")
163
+ def get_users():
164
+ """获取用户列表
165
+
166
+ Args:
167
+ page (int): 页码,默认 1
168
+ size (int): 每页条数
169
+ """
170
+ ...
171
+ ```
172
+
88
173
  ## 配置项
89
174
 
90
175
  ### 基础配置
@@ -160,6 +245,7 @@ nohup docker build . --force-rm=true -f docker/Dockerfile.alpine_continue -t wuh
160
245
  ```
161
246
  src/flask_frame/
162
247
  ├── app.py # 应用工厂 create_app()
248
+ ├── openapi.py # OpenAPI 文档生成(generate_openapi)
163
249
  ├── api/ # 请求/响应/异常处理
164
250
  │ ├── request.py # get_request_param(), proxy()
165
251
  │ ├── response.py # Response.make_flask_response()
@@ -7,7 +7,7 @@
7
7
  ### 本地开发
8
8
 
9
9
  ```bash
10
- pip install -e .
10
+ pip3 install -e .
11
11
  ```
12
12
 
13
13
  ### 发布到 PyPI
@@ -41,6 +41,93 @@ nohup docker build . --force-rm=true -f docker/Dockerfile.alpine_continue -t wuh
41
41
  - `GET /debug-sentry` - 触发错误(调试 Sentry)
42
42
  - 任意接口加 `?profile` 参数可获取性能分析报告
43
43
 
44
+ ## OpenAPI 文档生成
45
+
46
+ 从 Flask 路由自动生成 OpenAPI 3.0 JSON,支持导入 Apifox 或通过 API 自动推送(增量同步)。
47
+
48
+ ```python
49
+ from flask_frame import create_app, generate_openapi, sync_to_apifox
50
+
51
+ app = create_app(config)
52
+
53
+ # 生成 JSON 文件
54
+ generate_openapi(app, title="My API", output_dir="./api_json")
55
+
56
+ # 自动同步到 Apifox(支持增量)
57
+ sync_to_apifox(app, token="afxp_xxx", project_id="xxx", title="My API")
58
+ ```
59
+
60
+ 获取凭据:Apifox 项目设置 → API 认证获取 Token,项目设置 → 基本信息获取项目 ID。
61
+
62
+ ### VS Code Task 配置
63
+
64
+ 在业务项目中创建 `.vscode/tasks.json`:
65
+
66
+ ```json
67
+ {
68
+ "version": "2.0.0",
69
+ "tasks": [
70
+ {
71
+ "label": "sync_to_apifox",
72
+ "type": "shell",
73
+ "command": "/usr/bin/python3.12",
74
+ "args": [
75
+ "script/sync_to_apifox.py",
76
+ "--token", "afxp_xxx",
77
+ "--project-id", "xxx"
78
+ ],
79
+ "group": "build",
80
+ "presentation": {
81
+ "echo": true,
82
+ "reveal": "always",
83
+ "focus": true,
84
+ "panel": "dedicated",
85
+ "showReuseMessage": false,
86
+ "clear": false
87
+ },
88
+ "problemMatcher": []
89
+ }
90
+ ]
91
+ }
92
+ ```
93
+
94
+ 对应的 `script/sync_to_apifox.py`:
95
+
96
+ ```python
97
+ import argparse
98
+ from flask_frame import create_app, sync_to_apifox
99
+ from config import config
100
+ import module, context
101
+
102
+ app = create_app(config)
103
+ module.init_app(app)
104
+ context.init_app(app)
105
+
106
+ parser = argparse.ArgumentParser()
107
+ parser.add_argument("--token", required=True)
108
+ parser.add_argument("--project-id", required=True)
109
+ parser.add_argument("--force", action="store_true")
110
+ args = parser.parse_args()
111
+
112
+ sync_to_apifox(app, token=args.token, project_id=args.project_id, title="你的API名称", force=args.force)
113
+ ```
114
+
115
+ 使用 `Ctrl+Shift+B` 选择 `sync_to_apifox` 即可推送到 Apifox。
116
+
117
+ 视图函数的 Google 风格 docstring 会自动解析为 API 描述和参数说明:
118
+
119
+ ```python
120
+ @app.route("/api/users")
121
+ def get_users():
122
+ """获取用户列表
123
+
124
+ Args:
125
+ page (int): 页码,默认 1
126
+ size (int): 每页条数
127
+ """
128
+ ...
129
+ ```
130
+
44
131
  ## 配置项
45
132
 
46
133
  ### 基础配置
@@ -116,6 +203,7 @@ nohup docker build . --force-rm=true -f docker/Dockerfile.alpine_continue -t wuh
116
203
  ```
117
204
  src/flask_frame/
118
205
  ├── app.py # 应用工厂 create_app()
206
+ ├── openapi.py # OpenAPI 文档生成(generate_openapi)
119
207
  ├── api/ # 请求/响应/异常处理
120
208
  │ ├── request.py # get_request_param(), proxy()
121
209
  │ ├── response.py # Response.make_flask_response()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flask_frame"
7
- version = "1.2.2"
7
+ version = "1.3.0"
8
8
  description = "基于FLASK快速开发REST接口框架"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -24,23 +24,34 @@ classifiers = [
24
24
  "Typing :: Typed",
25
25
  ]
26
26
  dependencies = [
27
+ # 核心 Web 框架及扩展
27
28
  "Flask==3.1.3",
28
29
  "flask_marshmallow==1.3.0",
29
30
  "marshmallow==3.26.2",
30
31
  "marshmallow-sqlalchemy==1.4.2",
32
+
33
+ # 自动化
31
34
  "celery==5.5.1",
35
+
36
+ # 性能与监控
32
37
  "pyinstrument==5.0.1",
33
38
  "sentry_sdk==2.26.1",
39
+
40
+ # 网络与通信
34
41
  "Requests==2.32.3",
35
42
  "redis==5.2.1",
43
+
44
+ # 数据库
36
45
  "Flask_SQLAlchemy==3.1.1",
37
46
  "SQLAlchemy==2.0.40",
38
47
  "psycopg2-binary==2.9.10",
48
+
49
+ # 安全与加密
39
50
  "rsa==4.9",
40
51
  "pycryptodome==3.23.0",
41
52
  "cryptography==46.0.5",
42
- "gunicorn==23.0.0",
43
- "gevent==24.11.1",
53
+
54
+ # 工具库
44
55
  "tenacity==9.1.2",
45
56
  "loguru==0.7.3",
46
57
  "pytz~=2025.2",
@@ -56,4 +67,4 @@ Homepage = "https://github.com/wuhanchu/flask_frame"
56
67
  where = ["src"]
57
68
 
58
69
  [tool.setuptools.package-data]
59
- flask_frame = ["py.typed"]
70
+ flask_frame = ["py.typed"]
@@ -31,6 +31,7 @@ from .api.exception import ResourceError, CallException, BusiError
31
31
  from .schema import BaseSchema, DateTimeField
32
32
  from .annotation import deprecated, profile
33
33
  from .config import FlaskFrameConfig
34
+ from .openapi import generate_openapi, sync_to_apifox
34
35
 
35
36
  __all__ = [
36
37
  "create_app",
@@ -45,4 +46,6 @@ __all__ = [
45
46
  "deprecated",
46
47
  "profile",
47
48
  "FlaskFrameConfig",
49
+ "generate_openapi",
50
+ "sync_to_apifox",
48
51
  ]
@@ -0,0 +1,383 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ OpenAPI 文档生成与 Apifox 同步工具。
4
+ 从 Flask 应用的路由信息自动生成 OpenAPI 3.0 规范 JSON,可直接导入 Apifox 或通过 API 自动推送。
5
+
6
+ 使用示例:
7
+ from flask_frame import create_app
8
+ from flask_frame.openapi import generate_openapi, sync_to_apifox
9
+
10
+ app = create_app(config)
11
+
12
+ # 生成完整规范(返回 dict)
13
+ spec = generate_openapi(app, title="My API")
14
+
15
+ # 按蓝图分组生成并保存为文件
16
+ generate_openapi(app, title="My API", output_dir="./api_json", group_by="blueprint")
17
+
18
+ # 同步到 Apifox(支持增量同步)
19
+ sync_to_apifox(app, token="xxx", project_id="xxx", title="My API")
20
+ """
21
+ import json
22
+ import os
23
+ import re
24
+ import inspect
25
+ import hashlib
26
+ from typing import Any
27
+
28
+ __all__ = ["generate_openapi", "sync_to_apifox"]
29
+
30
+
31
+ def _parse_docstring(func) -> dict[str, Any]:
32
+ """
33
+ 解析视图函数的 docstring,提取 summary、description、参数说明。
34
+
35
+ 支持 Google 风格:
36
+ summary 一句话描述
37
+ 更详细的 description
38
+
39
+ Args:
40
+ name (str): 参数说明
41
+ page (int, optional): 页码,默认 1
42
+
43
+ Returns:
44
+ dict: 返回说明
45
+
46
+ Returns:
47
+ dict: {"summary": ..., "description": ..., "params": [...]}
48
+ """
49
+ doc = inspect.getdoc(func)
50
+ if not doc:
51
+ return {"summary": "", "description": "", "params": []}
52
+
53
+ lines = doc.strip().split("\n")
54
+ summary = lines[0].strip()
55
+ description = ""
56
+ params = []
57
+
58
+ # 提取 Args 段落
59
+ in_args = False
60
+ for line in lines[1:]:
61
+ stripped = line.strip()
62
+
63
+ if stripped.lower().startswith("args:"):
64
+ in_args = True
65
+ continue
66
+ if in_args:
67
+ if stripped and not stripped.startswith(" ") and not line.startswith(" "):
68
+ # 遇到新的顶级段落,结束 Args
69
+ in_args = False
70
+ else:
71
+ # 解析参数行:name (type): description 或 name: description
72
+ match = re.match(
73
+ r"(\w+)\s*(?:\([^)]*\))?\s*[::]\s*(.+)", stripped
74
+ )
75
+ if match:
76
+ params.append({
77
+ "name": match.group(1),
78
+ "description": match.group(2).strip(),
79
+ })
80
+
81
+ # 非 Args 段落的非首行内容作为 description
82
+ desc_lines = []
83
+ in_args = False
84
+ for line in lines[1:]:
85
+ stripped = line.strip()
86
+ if stripped.lower().startswith("args:"):
87
+ in_args = True
88
+ continue
89
+ if stripped.lower().startswith("returns:"):
90
+ break
91
+ if not in_args and stripped:
92
+ desc_lines.append(stripped)
93
+ description = "\n".join(desc_lines)
94
+
95
+ return {"summary": summary, "description": description, "params": params}
96
+
97
+
98
+ def _build_paths(app, filter_prefix: str | None = None) -> dict[str, dict]:
99
+ """
100
+ 从 Flask app 的 url_map 构建 OpenAPI paths 对象。
101
+
102
+ Args:
103
+ app: Flask 应用实例。
104
+ filter_prefix: 路径前缀过滤,仅包含以此前缀开头的路由。
105
+
106
+ Returns:
107
+ dict: OpenAPI paths 字典。
108
+ """
109
+ paths: dict[str, dict] = {}
110
+
111
+ for rule in app.url_map.iter_rules():
112
+ # 跳过静态文件和内部路由
113
+ if rule.endpoint in ("static",) or rule.endpoint.startswith("flasgger"):
114
+ continue
115
+
116
+ path = rule.rule
117
+ if filter_prefix and not path.startswith(filter_prefix):
118
+ continue
119
+
120
+ # 将 Flask 路径参数 <name> 转为 OpenAPI {name}
121
+ openapi_path = re.sub(r"<(?:\w+:)?(\w+)>", r"{\1}", path)
122
+
123
+ methods = [m for m in rule.methods if m not in ("HEAD", "OPTIONS")]
124
+ if not methods:
125
+ continue
126
+
127
+ view_func = app.view_functions.get(rule.endpoint)
128
+ doc_info = _parse_docstring(view_func) if view_func else {}
129
+
130
+ # 从路由参数推断路径参数类型
131
+ path_params = []
132
+ for arg in rule.arguments:
133
+ converter = rule._converters.get(arg)
134
+ param_type = "string"
135
+ if converter:
136
+ type_map = {"int": "integer", "float": "number"}
137
+ param_type = type_map.get(getattr(converter, "type", ""), "string")
138
+ path_params.append({
139
+ "name": arg,
140
+ "in": "path",
141
+ "required": True,
142
+ "schema": {"type": param_type},
143
+ })
144
+
145
+ # 从 docstring 的 Args 段落补充 query 参数
146
+ query_params = []
147
+ for p in doc_info.get("params", []):
148
+ query_params.append({
149
+ "name": p["name"],
150
+ "in": "query",
151
+ "required": False,
152
+ "schema": {"type": "string"},
153
+ "description": p["description"],
154
+ })
155
+
156
+ parameters = path_params + query_params
157
+
158
+ path_item: dict[str, Any] = {}
159
+ for method in methods:
160
+ operation: dict[str, Any] = {
161
+ "summary": doc_info.get("summary", f"{method} {openapi_path}"),
162
+ "tags": [_get_tag(rule.endpoint)],
163
+ "responses": {
164
+ "200": {"description": "成功"},
165
+ "500": {"description": "服务器错误"},
166
+ },
167
+ }
168
+ if doc_info.get("description"):
169
+ operation["description"] = doc_info["description"]
170
+ if parameters:
171
+ operation["parameters"] = parameters
172
+
173
+ path_item[method.lower()] = operation
174
+
175
+ paths[openapi_path] = path_item
176
+
177
+ return paths
178
+
179
+
180
+ def _get_tag(endpoint: str) -> str:
181
+ """从 endpoint 名称提取 tag(取第一段作为分组)。"""
182
+ parts = endpoint.split(".")
183
+ return parts[0] if len(parts) > 1 else "default"
184
+
185
+
186
+ def generate_openapi(
187
+ app,
188
+ title: str = "API",
189
+ version: str = "1.0.0",
190
+ description: str = "",
191
+ filter_prefix: str | None = None,
192
+ group_by: str | None = None,
193
+ output_dir: str | None = None,
194
+ ) -> dict | dict[str, dict]:
195
+ """
196
+ 从 Flask 应用生成 OpenAPI 3.0 规范。
197
+
198
+ Args:
199
+ app: 已初始化路由的 Flask 应用实例。
200
+ title: API 标题。
201
+ version: API 版本号。
202
+ description: API 描述。
203
+ filter_prefix: 仅生成以此路径前缀开头的路由(如 "/api/v1")。
204
+ group_by: 分组方式。"blueprint" 按蓝图前缀拆分为多个文件,None 生成单个文件。
205
+ output_dir: 输出目录。指定时自动保存 JSON 文件。
206
+
207
+ Returns:
208
+ 单文件模式返回 dict(OpenAPI 3.0 规范)。
209
+ 分组模式返回 dict[str, dict](键为蓝图名,值为规范)。
210
+ """
211
+ base_spec = {
212
+ "openapi": "3.0.0",
213
+ "info": {
214
+ "title": title,
215
+ "version": version,
216
+ },
217
+ "paths": {},
218
+ }
219
+ if description:
220
+ base_spec["info"]["description"] = description
221
+
222
+ if group_by == "blueprint":
223
+ # 按蓝图分组
224
+ tag_groups: dict[str, dict[str, dict]] = {}
225
+ all_paths = _build_paths(app, filter_prefix)
226
+
227
+ for path, path_item in all_paths.items():
228
+ for method, operation in path_item.items():
229
+ tag = operation.get("tags", ["default"])[0]
230
+ if tag not in tag_groups:
231
+ tag_groups[tag] = {}
232
+ if path not in tag_groups[tag]:
233
+ tag_groups[tag][path] = {}
234
+ tag_groups[tag][path][method] = operation
235
+
236
+ result = {}
237
+ for tag, paths in tag_groups.items():
238
+ spec = {**base_spec, "info": {**base_spec["info"], "title": f"{title} - {tag}"}, "paths": paths}
239
+ result[tag] = spec
240
+
241
+ if output_dir:
242
+ os.makedirs(output_dir, exist_ok=True)
243
+ for tag, spec in result.items():
244
+ file_path = os.path.join(output_dir, f"{tag}.json")
245
+ with open(file_path, "w", encoding="utf-8") as f:
246
+ json.dump(spec, f, ensure_ascii=False, indent=2)
247
+
248
+ return result
249
+ else:
250
+ # 单文件模式
251
+ spec = {**base_spec, "paths": _build_paths(app, filter_prefix)}
252
+
253
+ if output_dir:
254
+ os.makedirs(output_dir, exist_ok=True)
255
+ file_path = os.path.join(output_dir, "openapi.json")
256
+ with open(file_path, "w", encoding="utf-8") as f:
257
+ json.dump(spec, f, ensure_ascii=False, indent=2)
258
+
259
+ return spec
260
+
261
+
262
+ def _compute_hash(data: Any) -> str:
263
+ """计算数据的 SHA-256 哈希,用于增量同步对比。"""
264
+ normalized = json.dumps(data, sort_keys=True, ensure_ascii=False)
265
+ return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
266
+
267
+
268
+ def _load_snapshot(snapshot_dir: str) -> dict:
269
+ """加载增量同步快照。"""
270
+ hash_path = os.path.join(snapshot_dir, "path_hashes.json")
271
+ if os.path.exists(hash_path):
272
+ with open(hash_path, "r", encoding="utf-8") as f:
273
+ return json.load(f)
274
+ return {}
275
+
276
+
277
+ def _save_snapshot(snapshot_dir: str, data: dict) -> None:
278
+ """保存增量同步快照。"""
279
+ os.makedirs(snapshot_dir, exist_ok=True)
280
+ hash_path = os.path.join(snapshot_dir, "path_hashes.json")
281
+ with open(hash_path, "w", encoding="utf-8") as f:
282
+ json.dump(data, f, ensure_ascii=False, indent=2)
283
+
284
+
285
+ def sync_to_apifox(
286
+ app,
287
+ token: str,
288
+ project_id: str,
289
+ title: str = "API",
290
+ version: str = "1.0.0",
291
+ description: str = "",
292
+ filter_prefix: str | None = None,
293
+ snapshot_dir: str | None = None,
294
+ force: bool = False,
295
+ ) -> bool:
296
+ """
297
+ 生成 OpenAPI 规范并同步到 Apifox,支持增量同步。
298
+
299
+ Args:
300
+ app: 已初始化路由的 Flask 应用实例。
301
+ token: Apifox API Token(从 Apifox 项目设置 → API 认证 获取)。
302
+ project_id: Apifox 项目 ID(从项目设置 → 基本信息获取)。
303
+ title: API 标题。
304
+ version: API 版本号。
305
+ description: API 描述。
306
+ filter_prefix: 仅同步以此路径前缀开头的路由。
307
+ snapshot_dir: 增量快照目录,默认为当前工作目录下的 .sync_snapshot。
308
+ force: 强制全量同步,忽略增量检测。
309
+
310
+ Returns:
311
+ bool: 同步是否成功。
312
+ """
313
+ import requests
314
+
315
+ spec = generate_openapi(app, title=title, version=version, description=description, filter_prefix=filter_prefix)
316
+ paths = spec.get("paths", {})
317
+
318
+ # 增量同步检测
319
+ if snapshot_dir is None:
320
+ snapshot_dir = os.path.join(os.getcwd(), ".sync_snapshot")
321
+
322
+ if not force:
323
+ last_hashes = _load_snapshot(snapshot_dir)
324
+ new_hashes = {}
325
+ changed_paths = {}
326
+
327
+ for path, methods in paths.items():
328
+ h = _compute_hash(methods)
329
+ new_hashes[path] = h
330
+ if last_hashes.get(path) != h:
331
+ changed_paths[path] = methods
332
+
333
+ deleted = set(last_hashes.keys()) - set(paths.keys())
334
+
335
+ if not changed_paths and not deleted:
336
+ print("API 无变化,跳过同步")
337
+ return True
338
+
339
+ print(f"检测到 {len(changed_paths)} 个接口变更" + (f",{len(deleted)} 个接口删除" if deleted else ""))
340
+ push_spec = {**spec, "paths": changed_paths}
341
+ incremental = True
342
+ else:
343
+ push_spec = spec
344
+ incremental = False
345
+ new_hashes = {p: _compute_hash(m) for p, m in paths.items()}
346
+
347
+ # 调用 Apifox API
348
+ url = f"https://api.apifox.com/v1/projects/{project_id}/import-openapi?locale=zh-CN"
349
+ headers = {
350
+ "Authorization": f"Bearer {token}",
351
+ "X-Apifox-Api-Version": "2024-03-28",
352
+ "Content-Type": "application/json",
353
+ }
354
+ payload = {
355
+ "input": json.dumps(push_spec, ensure_ascii=False),
356
+ "options": {
357
+ "endpointOverwriteBehavior": "OVERWRITE_EXISTING",
358
+ "schemaOverwriteBehavior": "OVERWRITE_EXISTING",
359
+ "updateFolderOfChangedEndpoint": True,
360
+ "deleteUnmatchedResources": not incremental,
361
+ },
362
+ }
363
+
364
+ try:
365
+ response = requests.post(url, headers=headers, json=payload, timeout=60)
366
+ if response.status_code == 200:
367
+ result = response.json()
368
+ counters = result.get("data", {}).get("counters", {})
369
+ created = counters.get("endpointCreated", 0)
370
+ updated = counters.get("endpointUpdated", 0)
371
+ failed = counters.get("endpointFailed", 0)
372
+ print(f"同步成功!创建: {created}, 更新: {updated}, 失败: {failed}")
373
+
374
+ # 保存快照
375
+ _save_snapshot(snapshot_dir, new_hashes)
376
+ return True
377
+ else:
378
+ print(f"同步失败,状态码: {response.status_code}")
379
+ print(f"错误信息: {response.text}")
380
+ return False
381
+ except requests.exceptions.RequestException as e:
382
+ print(f"网络请求失败: {e}")
383
+ return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flask_frame
3
- Version: 1.2.2
3
+ Version: 1.3.0
4
4
  Summary: 基于FLASK快速开发REST接口框架
5
5
  Author-email: wuhanchu <whcwuhanchu@gmail.com>
6
6
  License-Expression: MIT
@@ -32,8 +32,6 @@ Requires-Dist: psycopg2-binary==2.9.10
32
32
  Requires-Dist: rsa==4.9
33
33
  Requires-Dist: pycryptodome==3.23.0
34
34
  Requires-Dist: cryptography==46.0.5
35
- Requires-Dist: gunicorn==23.0.0
36
- Requires-Dist: gevent==24.11.1
37
35
  Requires-Dist: tenacity==9.1.2
38
36
  Requires-Dist: loguru==0.7.3
39
37
  Requires-Dist: pytz~=2025.2
@@ -51,7 +49,7 @@ Dynamic: license-file
51
49
  ### 本地开发
52
50
 
53
51
  ```bash
54
- pip install -e .
52
+ pip3 install -e .
55
53
  ```
56
54
 
57
55
  ### 发布到 PyPI
@@ -85,6 +83,93 @@ nohup docker build . --force-rm=true -f docker/Dockerfile.alpine_continue -t wuh
85
83
  - `GET /debug-sentry` - 触发错误(调试 Sentry)
86
84
  - 任意接口加 `?profile` 参数可获取性能分析报告
87
85
 
86
+ ## OpenAPI 文档生成
87
+
88
+ 从 Flask 路由自动生成 OpenAPI 3.0 JSON,支持导入 Apifox 或通过 API 自动推送(增量同步)。
89
+
90
+ ```python
91
+ from flask_frame import create_app, generate_openapi, sync_to_apifox
92
+
93
+ app = create_app(config)
94
+
95
+ # 生成 JSON 文件
96
+ generate_openapi(app, title="My API", output_dir="./api_json")
97
+
98
+ # 自动同步到 Apifox(支持增量)
99
+ sync_to_apifox(app, token="afxp_xxx", project_id="xxx", title="My API")
100
+ ```
101
+
102
+ 获取凭据:Apifox 项目设置 → API 认证获取 Token,项目设置 → 基本信息获取项目 ID。
103
+
104
+ ### VS Code Task 配置
105
+
106
+ 在业务项目中创建 `.vscode/tasks.json`:
107
+
108
+ ```json
109
+ {
110
+ "version": "2.0.0",
111
+ "tasks": [
112
+ {
113
+ "label": "sync_to_apifox",
114
+ "type": "shell",
115
+ "command": "/usr/bin/python3.12",
116
+ "args": [
117
+ "script/sync_to_apifox.py",
118
+ "--token", "afxp_xxx",
119
+ "--project-id", "xxx"
120
+ ],
121
+ "group": "build",
122
+ "presentation": {
123
+ "echo": true,
124
+ "reveal": "always",
125
+ "focus": true,
126
+ "panel": "dedicated",
127
+ "showReuseMessage": false,
128
+ "clear": false
129
+ },
130
+ "problemMatcher": []
131
+ }
132
+ ]
133
+ }
134
+ ```
135
+
136
+ 对应的 `script/sync_to_apifox.py`:
137
+
138
+ ```python
139
+ import argparse
140
+ from flask_frame import create_app, sync_to_apifox
141
+ from config import config
142
+ import module, context
143
+
144
+ app = create_app(config)
145
+ module.init_app(app)
146
+ context.init_app(app)
147
+
148
+ parser = argparse.ArgumentParser()
149
+ parser.add_argument("--token", required=True)
150
+ parser.add_argument("--project-id", required=True)
151
+ parser.add_argument("--force", action="store_true")
152
+ args = parser.parse_args()
153
+
154
+ sync_to_apifox(app, token=args.token, project_id=args.project_id, title="你的API名称", force=args.force)
155
+ ```
156
+
157
+ 使用 `Ctrl+Shift+B` 选择 `sync_to_apifox` 即可推送到 Apifox。
158
+
159
+ 视图函数的 Google 风格 docstring 会自动解析为 API 描述和参数说明:
160
+
161
+ ```python
162
+ @app.route("/api/users")
163
+ def get_users():
164
+ """获取用户列表
165
+
166
+ Args:
167
+ page (int): 页码,默认 1
168
+ size (int): 每页条数
169
+ """
170
+ ...
171
+ ```
172
+
88
173
  ## 配置项
89
174
 
90
175
  ### 基础配置
@@ -160,6 +245,7 @@ nohup docker build . --force-rm=true -f docker/Dockerfile.alpine_continue -t wuh
160
245
  ```
161
246
  src/flask_frame/
162
247
  ├── app.py # 应用工厂 create_app()
248
+ ├── openapi.py # OpenAPI 文档生成(generate_openapi)
163
249
  ├── api/ # 请求/响应/异常处理
164
250
  │ ├── request.py # get_request_param(), proxy()
165
251
  │ ├── response.py # Response.make_flask_response()
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  src/flask_frame/__init__.py
5
5
  src/flask_frame/app.py
6
6
  src/flask_frame/config.py
7
+ src/flask_frame/openapi.py
7
8
  src/flask_frame/permission_context.py
8
9
  src/flask_frame/py.typed
9
10
  src/flask_frame.egg-info/PKG-INFO
@@ -13,8 +13,6 @@ psycopg2-binary==2.9.10
13
13
  rsa==4.9
14
14
  pycryptodome==3.23.0
15
15
  cryptography==46.0.5
16
- gunicorn==23.0.0
17
- gevent==24.11.1
18
16
  tenacity==9.1.2
19
17
  loguru==0.7.3
20
18
  pytz~=2025.2
File without changes
File without changes