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.
- {flask_frame-1.2.2/src/flask_frame.egg-info → flask_frame-1.3.0}/PKG-INFO +90 -4
- {flask_frame-1.2.2 → flask_frame-1.3.0}/README.md +89 -1
- {flask_frame-1.2.2 → flask_frame-1.3.0}/pyproject.toml +15 -4
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/__init__.py +3 -0
- flask_frame-1.3.0/src/flask_frame/openapi.py +383 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0/src/flask_frame.egg-info}/PKG-INFO +90 -4
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame.egg-info/SOURCES.txt +1 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame.egg-info/requires.txt +0 -2
- {flask_frame-1.2.2 → flask_frame-1.3.0}/LICENSE +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/setup.cfg +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/algorithm/__init__.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/algorithm/text.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/annotation/__init__.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/api/__init__.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/api/exception.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/api/request.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/api/response.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/app.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/config.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/__init__.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/api_log/__init__.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/api_log/model.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/celery.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/consul.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/database/__init__.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/database/model.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/lock.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/loguru/__init__.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/loguru/compress.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/loguru/macro.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/marshmallow.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/minio.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/permission.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/postgrest/__init__.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/postgrest/sql_generator.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/postgrest/util.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/redis.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/sentry.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/permission_context.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/py.typed +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/schema/__init__.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/thread/__init__.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/thread/dk_thread_pool.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/thread/my_synchronized.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/__init__.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/com_tool.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/db.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/enum.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/fernet_tool.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/file.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/json.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/lock.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/model.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/param_tool.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/py_utils.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/rsa_tool.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/util/sql_tool.py +0 -0
- {flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame.egg-info/dependency_links.txt +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
43
|
-
|
|
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.
|
|
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
|
-
|
|
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()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flask_frame-1.2.2 → flask_frame-1.3.0}/src/flask_frame/extension/postgrest/sql_generator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|