progmune-runtime 3.7.9 → 3.7.10
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.
- package/CHANGELOG.md +9 -0
- package/dist/sdk.js +1 -1
- package/package.json +7 -3
- package/tools/extract_framework_django.py +269 -0
- package/tools/extract_framework_flask.py +169 -0
- package/tools/extract_framework_py.py +237 -0
- package/tools/extract_ir.py +1109 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
FastAPI Framework Structure Extractor — 框架结构扫描(与 extract_ir.py 解耦)
|
|
4
|
+
|
|
5
|
+
Progmune 框架适配 M1:从 Python AST 提取 FastAPI 结构——
|
|
6
|
+
路由(@app.get/@router.post 等)、处理器依赖注入(Depends()/Security())、
|
|
7
|
+
认证方案声明(OAuth2PasswordBearer/HTTPBearer/APIKeyHeader)、全局中间件。
|
|
8
|
+
|
|
9
|
+
输出 JSON:
|
|
10
|
+
{
|
|
11
|
+
"hasFastAPI": bool,
|
|
12
|
+
"apps": [{"name","file"}],
|
|
13
|
+
"routers": [{"name","file"}],
|
|
14
|
+
"authSchemes": [{"name","type"}],
|
|
15
|
+
"globalAuthMiddleware": [names...],
|
|
16
|
+
"routes": [{
|
|
17
|
+
"method","path","handler","file","line",
|
|
18
|
+
"dependencies": [{"name","via","authLike"}],
|
|
19
|
+
}],
|
|
20
|
+
"filesScanned": int
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
用法:python3 extract_framework_py.py <projectRoot> <outJson>
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import ast
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
# 认证词表(依赖函数名命中即视为 auth-like;与 TS 侧 annotation-suggest 词表同源)
|
|
32
|
+
# 注意:扫描器只做「结构提取」,规则判定(豁免词/方法门控)在 TS 检测器侧。
|
|
33
|
+
AUTH_WORDS = (
|
|
34
|
+
"auth", "login", "token", "user", "credential", "session",
|
|
35
|
+
"bearer", "permission", "current_user", "api_key", "oauth",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
SKIP_DIRS = {"tests", "test", "deps", "venv", "env", "node_modules", "vendor",
|
|
39
|
+
".git", "migrations", "__pycache__", "scripts", "docs"}
|
|
40
|
+
|
|
41
|
+
ROUTE_DECORATORS = {"get", "post", "put", "delete", "patch", "head", "options",
|
|
42
|
+
"websocket", "api_route"}
|
|
43
|
+
|
|
44
|
+
SCHEME_CLASSES = {
|
|
45
|
+
"OAuth2PasswordBearer": "OAuth2PasswordBearer",
|
|
46
|
+
"OAuth2AuthorizationCodeBearer": "OAuth2AuthorizationCodeBearer",
|
|
47
|
+
"HTTPBearer": "HTTPBearer",
|
|
48
|
+
"HTTPBasic": "HTTPBasic",
|
|
49
|
+
"HTTPDigest": "HTTPDigest",
|
|
50
|
+
"APIKeyHeader": "APIKeyHeader",
|
|
51
|
+
"APIKeyQuery": "APIKeyQuery",
|
|
52
|
+
"APIKeyCookie": "APIKeyCookie",
|
|
53
|
+
"OpenIdConnect": "OpenIdConnect",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def name_of(node):
|
|
58
|
+
"""ast 名字解析(Name/Attribute/Subscript 兼容)"""
|
|
59
|
+
if isinstance(node, ast.Name):
|
|
60
|
+
return node.id
|
|
61
|
+
if isinstance(node, ast.Attribute):
|
|
62
|
+
return node.attr
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def lower_name(name):
|
|
67
|
+
return name.lower() if name else ""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def is_auth_like(name, schemes):
|
|
71
|
+
"""依赖名是否为认证依赖:命中认证词表,或直接是声明的认证方案"""
|
|
72
|
+
if not name:
|
|
73
|
+
return False
|
|
74
|
+
if name in schemes:
|
|
75
|
+
return True
|
|
76
|
+
ln = lower_name(name)
|
|
77
|
+
return any(w in ln for w in AUTH_WORDS)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def resolve_dep_target(node):
|
|
81
|
+
"""Depends(x) 的 x:Name/Attribute 直取;嵌套 Call(如 get_current_user_authorizer())取内层函数名"""
|
|
82
|
+
if isinstance(node, (ast.Name, ast.Attribute)):
|
|
83
|
+
return name_of(node)
|
|
84
|
+
if isinstance(node, ast.Call):
|
|
85
|
+
return name_of(node.func)
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def iter_dependency_calls(node):
|
|
90
|
+
"""从签名节点收集 Depends(...)/Security(...) 调用(含 Annotated[...] 订阅)"""
|
|
91
|
+
found = []
|
|
92
|
+
for child in ast.walk(node):
|
|
93
|
+
# Depends(x) / Security(x)
|
|
94
|
+
if isinstance(child, ast.Call) and isinstance(child.func, ast.Name):
|
|
95
|
+
if child.func.id in ("Depends", "Security") and child.args:
|
|
96
|
+
found.append((resolve_dep_target(child.args[0]), child.func.id))
|
|
97
|
+
# Annotated[X, Depends(y)]
|
|
98
|
+
if isinstance(child, ast.Subscript):
|
|
99
|
+
for item in child.slice.elts if isinstance(child.slice, ast.Tuple) else [child.slice]:
|
|
100
|
+
if isinstance(item, ast.Call) and isinstance(item.func, ast.Name):
|
|
101
|
+
if item.func.id in ("Depends", "Security") and item.args:
|
|
102
|
+
found.append((resolve_dep_target(item.args[0]), item.func.id))
|
|
103
|
+
return found
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Scanner(ast.NodeVisitor):
|
|
107
|
+
def __init__(self, filepath):
|
|
108
|
+
self.file = filepath
|
|
109
|
+
self.apps = []
|
|
110
|
+
self.routers = []
|
|
111
|
+
self.schemes = {}
|
|
112
|
+
self.middlewares = []
|
|
113
|
+
self.routes = []
|
|
114
|
+
self._decorator_targets = {} # name -> {method,path} 列表
|
|
115
|
+
|
|
116
|
+
def visit_Assign(self, node):
|
|
117
|
+
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
|
118
|
+
v = node.value
|
|
119
|
+
if isinstance(v, ast.Call) and isinstance(v.func, ast.Name):
|
|
120
|
+
fn = v.func.id
|
|
121
|
+
if fn == "FastAPI":
|
|
122
|
+
self.apps.append(node.targets[0].id)
|
|
123
|
+
elif fn == "APIRouter":
|
|
124
|
+
self.routers.append(node.targets[0].id)
|
|
125
|
+
elif fn in SCHEME_CLASSES:
|
|
126
|
+
self.schemes[node.targets[0].id] = SCHEME_CLASSES[fn]
|
|
127
|
+
self.generic_visit(node)
|
|
128
|
+
|
|
129
|
+
def visit_Expr(self, node):
|
|
130
|
+
# app.add_middleware(...) 是 Expr(Call(Attribute(app, add_middleware)))
|
|
131
|
+
v = node.value
|
|
132
|
+
if (isinstance(v, ast.Call) and isinstance(v.func, ast.Attribute)
|
|
133
|
+
and v.func.attr == "add_middleware" and v.args
|
|
134
|
+
and isinstance(v.args[0], ast.Name)):
|
|
135
|
+
self.middlewares.append(v.args[0].id)
|
|
136
|
+
self.generic_visit(node)
|
|
137
|
+
|
|
138
|
+
def visit_FunctionDef(self, node):
|
|
139
|
+
self._scan_decorated(node, is_async=False)
|
|
140
|
+
self.generic_visit(node)
|
|
141
|
+
|
|
142
|
+
def visit_AsyncFunctionDef(self, node):
|
|
143
|
+
self._scan_decorated(node, is_async=True)
|
|
144
|
+
self.generic_visit(node)
|
|
145
|
+
|
|
146
|
+
def _scan_decorated(self, node, is_async):
|
|
147
|
+
for dec in node.decorator_list:
|
|
148
|
+
# @app.get("/path") / @router.post(...) / @r.api_route(...)
|
|
149
|
+
if (isinstance(dec, ast.Call) and isinstance(dec.func, ast.Attribute)
|
|
150
|
+
and dec.func.attr in ROUTE_DECORATORS):
|
|
151
|
+
target = name_of(dec.func.value)
|
|
152
|
+
method = dec.func.attr
|
|
153
|
+
path = dec.args[0].value if dec.args and isinstance(dec.args[0], ast.Constant) else ""
|
|
154
|
+
deps = iter_dependency_calls(node.args)
|
|
155
|
+
# 装饰器级 dependencies=[Depends(...)](realworld 风格:
|
|
156
|
+
# @router.put(..., dependencies=[Depends(check_permissions)])
|
|
157
|
+
for kw in dec.keywords:
|
|
158
|
+
if kw.arg == "dependencies" and isinstance(kw.value, ast.List):
|
|
159
|
+
for item in kw.value.elts:
|
|
160
|
+
if isinstance(item, ast.Call) and isinstance(item.func, ast.Name):
|
|
161
|
+
if item.func.id in ("Depends", "Security") and item.args:
|
|
162
|
+
deps.append((resolve_dep_target(item.args[0]), item.func.id))
|
|
163
|
+
auth_like = [
|
|
164
|
+
{"name": d[0], "via": d[1],
|
|
165
|
+
"authLike": is_auth_like(d[0], self.schemes)}
|
|
166
|
+
for d in deps
|
|
167
|
+
]
|
|
168
|
+
self.routes.append({
|
|
169
|
+
"method": method,
|
|
170
|
+
"path": path,
|
|
171
|
+
"handler": node.name,
|
|
172
|
+
"file": self.file,
|
|
173
|
+
"line": node.lineno,
|
|
174
|
+
"dependencies": auth_like,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def scan_file(filepath):
|
|
179
|
+
try:
|
|
180
|
+
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
|
|
181
|
+
tree = ast.parse(f.read(), filename=filepath)
|
|
182
|
+
except (SyntaxError, UnicodeDecodeError, OSError):
|
|
183
|
+
return None
|
|
184
|
+
scanner = Scanner(filepath)
|
|
185
|
+
scanner.visit(tree)
|
|
186
|
+
return scanner
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def walk_py_files(root):
|
|
190
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
191
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
192
|
+
for fn in filenames:
|
|
193
|
+
if fn.endswith(".py") and not fn.startswith("test_"):
|
|
194
|
+
yield os.path.join(dirpath, fn)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def main():
|
|
198
|
+
root = sys.argv[1]
|
|
199
|
+
out = sys.argv[2]
|
|
200
|
+
apps, routers, schemes, middlewares, routes = [], [], {}, [], []
|
|
201
|
+
files_scanned = 0
|
|
202
|
+
for fp in walk_py_files(root):
|
|
203
|
+
files_scanned += 1
|
|
204
|
+
sc = scan_file(fp)
|
|
205
|
+
if not sc:
|
|
206
|
+
continue
|
|
207
|
+
apps.extend(sc.apps)
|
|
208
|
+
routers.extend(sc.routers)
|
|
209
|
+
schemes.update(sc.schemes)
|
|
210
|
+
middlewares.extend(sc.middlewares)
|
|
211
|
+
routes.extend(sc.routes)
|
|
212
|
+
|
|
213
|
+
# 路由处理器名去重(同文件同名函数只记一次)
|
|
214
|
+
seen = set()
|
|
215
|
+
unique_routes = []
|
|
216
|
+
for r in routes:
|
|
217
|
+
key = (r["file"], r["handler"], r["method"], r["path"])
|
|
218
|
+
if key in seen:
|
|
219
|
+
continue
|
|
220
|
+
seen.add(key)
|
|
221
|
+
unique_routes.append(r)
|
|
222
|
+
|
|
223
|
+
result = {
|
|
224
|
+
"hasFastAPI": bool(apps or routers),
|
|
225
|
+
"apps": sorted(set(apps)),
|
|
226
|
+
"routers": sorted(set(routers)),
|
|
227
|
+
"authSchemes": [{"name": n, "type": t} for n, t in schemes.items()],
|
|
228
|
+
"globalAuthMiddleware": sorted(set(middlewares)),
|
|
229
|
+
"routes": unique_routes,
|
|
230
|
+
"filesScanned": files_scanned,
|
|
231
|
+
}
|
|
232
|
+
with open(out, "w", encoding="utf-8") as f:
|
|
233
|
+
json.dump(result, f, ensure_ascii=False, indent=2)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
if __name__ == "__main__":
|
|
237
|
+
main()
|