@ihelpee/prumo-plugin-python-fastapi 0.0.0-stage → 0.9.0
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/LICENSE +202 -0
- package/README.md +108 -2
- package/dist/ast.d.ts +84 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +90 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/rules.d.ts +30 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +426 -0
- package/package.json +35 -3
- package/python/ast_extractor.py +439 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Prumo AST Extractor para Python/FastAPI — contrato canônico JSON v1.
|
|
4
|
+
|
|
5
|
+
Spec: docs/especificacao-prumo-python.md §1.4. Este script é DADO do gate,
|
|
6
|
+
não implementador da lei: ele apenas serializa AST em JSON. A decisão de
|
|
7
|
+
"é violação?" fica 100% no core Node — a lei permanece única.
|
|
8
|
+
|
|
9
|
+
Invariantes deste arquivo (atômicos, da spec):
|
|
10
|
+
R1.1 — apenas stdlib (`ast`, `json`, `sys`, `pathlib`). Import de terceiros
|
|
11
|
+
é falha de build.
|
|
12
|
+
R1.2 — nunca executa `import`/`exec`/`eval` do código analisado. Apenas
|
|
13
|
+
`ast.parse`. O subprocesso é o isolador: a análise é estática, o app
|
|
14
|
+
nunca dá boot.
|
|
15
|
+
R1.3 — arquivo com SyntaxError NÃO derruba o run: emite
|
|
16
|
+
{ "path", "error": "syntax" } e sai com código 0.
|
|
17
|
+
|
|
18
|
+
Uso: python3 ast_extractor.py --files <path> [<path> ...]
|
|
19
|
+
Saída: um único JSON no stdout: {"version": 1, "files": [...]}
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import ast
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
VERSION = 1
|
|
28
|
+
|
|
29
|
+
HTTP_METHODS = {"get", "post", "put", "patch", "delete"}
|
|
30
|
+
|
|
31
|
+
# Kwargs de Field(...) que constituem constraint de domínio (Pydantic v2;
|
|
32
|
+
# `regex` é o nome v1 — R3.6).
|
|
33
|
+
FIELD_CONSTRAINT_KWARGS = {
|
|
34
|
+
"gt", "ge", "lt", "le",
|
|
35
|
+
"min_length", "max_length",
|
|
36
|
+
"pattern", "regex",
|
|
37
|
+
"min_items", "max_items",
|
|
38
|
+
"multiple_of", "decimal_places",
|
|
39
|
+
"min_digits", "max_digits",
|
|
40
|
+
"allow_inf_nan", "strict",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Tipos/constraints que já são constraint por construção (R3.6).
|
|
44
|
+
CONSTRAINED_TYPES = {
|
|
45
|
+
"EmailStr", "PhoneEmail", "AnyUrl", "HttpUrl",
|
|
46
|
+
"conint", "constr", "confloat", "condecimal", "conlist", "conset",
|
|
47
|
+
"PositiveInt", "NonNegativeInt", "NegativeInt", "NonPositiveInt",
|
|
48
|
+
"PositiveFloat", "NonNegativeFloat", "NegativeFloat",
|
|
49
|
+
"FilePath", "DirectoryPath", "UUID4", "UUID", "DateTime",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Anotações de parâmetro que NÃO são body model (FastAPI builtins).
|
|
53
|
+
NON_BODY_ANNOTATIONS = {
|
|
54
|
+
"str", "int", "float", "bool", "bytes", "complex",
|
|
55
|
+
"dict", "list", "set", "tuple", "frozenset",
|
|
56
|
+
"Request", "WebSocket", "Response", "BackgroundTasks", "BackgroundTask",
|
|
57
|
+
"UploadFile", "Form", "File", "Query", "Path", "Header", "Cookie",
|
|
58
|
+
"Body", "Depends", "Security", "HTTPException",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _name_of(node):
|
|
63
|
+
"""Nome legível de um nó: Name.id, Attribute.attr, Subscript base."""
|
|
64
|
+
if node is None:
|
|
65
|
+
return ""
|
|
66
|
+
if isinstance(node, ast.Name):
|
|
67
|
+
return node.id
|
|
68
|
+
if isinstance(node, ast.Attribute):
|
|
69
|
+
return node.attr
|
|
70
|
+
if isinstance(node, ast.Subscript):
|
|
71
|
+
return _name_of(node.value)
|
|
72
|
+
if isinstance(node, ast.Call):
|
|
73
|
+
return _name_of(node.func)
|
|
74
|
+
return ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _dotted(node):
|
|
78
|
+
"""Cadeia pontuada completa de um Attribute/Name (ex.: `importlib.import_module`)."""
|
|
79
|
+
if isinstance(node, ast.Name):
|
|
80
|
+
return node.id
|
|
81
|
+
if isinstance(node, ast.Attribute):
|
|
82
|
+
prefix = _dotted(node.value)
|
|
83
|
+
return f"{prefix}.{node.attr}" if prefix else node.attr
|
|
84
|
+
return ""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _call_name(node):
|
|
88
|
+
if isinstance(node, ast.Call):
|
|
89
|
+
return _name_of(node.func)
|
|
90
|
+
return ""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _field_has_constraint(node):
|
|
94
|
+
"""Call a Field(...) com ao menos um kwarg de constraint de domínio."""
|
|
95
|
+
if not isinstance(node, ast.Call):
|
|
96
|
+
return False
|
|
97
|
+
if _call_name(node) != "Field":
|
|
98
|
+
return False
|
|
99
|
+
return any(k.arg in FIELD_CONSTRAINT_KWARGS for k in node.keywords)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _annotation_constrained(ann):
|
|
103
|
+
"""A anotação carrega constraint? (Field em Annotated, tipos con*, EmailStr, unions)."""
|
|
104
|
+
if ann is None:
|
|
105
|
+
return False
|
|
106
|
+
if isinstance(ann, ast.BinOp): # X | None
|
|
107
|
+
return _annotation_constrained(ann.left) or _annotation_constrained(ann.right)
|
|
108
|
+
if isinstance(ann, ast.Subscript):
|
|
109
|
+
base = _name_of(ann.value)
|
|
110
|
+
if base == "Annotated":
|
|
111
|
+
sl = ann.slice
|
|
112
|
+
elts = sl.elts if isinstance(sl, ast.Tuple) else [sl]
|
|
113
|
+
for extra in elts[1:]:
|
|
114
|
+
if _field_has_constraint(extra):
|
|
115
|
+
return True
|
|
116
|
+
if _name_of(extra) in CONSTRAINED_TYPES:
|
|
117
|
+
return True
|
|
118
|
+
if isinstance(extra, ast.Call) and _name_of(extra.func) in CONSTRAINED_TYPES:
|
|
119
|
+
return True
|
|
120
|
+
return False
|
|
121
|
+
if base in {"Optional", "Union"}:
|
|
122
|
+
sl = ann.slice
|
|
123
|
+
elts = sl.elts if isinstance(sl, ast.Tuple) else [sl]
|
|
124
|
+
return any(_annotation_constrained(e) for e in elts)
|
|
125
|
+
return _annotation_constrained(ann.value)
|
|
126
|
+
if isinstance(ann, ast.Call): # conint(gt=0)
|
|
127
|
+
return _name_of(ann.func) in CONSTRAINED_TYPES
|
|
128
|
+
return _name_of(ann) in CONSTRAINED_TYPES
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _depends_inner(call):
|
|
132
|
+
"""Depends(X) -> 'X' (Name, Attribute base, ou func de Call: Depends(HTTPBearer()))."""
|
|
133
|
+
if not (isinstance(call, ast.Call) and _call_name(call) == "Depends"):
|
|
134
|
+
return None
|
|
135
|
+
if not call.args:
|
|
136
|
+
return "Depends"
|
|
137
|
+
arg = call.args[0]
|
|
138
|
+
if isinstance(arg, ast.Call):
|
|
139
|
+
return _name_of(arg.func) or "Depends"
|
|
140
|
+
return _name_of(arg) or "Depends"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _param_depends(fn):
|
|
144
|
+
"""R3.1: Depends(X) em default de parâmetro OU em Annotated[X, Depends(...)]."""
|
|
145
|
+
out = []
|
|
146
|
+
positional = list(fn.args.posonlyargs) + list(fn.args.args)
|
|
147
|
+
# defaults de posicionais alinham com os ÚLTIMOS N args (ast padrão).
|
|
148
|
+
defaults = list(fn.args.defaults)
|
|
149
|
+
pad = [None] * (len(positional) - len(defaults)) + defaults
|
|
150
|
+
kwonly = list(fn.args.kwonlyargs)
|
|
151
|
+
kwdefaults = list(fn.args.kw_defaults)
|
|
152
|
+
pairs = list(zip(positional, pad)) + list(zip(kwonly, kwdefaults))
|
|
153
|
+
for a, default in pairs:
|
|
154
|
+
dep = _depends_inner(default)
|
|
155
|
+
if dep:
|
|
156
|
+
out.append(dep)
|
|
157
|
+
ann = a.annotation
|
|
158
|
+
if isinstance(ann, ast.Subscript) and _name_of(ann.value) == "Annotated":
|
|
159
|
+
sl = ann.slice
|
|
160
|
+
elts = sl.elts if isinstance(sl, ast.Tuple) else [sl]
|
|
161
|
+
for extra in elts[1:]:
|
|
162
|
+
dep = _depends_inner(extra)
|
|
163
|
+
if dep:
|
|
164
|
+
out.append(dep)
|
|
165
|
+
return out
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _body_annotation(fn):
|
|
169
|
+
"""Primeira anotação de parâmetro que é candidata a Pydantic model (body)."""
|
|
170
|
+
args = list(fn.args.posonlyargs) + list(fn.args.args) + list(fn.args.kwonlyargs)
|
|
171
|
+
for a in args:
|
|
172
|
+
ann = a.annotation
|
|
173
|
+
if ann is None:
|
|
174
|
+
continue
|
|
175
|
+
name = _name_of(ann)
|
|
176
|
+
if not name or name in NON_BODY_ANNOTATIONS:
|
|
177
|
+
continue
|
|
178
|
+
# Annotated[Model, Body(...)] -> Model
|
|
179
|
+
if isinstance(ann, ast.Subscript) and _name_of(ann.value) == "Annotated":
|
|
180
|
+
sl = ann.slice
|
|
181
|
+
elts = sl.elts if isinstance(sl, ast.Tuple) else [sl]
|
|
182
|
+
name = _name_of(elts[0]) if elts else name
|
|
183
|
+
if name and name not in NON_BODY_ANNOTATIONS:
|
|
184
|
+
return name
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _dependencies_kw(call):
|
|
189
|
+
"""dependencies=[Depends(X), ...] -> [X, ...] (R3.2/R3.3)."""
|
|
190
|
+
out = []
|
|
191
|
+
for k in call.keywords:
|
|
192
|
+
if k.arg != "dependencies" or not isinstance(k.value, ast.List):
|
|
193
|
+
continue
|
|
194
|
+
for elt in k.value.elts:
|
|
195
|
+
dep = _depends_inner(elt)
|
|
196
|
+
out.append(dep if dep else _name_of(elt))
|
|
197
|
+
return [d for d in out if d]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _route_from_decorators(fn):
|
|
201
|
+
"""Decorators @app.post / @router.put / @api_router.delete (FastAPI)."""
|
|
202
|
+
routes = []
|
|
203
|
+
for dec in fn.decorator_list:
|
|
204
|
+
if not isinstance(dec, ast.Call) or not isinstance(dec.func, ast.Attribute):
|
|
205
|
+
continue
|
|
206
|
+
method = dec.func.attr.upper()
|
|
207
|
+
if dec.func.attr.lower() not in HTTP_METHODS:
|
|
208
|
+
continue
|
|
209
|
+
prefix = _dotted(dec.func.value) # 'app', 'router', 'module.router'
|
|
210
|
+
path = ""
|
|
211
|
+
if dec.args and isinstance(dec.args[0], ast.Constant) and isinstance(dec.args[0].value, str):
|
|
212
|
+
path = dec.args[0].value
|
|
213
|
+
response_model = None
|
|
214
|
+
for k in dec.keywords:
|
|
215
|
+
if k.arg == "response_model":
|
|
216
|
+
if isinstance(k.value, ast.Constant) and isinstance(k.value.value, str):
|
|
217
|
+
response_model = k.value.value
|
|
218
|
+
else:
|
|
219
|
+
response_model = _name_of(k.value) or None
|
|
220
|
+
routes.append({
|
|
221
|
+
"method": method,
|
|
222
|
+
"path": path,
|
|
223
|
+
"handler": fn.name,
|
|
224
|
+
"decorators": [f"{prefix}.{dec.func.attr}" if prefix else dec.func.attr],
|
|
225
|
+
"router_var": prefix.split(".")[-1] if prefix else "",
|
|
226
|
+
"router_dependencies": _dependencies_kw(dec),
|
|
227
|
+
"param_depends": _param_depends(fn),
|
|
228
|
+
"body_model": _body_annotation(fn),
|
|
229
|
+
"response_model": response_model,
|
|
230
|
+
"line": dec.lineno,
|
|
231
|
+
})
|
|
232
|
+
return routes
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _is_pydantic_model(classdef):
|
|
236
|
+
for b in classdef.bases:
|
|
237
|
+
if _name_of(b) in {"BaseModel", "BaseSettings"}:
|
|
238
|
+
return True
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _collect_validators(classdef):
|
|
243
|
+
"""(campos citados por @field_validator/@validator, model_validator presente)."""
|
|
244
|
+
cited = set()
|
|
245
|
+
model_level = False
|
|
246
|
+
for item in classdef.body:
|
|
247
|
+
if not isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
248
|
+
continue
|
|
249
|
+
for dec in item.decorator_list:
|
|
250
|
+
dname = _call_name(dec) if isinstance(dec, ast.Call) else _name_of(dec)
|
|
251
|
+
if dname in {"field_validator", "validator"} and isinstance(dec, ast.Call):
|
|
252
|
+
for a in dec.args:
|
|
253
|
+
if isinstance(a, ast.Constant) and isinstance(a.value, str):
|
|
254
|
+
cited.add(a.value)
|
|
255
|
+
if dname in {"model_validator", "root_validator"}:
|
|
256
|
+
model_level = True
|
|
257
|
+
return cited, model_level
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _extract_model(classdef):
|
|
261
|
+
cited, model_level = _collect_validators(classdef)
|
|
262
|
+
fields = []
|
|
263
|
+
for item in classdef.body:
|
|
264
|
+
if not isinstance(item, ast.AnnAssign) or not isinstance(item.target, ast.Name):
|
|
265
|
+
continue
|
|
266
|
+
fname = item.target.id
|
|
267
|
+
has_constraint = _annotation_constrained(item.annotation) or _field_has_constraint(item.value)
|
|
268
|
+
has_validator = model_level or fname in cited
|
|
269
|
+
fields.append({
|
|
270
|
+
"name": fname,
|
|
271
|
+
"annotation": _name_of(item.annotation),
|
|
272
|
+
"has_constraint": has_constraint,
|
|
273
|
+
"has_validator": has_validator,
|
|
274
|
+
})
|
|
275
|
+
return {
|
|
276
|
+
"name": classdef.name,
|
|
277
|
+
"bases": [_name_of(b) for b in classdef.bases],
|
|
278
|
+
"line": classdef.lineno,
|
|
279
|
+
"fields": fields,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _module_of(rel_path):
|
|
284
|
+
"""'app/orders/router.py' -> 'app.orders.router'; __init__.py -> pacote."""
|
|
285
|
+
p = rel_path[:-3] if rel_path.endswith(".py") else rel_path
|
|
286
|
+
parts = p.replace("\\", "/").split("/")
|
|
287
|
+
if parts and parts[-1] == "__init__":
|
|
288
|
+
parts = parts[:-1]
|
|
289
|
+
return ".".join(parts)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _resolve_relative(file_module_parts, level, module):
|
|
293
|
+
"""R2.3: resolve `from ..x import y` contra o pacote do arquivo."""
|
|
294
|
+
base = list(file_module_parts) # partes do diretório do módulo
|
|
295
|
+
drop = level - 1
|
|
296
|
+
if drop > len(base):
|
|
297
|
+
return None
|
|
298
|
+
if drop:
|
|
299
|
+
base = base[: len(base) - drop]
|
|
300
|
+
parts = base + (module.split(".") if module else [])
|
|
301
|
+
return ".".join(parts) if parts else None
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _extract_imports(tree, rel_path):
|
|
305
|
+
imports = []
|
|
306
|
+
parts = _module_of(rel_path).split(".") if _module_of(rel_path) else []
|
|
307
|
+
dir_parts = parts[:-1] if parts else []
|
|
308
|
+
for node in ast.walk(tree):
|
|
309
|
+
if isinstance(node, ast.Import):
|
|
310
|
+
for alias in node.names:
|
|
311
|
+
imports.append({
|
|
312
|
+
"module": alias.name,
|
|
313
|
+
"names": [alias.asname or alias.name],
|
|
314
|
+
"relative": False,
|
|
315
|
+
"line": node.lineno,
|
|
316
|
+
})
|
|
317
|
+
elif isinstance(node, ast.ImportFrom):
|
|
318
|
+
if node.level and node.level > 0:
|
|
319
|
+
resolved = _resolve_relative(dir_parts, node.level, node.module or "")
|
|
320
|
+
if resolved is None:
|
|
321
|
+
continue
|
|
322
|
+
imports.append({
|
|
323
|
+
"module": resolved,
|
|
324
|
+
"names": [a.asname or a.name for a in node.names],
|
|
325
|
+
"relative": True,
|
|
326
|
+
"line": node.lineno,
|
|
327
|
+
})
|
|
328
|
+
else:
|
|
329
|
+
imports.append({
|
|
330
|
+
"module": node.module or "",
|
|
331
|
+
"names": [a.asname or a.name for a in node.names],
|
|
332
|
+
"relative": False,
|
|
333
|
+
"line": node.lineno,
|
|
334
|
+
})
|
|
335
|
+
return imports
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _extract_dynamic_imports(tree):
|
|
339
|
+
"""R2.4 anti-burla: importlib.import_module('...') e __import__('...')."""
|
|
340
|
+
out = []
|
|
341
|
+
for node in ast.walk(tree):
|
|
342
|
+
if not isinstance(node, ast.Call):
|
|
343
|
+
continue
|
|
344
|
+
dotted = _dotted(node.func)
|
|
345
|
+
is_dynamic = dotted == "__import__" or dotted.endswith("importlib.import_module") or dotted == "import_module"
|
|
346
|
+
if not is_dynamic or not node.args:
|
|
347
|
+
continue
|
|
348
|
+
first = node.args[0]
|
|
349
|
+
if isinstance(first, ast.Constant) and isinstance(first.value, str):
|
|
350
|
+
out.append({"expr": f"{dotted}({first.value!r})", "module": first.value, "line": node.lineno})
|
|
351
|
+
return out
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _extract_router_defs(tree):
|
|
355
|
+
"""`router = APIRouter(dependencies=[...])` (R3.3, camada de definição)."""
|
|
356
|
+
routers = []
|
|
357
|
+
for node in ast.walk(tree):
|
|
358
|
+
if not isinstance(node, ast.Assign) or not isinstance(node.value, ast.Call):
|
|
359
|
+
continue
|
|
360
|
+
if _call_name(node.value) != "APIRouter":
|
|
361
|
+
continue
|
|
362
|
+
for t in node.targets:
|
|
363
|
+
if isinstance(t, ast.Name):
|
|
364
|
+
routers.append({"var": t.id, "dependencies": _dependencies_kw(node.value)})
|
|
365
|
+
return routers
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _extract_include_routers(tree):
|
|
369
|
+
"""`app.include_router(x, dependencies=[...])` (R3.3, camada de montagem)."""
|
|
370
|
+
out = []
|
|
371
|
+
for node in ast.walk(tree):
|
|
372
|
+
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
|
|
373
|
+
continue
|
|
374
|
+
if node.func.attr != "include_router" or not node.args:
|
|
375
|
+
continue
|
|
376
|
+
target = _dotted(node.args[0])
|
|
377
|
+
if not target:
|
|
378
|
+
continue
|
|
379
|
+
out.append({"target": target, "dependencies": _dependencies_kw(node)})
|
|
380
|
+
return out
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _extract_file(path, root):
|
|
384
|
+
rel = str(path.relative_to(root)) if root in path.parents or path == root else str(path)
|
|
385
|
+
rel = rel.replace("\\", "/")
|
|
386
|
+
try:
|
|
387
|
+
source = path.read_text(encoding="utf-8")
|
|
388
|
+
except (OSError, UnicodeDecodeError):
|
|
389
|
+
return {"path": rel, "error": "unreadable"}
|
|
390
|
+
try:
|
|
391
|
+
tree = ast.parse(source, filename=rel)
|
|
392
|
+
except SyntaxError:
|
|
393
|
+
# R1.3: nunca derruba o run, nunca stack trace no stdout.
|
|
394
|
+
return {"path": rel, "error": "syntax"}
|
|
395
|
+
|
|
396
|
+
pydantic_v1 = any(
|
|
397
|
+
imp["module"] == "pydantic.v1" or imp["module"].startswith("pydantic.v1.")
|
|
398
|
+
for imp in _extract_imports(tree, rel)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
routes = []
|
|
402
|
+
models = []
|
|
403
|
+
for node in ast.walk(tree):
|
|
404
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
405
|
+
routes.extend(_route_from_decorators(node))
|
|
406
|
+
elif isinstance(node, ast.ClassDef) and _is_pydantic_model(node):
|
|
407
|
+
models.append(_extract_model(node))
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
"path": rel,
|
|
411
|
+
"imports": _extract_imports(tree, rel),
|
|
412
|
+
"dynamic_imports": _extract_dynamic_imports(tree),
|
|
413
|
+
"routes": routes,
|
|
414
|
+
"models": models,
|
|
415
|
+
"routers": _extract_router_defs(tree),
|
|
416
|
+
"include_routers": _extract_include_routers(tree),
|
|
417
|
+
"pydantic_v1": pydantic_v1,
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def main(argv):
|
|
422
|
+
if "--files" not in argv:
|
|
423
|
+
print(json.dumps({"version": VERSION, "files": []}))
|
|
424
|
+
return 0
|
|
425
|
+
idx = argv.index("--files")
|
|
426
|
+
root = Path.cwd()
|
|
427
|
+
files = []
|
|
428
|
+
for raw in argv[idx + 1:]:
|
|
429
|
+
p = Path(raw)
|
|
430
|
+
if not p.is_absolute():
|
|
431
|
+
p = root / p
|
|
432
|
+
files.append(p)
|
|
433
|
+
out = [_extract_file(p, root) for p in files]
|
|
434
|
+
print(json.dumps({"version": VERSION, "files": out}))
|
|
435
|
+
return 0
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
if __name__ == "__main__":
|
|
439
|
+
sys.exit(main(sys.argv[1:]))
|