msra-codegen 0.1.0__py3-none-any.whl
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.
- msra_codegen/README.md +23 -0
- msra_codegen/__init__.py +6 -0
- msra_codegen/__main__.py +5 -0
- msra_codegen/bridge.py +29 -0
- msra_codegen/cli.py +105 -0
- msra_codegen/codegen_context.py +1690 -0
- msra_codegen/config.toml +164 -0
- msra_codegen/core_naming.py +155 -0
- msra_codegen/docs_generator.py +346 -0
- msra_codegen/file_utils.py +8 -0
- msra_codegen/funcresult.py +156 -0
- msra_codegen/generator.py +6 -0
- msra_codegen/generator_config.py +35 -0
- msra_codegen/github_workflows.py +129 -0
- msra_codegen/gitignore.py +31 -0
- msra_codegen/issue_templates.py +100 -0
- msra_codegen/logo_assets.py +99 -0
- msra_codegen/msra_serializer.py +205 -0
- msra_codegen/node_export.js +296 -0
- msra_codegen/package_metadata.py +306 -0
- msra_codegen/package_writer.py +175 -0
- msra_codegen/project_model.py +490 -0
- msra_codegen/python_formatting.py +88 -0
- msra_codegen/python_render.py +242 -0
- msra_codegen/readme_pipeline.py +519 -0
- msra_codegen/requirements.txt +5 -0
- msra_codegen/template_engine.py +26 -0
- msra_codegen/templates/Makefile.tpl +44 -0
- msra_codegen/templates/README.md.tpl +55 -0
- msra_codegen/templates/abstraction/__init__.py.tpl +188 -0
- msra_codegen/templates/abstraction/regexes.py.tpl +25 -0
- msra_codegen/templates/docs/requirements.txt.tpl +3 -0
- msra_codegen/templates/docs/source/Makefile.tpl +20 -0
- msra_codegen/templates/docs/source/api.rst.tpl +9 -0
- msra_codegen/templates/docs/source/conf.py.tpl +88 -0
- msra_codegen/templates/docs/source/index.rst.tpl +14 -0
- msra_codegen/templates/docs/source/module.rst.tpl +34 -0
- msra_codegen/templates/docs/source/quick_start.rst.tpl +19 -0
- msra_codegen/templates/endpoints_init.py.tpl +15 -0
- msra_codegen/templates/example.py.tpl +1 -0
- msra_codegen/templates/function.py.tpl +364 -0
- msra_codegen/templates/github/issue_templates/bug_report.yml.tpl +55 -0
- msra_codegen/templates/github/issue_templates/config.yml.tpl +8 -0
- msra_codegen/templates/github/issue_templates/documentation_issue.yml.tpl +33 -0
- msra_codegen/templates/github/issue_templates/feature_request.yml.tpl +36 -0
- msra_codegen/templates/github/workflows/publish.yml.tpl +100 -0
- msra_codegen/templates/github/workflows/source-sync.yml.tpl +177 -0
- msra_codegen/templates/github/workflows/tests.yml.tpl +69 -0
- msra_codegen/templates/gitignore.tpl +3 -0
- msra_codegen/templates/group.py.tpl +56 -0
- msra_codegen/templates/group_init.py.tpl +14 -0
- msra_codegen/templates/init.py.tpl +4 -0
- msra_codegen/templates/licenses/GPL-3.0-or-later.txt.tpl +674 -0
- msra_codegen/templates/licenses/MIT.txt.tpl +21 -0
- msra_codegen/templates/manager.py.tpl +257 -0
- msra_codegen/templates/pyproject.toml.tpl +38 -0
- msra_codegen/templates/tests/api_test.py.tpl +49 -0
- msra_codegen/templates/tests/conftest.py.tpl +21 -0
- msra_codegen/templates/variable.py.tpl +54 -0
- msra_codegen/tests_generator.py +988 -0
- msra_codegen/typespec.py +275 -0
- msra_codegen/validation.py +118 -0
- msra_codegen-0.1.0.dist-info/METADATA +47 -0
- msra_codegen-0.1.0.dist-info/RECORD +68 -0
- msra_codegen-0.1.0.dist-info/WHEEL +5 -0
- msra_codegen-0.1.0.dist-info/entry_points.txt +2 -0
- msra_codegen-0.1.0.dist-info/licenses/LICENSE +674 -0
- msra_codegen-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .core_naming import group_path_from_expr, parse_script_reference
|
|
7
|
+
from .python_render import get_plain_value
|
|
8
|
+
from .typespec import extract_variable_match
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_group_tree(project: dict[str, Any]) -> dict[str, Any]:
|
|
12
|
+
root: dict[str, Any] = {"children": {}, "functions": [], "path": []}
|
|
13
|
+
for group in project.get("groups", []):
|
|
14
|
+
node = root
|
|
15
|
+
for segment in group.get("path", []):
|
|
16
|
+
node = node["children"].setdefault(
|
|
17
|
+
segment,
|
|
18
|
+
{
|
|
19
|
+
"name": segment,
|
|
20
|
+
"path": node["path"] + [segment],
|
|
21
|
+
"description": "",
|
|
22
|
+
"children": {},
|
|
23
|
+
"functions": [],
|
|
24
|
+
},
|
|
25
|
+
)
|
|
26
|
+
node["path"] = node["path"]
|
|
27
|
+
node["description"] = group.get("description", "")
|
|
28
|
+
for func in project.get("functions", []):
|
|
29
|
+
path = [part for part in str(func.get("group", "")).split(".") if part]
|
|
30
|
+
node = root
|
|
31
|
+
for segment in path:
|
|
32
|
+
node = node["children"].setdefault(
|
|
33
|
+
segment,
|
|
34
|
+
{
|
|
35
|
+
"name": segment,
|
|
36
|
+
"path": node["path"] + [segment],
|
|
37
|
+
"description": "",
|
|
38
|
+
"children": {},
|
|
39
|
+
"functions": [],
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
node["functions"].append(func)
|
|
43
|
+
return root
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def iter_group_nodes(tree: dict[str, Any]) -> list[dict[str, Any]]:
|
|
47
|
+
nodes: list[dict[str, Any]] = []
|
|
48
|
+
|
|
49
|
+
def walk(node: dict[str, Any]) -> None:
|
|
50
|
+
for child in node.get("children", {}).values():
|
|
51
|
+
nodes.append(child)
|
|
52
|
+
walk(child)
|
|
53
|
+
|
|
54
|
+
walk(tree)
|
|
55
|
+
return nodes
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def top_level_groups(tree: dict[str, Any]) -> list[dict[str, Any]]:
|
|
59
|
+
return list(tree.get("children", {}).values())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def build_project(ast: dict[str, Any], msra_path: Path) -> dict[str, Any]:
|
|
63
|
+
tables = [table for table in ast.get("tables", [])]
|
|
64
|
+
table_index: dict[tuple[str, ...], list[dict[str, Any]]] = {}
|
|
65
|
+
for table in tables:
|
|
66
|
+
table_index.setdefault(tuple(table["path"]), []).append(table)
|
|
67
|
+
examples_by_function: dict[str, list[dict[str, Any]]] = {}
|
|
68
|
+
|
|
69
|
+
def get_table(path: list[str] | tuple[str, ...]) -> dict[str, Any] | None:
|
|
70
|
+
entries = table_index.get(tuple(path))
|
|
71
|
+
if not entries:
|
|
72
|
+
return None
|
|
73
|
+
return entries[0]
|
|
74
|
+
|
|
75
|
+
def get_tables(path: list[str] | tuple[str, ...]) -> list[dict[str, Any]]:
|
|
76
|
+
return list(table_index.get(tuple(path), []))
|
|
77
|
+
|
|
78
|
+
def get_assignment_entry(table: dict[str, Any] | None, key: str) -> dict[str, Any] | None:
|
|
79
|
+
if not table:
|
|
80
|
+
return None
|
|
81
|
+
for assignment in table.get("assignments", []):
|
|
82
|
+
if assignment.get("key") == key:
|
|
83
|
+
return assignment
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def get_assignment(table: dict[str, Any] | None, key: str, default: Any = None) -> Any:
|
|
87
|
+
entry = get_assignment_entry(table, key)
|
|
88
|
+
if entry is None:
|
|
89
|
+
return default
|
|
90
|
+
return entry.get("value")
|
|
91
|
+
|
|
92
|
+
app_table = get_table(["app"])
|
|
93
|
+
app = {
|
|
94
|
+
"name": str(get_plain_value(get_assignment(app_table, "name", "GeneratedAPI"))),
|
|
95
|
+
"package_name": str(get_plain_value(get_assignment(app_table, "package_name", ""))),
|
|
96
|
+
"package_owner": str(get_plain_value(get_assignment(app_table, "package_owner", ""))),
|
|
97
|
+
"social": get_plain_value(get_assignment(app_table, "social", {})),
|
|
98
|
+
"authors": get_plain_value(get_assignment(app_table, "authors", [])),
|
|
99
|
+
"logo": str(get_plain_value(get_assignment(app_table, "logo", ""))).strip(),
|
|
100
|
+
"license": str(get_plain_value(get_assignment(app_table, "license", "MIT"))),
|
|
101
|
+
"keywords": get_plain_value(get_assignment(app_table, "keywords", [])),
|
|
102
|
+
"min_required_python": str(get_plain_value(get_assignment(app_table, "min_required_python", "3.10"))),
|
|
103
|
+
"description": str(get_plain_value(get_assignment(app_table, "description", ""))),
|
|
104
|
+
"version": str(get_plain_value(get_assignment(app_table, "version", "0.1.0"))),
|
|
105
|
+
"timeout_ms": int(get_plain_value(get_assignment(app_table, "timeout_ms", 35000))),
|
|
106
|
+
"browser": str(get_plain_value(get_assignment(app_table, "browser", "camoufox"))),
|
|
107
|
+
"humanize": get_plain_value(get_assignment(app_table, "humanize", False)),
|
|
108
|
+
"block_images": bool(get_plain_value(get_assignment(app_table, "block_images", False))),
|
|
109
|
+
"disallow_headless": bool(get_plain_value(get_assignment(app_table, "disallow_headless", False))),
|
|
110
|
+
"sync": {},
|
|
111
|
+
"abstractions": [],
|
|
112
|
+
}
|
|
113
|
+
abstractions_value = get_plain_value(get_assignment(app_table, "abstractions", []))
|
|
114
|
+
if not isinstance(abstractions_value, list):
|
|
115
|
+
raise TypeError("app.abstractions must be a list of strings.")
|
|
116
|
+
for item in abstractions_value:
|
|
117
|
+
if not isinstance(item, str):
|
|
118
|
+
raise TypeError("app.abstractions entries must be strings.")
|
|
119
|
+
text = item.strip()
|
|
120
|
+
if text:
|
|
121
|
+
app["abstractions"].append(text)
|
|
122
|
+
|
|
123
|
+
prefixes_table = get_table(["app", "prefixes"])
|
|
124
|
+
prefixes = {
|
|
125
|
+
assignment["key"]: get_plain_value(assignment["value"])
|
|
126
|
+
for assignment in (prefixes_table or {}).get("assignments", [])
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
regex_tables = [
|
|
130
|
+
table
|
|
131
|
+
for table in tables
|
|
132
|
+
if len(table["path"]) == 3 and table["path"][0] == "app" and table["path"][1] == "regexes"
|
|
133
|
+
]
|
|
134
|
+
regexes: list[dict[str, Any]] = []
|
|
135
|
+
for table in regex_tables:
|
|
136
|
+
regexes.append(
|
|
137
|
+
{
|
|
138
|
+
"name": table["path"][2],
|
|
139
|
+
"regex": get_plain_value(get_assignment(table, "regex", "")),
|
|
140
|
+
"raise": get_plain_value(get_assignment(table, "raise", "")),
|
|
141
|
+
"description": get_plain_value(get_assignment(table, "description", "")),
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
groups_tables = [
|
|
146
|
+
table
|
|
147
|
+
for table in tables
|
|
148
|
+
if len(table["path"]) >= 3 and table["path"][0] == "app" and table["path"][1] == "groups"
|
|
149
|
+
]
|
|
150
|
+
groups = []
|
|
151
|
+
for table in groups_tables:
|
|
152
|
+
groups.append(
|
|
153
|
+
{
|
|
154
|
+
"path": table["path"][2:],
|
|
155
|
+
"name": ".".join(table["path"][2:]),
|
|
156
|
+
"description": get_plain_value(get_assignment(table, "description", "")),
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
headers_table = get_table(["app", "defaults", "func", "headers"])
|
|
161
|
+
headers_spec = build_headers_spec(headers_table, get_assignment)
|
|
162
|
+
|
|
163
|
+
warmup_table = get_table(["app", "warmup"])
|
|
164
|
+
warmup_spec = None
|
|
165
|
+
if warmup_table:
|
|
166
|
+
warmup_script = parse_script_reference(get_assignment(warmup_table, "warmup"))
|
|
167
|
+
warmup_spec = {
|
|
168
|
+
"headers_sniffer": bool(get_plain_value(get_assignment(warmup_table, "headers_sniffer", False))),
|
|
169
|
+
"on_error_screenshot_path": get_plain_value(get_assignment(warmup_table, "on_error_screenshot_path", "")),
|
|
170
|
+
"script": warmup_script,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
sync_table = get_table(["app", "sync"])
|
|
174
|
+
if sync_table:
|
|
175
|
+
preserved_target_paths_value = get_plain_value(
|
|
176
|
+
get_assignment(sync_table, "preserved_target_paths", [])
|
|
177
|
+
)
|
|
178
|
+
if not isinstance(preserved_target_paths_value, list):
|
|
179
|
+
raise TypeError("app.sync.preserved_target_paths must be a list of strings.")
|
|
180
|
+
preserved_target_paths: list[str] = []
|
|
181
|
+
for item in preserved_target_paths_value:
|
|
182
|
+
if not isinstance(item, str):
|
|
183
|
+
raise TypeError("app.sync.preserved_target_paths entries must be strings.")
|
|
184
|
+
text = item.strip()
|
|
185
|
+
if text:
|
|
186
|
+
preserved_target_paths.append(text)
|
|
187
|
+
ignored_generated_patterns_value = get_plain_value(
|
|
188
|
+
get_assignment(sync_table, "ignored_generated_patterns", [])
|
|
189
|
+
)
|
|
190
|
+
if not isinstance(ignored_generated_patterns_value, list):
|
|
191
|
+
raise TypeError("app.sync.ignored_generated_patterns must be a list of strings.")
|
|
192
|
+
ignored_generated_patterns: list[str] = []
|
|
193
|
+
for item in ignored_generated_patterns_value:
|
|
194
|
+
if not isinstance(item, str):
|
|
195
|
+
raise TypeError("app.sync.ignored_generated_patterns entries must be strings.")
|
|
196
|
+
text = item.strip()
|
|
197
|
+
if text:
|
|
198
|
+
ignored_generated_patterns.append(text)
|
|
199
|
+
app["sync"] = {
|
|
200
|
+
"preserved_target_paths": preserved_target_paths,
|
|
201
|
+
"ignored_generated_patterns": ignored_generated_patterns,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
issue_templates_table = get_table(["app", "issue_templates"])
|
|
205
|
+
issue_templates_child_tables = [
|
|
206
|
+
table
|
|
207
|
+
for table in tables
|
|
208
|
+
if len(table["path"]) >= 3 and table["path"][0] == "app" and table["path"][1] == "issue_templates"
|
|
209
|
+
and len(table["path"]) > 2
|
|
210
|
+
]
|
|
211
|
+
if issue_templates_child_tables:
|
|
212
|
+
raise RuntimeError("app.issue_templates only supports the assignee field and no child tables.")
|
|
213
|
+
if issue_templates_table:
|
|
214
|
+
app["issue_templates"] = build_issue_templates_spec(issue_templates_table, get_assignment)
|
|
215
|
+
|
|
216
|
+
variable_tables = [
|
|
217
|
+
table
|
|
218
|
+
for table in tables
|
|
219
|
+
if len(table["path"]) == 3 and table["path"][0] == "app" and table["path"][1] == "variables"
|
|
220
|
+
]
|
|
221
|
+
variables = []
|
|
222
|
+
for table in variable_tables:
|
|
223
|
+
types_expr = get_assignment(table, "types")
|
|
224
|
+
variables.append(
|
|
225
|
+
{
|
|
226
|
+
"name": table["path"][2],
|
|
227
|
+
"types": types_expr,
|
|
228
|
+
"match": extract_variable_match(types_expr),
|
|
229
|
+
"read_only": bool(get_plain_value(get_assignment(table, "read_only", False))),
|
|
230
|
+
"nullable": bool(get_plain_value(get_assignment(table, "nullable", False))),
|
|
231
|
+
"from": get_assignment(table, "from"),
|
|
232
|
+
"description": get_plain_value(get_assignment(table, "description", "")),
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
functions: list[dict[str, Any]] = []
|
|
237
|
+
for table in tables:
|
|
238
|
+
path = table["path"]
|
|
239
|
+
if len(path) == 5 and path[0] == "app" and path[1] == "func" and path[3] == "examples":
|
|
240
|
+
func_id = path[2]
|
|
241
|
+
docs_assignment = get_assignment_entry(table, "docs")
|
|
242
|
+
examples_by_function.setdefault(func_id, []).append(
|
|
243
|
+
{
|
|
244
|
+
"name": path[4],
|
|
245
|
+
"docs": bool(docs_assignment and bool(get_plain_value(docs_assignment.get("value")))),
|
|
246
|
+
"test": bool(get_plain_value(get_assignment(table, "test", False))),
|
|
247
|
+
"type": str(get_plain_value(get_assignment(table, "type", "json"))).strip().lower(),
|
|
248
|
+
"description": str(get_plain_value(get_assignment(table, "description", ""))),
|
|
249
|
+
"inputs": get_assignment(table, "inputs"),
|
|
250
|
+
"print": extract_docs_print_value(docs_assignment),
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
continue
|
|
254
|
+
if len(path) != 3 or path[0] != "app" or path[1] != "func":
|
|
255
|
+
continue
|
|
256
|
+
func_id = path[2]
|
|
257
|
+
|
|
258
|
+
root = table
|
|
259
|
+
group = group_path_from_expr(get_assignment(root, "group", ""))
|
|
260
|
+
transport = str(get_plain_value(get_assignment(root, "transport", "fetch")))
|
|
261
|
+
method = str(get_plain_value(get_assignment(root, "method", "GET")))
|
|
262
|
+
functions.append(
|
|
263
|
+
{
|
|
264
|
+
"id": func_id,
|
|
265
|
+
"name": str(get_plain_value(get_assignment(root, "name", func_id.lower()))),
|
|
266
|
+
"group": group,
|
|
267
|
+
"transport": transport,
|
|
268
|
+
"method": method,
|
|
269
|
+
"description": str(get_plain_value(get_assignment(root, "description", ""))),
|
|
270
|
+
"root_table": root,
|
|
271
|
+
"inputs": [],
|
|
272
|
+
"url": None,
|
|
273
|
+
"body": None,
|
|
274
|
+
"headers": None,
|
|
275
|
+
"extractor": None,
|
|
276
|
+
"examples": examples_by_function.get(func_id, []),
|
|
277
|
+
}
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
for func in functions:
|
|
281
|
+
func_id = func["id"]
|
|
282
|
+
prefix = ["app", "func", func_id]
|
|
283
|
+
input_tables = [
|
|
284
|
+
table
|
|
285
|
+
for table in tables
|
|
286
|
+
if len(table["path"]) == 5
|
|
287
|
+
and table["path"][:4] == prefix + ["input"]
|
|
288
|
+
]
|
|
289
|
+
function_overload_tables = [
|
|
290
|
+
table
|
|
291
|
+
for table in tables
|
|
292
|
+
if len(table["path"]) == 5
|
|
293
|
+
and table["path"][:4] == prefix + ["overload"]
|
|
294
|
+
]
|
|
295
|
+
overload_names = [str(table["path"][-1]) for table in function_overload_tables]
|
|
296
|
+
|
|
297
|
+
input_contexts = []
|
|
298
|
+
for table in input_tables:
|
|
299
|
+
input_name = str(table["path"][-1])
|
|
300
|
+
input_overload_tables = [
|
|
301
|
+
overload_table
|
|
302
|
+
for overload_table in tables
|
|
303
|
+
if len(overload_table["path"]) == 7
|
|
304
|
+
and overload_table["path"][:5] == prefix + ["input", input_name]
|
|
305
|
+
and overload_table["path"][5] == "overload"
|
|
306
|
+
]
|
|
307
|
+
overload_specs = {
|
|
308
|
+
str(overload_table["path"][-1]): build_input_spec(
|
|
309
|
+
overload_table,
|
|
310
|
+
get_assignment,
|
|
311
|
+
get_assignment_entry,
|
|
312
|
+
explicit_only=True,
|
|
313
|
+
)
|
|
314
|
+
for overload_table in input_overload_tables
|
|
315
|
+
}
|
|
316
|
+
overload_names.extend(name for name in overload_specs.keys() if name not in overload_names)
|
|
317
|
+
input_context = build_input_spec(table, get_assignment, get_assignment_entry)
|
|
318
|
+
input_context["overloads"] = overload_specs
|
|
319
|
+
input_contexts.append(input_context)
|
|
320
|
+
func["inputs"] = input_contexts
|
|
321
|
+
func["overload_names"] = list(dict.fromkeys(overload_names))
|
|
322
|
+
|
|
323
|
+
url_tables = get_tables(prefix + ["url"])
|
|
324
|
+
if url_tables:
|
|
325
|
+
param_tables = [
|
|
326
|
+
table
|
|
327
|
+
for table in tables
|
|
328
|
+
if len(table["path"]) == 6
|
|
329
|
+
and table["path"][:5] == prefix + ["url", "params"]
|
|
330
|
+
]
|
|
331
|
+
url_entries = []
|
|
332
|
+
for url_table in url_tables:
|
|
333
|
+
url_entries.append(
|
|
334
|
+
{
|
|
335
|
+
"base": get_assignment(url_table, "base"),
|
|
336
|
+
"priority": get_plain_value(get_assignment(url_table, "priority", 0)),
|
|
337
|
+
"wants": get_assignment(url_table, "wants"),
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
func["url"] = {
|
|
341
|
+
"base": url_entries[0]["base"] if url_entries else None,
|
|
342
|
+
"entries": url_entries,
|
|
343
|
+
"params": [build_url_param_spec(table, get_assignment) for table in param_tables],
|
|
344
|
+
}
|
|
345
|
+
else:
|
|
346
|
+
func["url"] = None
|
|
347
|
+
|
|
348
|
+
body_table = get_table(prefix + ["body"])
|
|
349
|
+
if body_table:
|
|
350
|
+
func["body"] = {
|
|
351
|
+
"type": str(get_plain_value(get_assignment(body_table, "type", "application/json"))),
|
|
352
|
+
"from": get_assignment(body_table, "from"),
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
func_headers_table = get_table(prefix + ["headers"])
|
|
356
|
+
if func_headers_table:
|
|
357
|
+
func["headers"] = build_headers_spec(func_headers_table, get_assignment)
|
|
358
|
+
|
|
359
|
+
extractor_table = get_table(prefix + ["extractor"])
|
|
360
|
+
if extractor_table:
|
|
361
|
+
goto_pipeline = parse_script_reference(get_assignment(extractor_table, "goto_pipeline"))
|
|
362
|
+
func["extractor"] = {
|
|
363
|
+
"render_html": bool(get_plain_value(get_assignment(extractor_table, "render_html", False))),
|
|
364
|
+
"script": get_plain_value(get_assignment(extractor_table, "script", "")),
|
|
365
|
+
"goto_pipeline": goto_pipeline,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for func in functions:
|
|
369
|
+
func["examples"] = examples_by_function.get(func["id"], [])
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
"source_path": str(msra_path.resolve()),
|
|
373
|
+
"app": app,
|
|
374
|
+
"prefixes": prefixes,
|
|
375
|
+
"regexes": regexes,
|
|
376
|
+
"groups": groups,
|
|
377
|
+
"variables": variables,
|
|
378
|
+
"headers": headers_spec,
|
|
379
|
+
"warmup": warmup_spec,
|
|
380
|
+
"functions": functions,
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def extract_docs_print_value(docs_assignment: dict[str, Any] | None) -> Any:
|
|
385
|
+
if not docs_assignment or not bool(docs_assignment.get("annotation")):
|
|
386
|
+
return None
|
|
387
|
+
if str(docs_assignment.get("annotationName") or "").lower() != "docs":
|
|
388
|
+
return None
|
|
389
|
+
args = docs_assignment.get("annotationArgs")
|
|
390
|
+
if not isinstance(args, list):
|
|
391
|
+
return None
|
|
392
|
+
for arg in args:
|
|
393
|
+
if isinstance(arg, dict) and str(arg.get("name") or "") == "print":
|
|
394
|
+
return arg.get("value")
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def build_input_spec(
|
|
399
|
+
table: dict[str, Any],
|
|
400
|
+
get_assignment,
|
|
401
|
+
get_assignment_entry=None,
|
|
402
|
+
*,
|
|
403
|
+
explicit_only: bool = False,
|
|
404
|
+
) -> dict[str, Any]:
|
|
405
|
+
spec: dict[str, Any] = {
|
|
406
|
+
"name": table["path"][-1],
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
def assignment_value(key: str, default: Any = None) -> Any:
|
|
410
|
+
if get_assignment_entry is None:
|
|
411
|
+
if explicit_only:
|
|
412
|
+
return default
|
|
413
|
+
return get_assignment(table, key, default)
|
|
414
|
+
entry = get_assignment_entry(table, key)
|
|
415
|
+
if entry is None:
|
|
416
|
+
if explicit_only:
|
|
417
|
+
return None
|
|
418
|
+
return get_assignment(table, key, default)
|
|
419
|
+
return entry.get("value")
|
|
420
|
+
|
|
421
|
+
def include(key: str, value: Any) -> None:
|
|
422
|
+
if explicit_only and value is None:
|
|
423
|
+
return
|
|
424
|
+
spec[key] = value
|
|
425
|
+
|
|
426
|
+
include("type", assignment_value("type"))
|
|
427
|
+
include("default", assignment_value("default"))
|
|
428
|
+
if not explicit_only:
|
|
429
|
+
include("required", bool(get_plain_value(assignment_value("required", False))))
|
|
430
|
+
else:
|
|
431
|
+
required_value = assignment_value("required")
|
|
432
|
+
if required_value is not None:
|
|
433
|
+
include("required", bool(get_plain_value(required_value)))
|
|
434
|
+
include("const", assignment_value("const"))
|
|
435
|
+
include("values", assignment_value("values"))
|
|
436
|
+
include("match", assignment_value("match"))
|
|
437
|
+
if not explicit_only:
|
|
438
|
+
include("read_only", bool(get_plain_value(assignment_value("read_only", False))))
|
|
439
|
+
else:
|
|
440
|
+
read_only_value = assignment_value("read_only")
|
|
441
|
+
if read_only_value is not None:
|
|
442
|
+
include("read_only", bool(get_plain_value(read_only_value)))
|
|
443
|
+
include("from", assignment_value("from"))
|
|
444
|
+
description_value = assignment_value("description")
|
|
445
|
+
if description_value is None:
|
|
446
|
+
if not explicit_only:
|
|
447
|
+
spec["description"] = ""
|
|
448
|
+
else:
|
|
449
|
+
spec["description"] = str(get_plain_value(description_value))
|
|
450
|
+
return spec
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def build_headers_spec(table: dict[str, Any] | None, get_assignment) -> dict[str, Any] | None:
|
|
454
|
+
if not table:
|
|
455
|
+
return None
|
|
456
|
+
return {
|
|
457
|
+
"referrer": get_assignment(table, "referrer"),
|
|
458
|
+
"cors_mode": get_assignment(table, "cors_mode"),
|
|
459
|
+
"credentials": get_assignment(table, "credentials"),
|
|
460
|
+
"headers": get_assignment(table, "headers"),
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def build_issue_templates_spec(root_table: dict[str, Any], get_assignment) -> dict[str, Any]:
|
|
465
|
+
assignee = str(get_plain_value(get_assignment(root_table, "assignee", ""))).strip()
|
|
466
|
+
if not assignee:
|
|
467
|
+
raise RuntimeError('app.issue_templates.assignee is required and must be a non-empty string.')
|
|
468
|
+
return {
|
|
469
|
+
"assignee": assignee,
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def build_url_param_spec(table: dict[str, Any], get_assignment) -> dict[str, Any]:
|
|
474
|
+
list_style = get_plain_value(get_assignment(table, "list_style"))
|
|
475
|
+
if not isinstance(list_style, dict):
|
|
476
|
+
list_style = {}
|
|
477
|
+
return {
|
|
478
|
+
"name": table["path"][-1],
|
|
479
|
+
"sub_url": bool(get_plain_value(get_assignment(table, "sub_url", False))),
|
|
480
|
+
"list": bool(get_plain_value(get_assignment(table, "list", False))),
|
|
481
|
+
"list_style": {
|
|
482
|
+
"style": str(list_style.get("style", "repeat") or "repeat").strip().lower(),
|
|
483
|
+
"delimiter": str(list_style.get("delimiter", ",") or ","),
|
|
484
|
+
"indexed": bool(list_style.get("indexed", False)),
|
|
485
|
+
},
|
|
486
|
+
"from": get_assignment(table, "from"),
|
|
487
|
+
"const": get_assignment(table, "const"),
|
|
488
|
+
"values": get_assignment(table, "values"),
|
|
489
|
+
"description": str(get_plain_value(get_assignment(table, "description", ""))),
|
|
490
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Iterable
|
|
8
|
+
|
|
9
|
+
from .generator_config import config_section
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_python_line_length() -> int:
|
|
13
|
+
ruff_config = config_section("ruff")
|
|
14
|
+
value = ruff_config.get("line_length")
|
|
15
|
+
if not isinstance(value, int):
|
|
16
|
+
raise RuntimeError("ruff.line_length must be an integer.")
|
|
17
|
+
return value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def format_python_source(source: str, *, line_length: int | None = None) -> str:
|
|
21
|
+
with tempfile.TemporaryDirectory(prefix="msra-python-format-") as tmp_dir:
|
|
22
|
+
temp_path = Path(tmp_dir) / "snippet.py"
|
|
23
|
+
temp_path.write_text(source, encoding="utf-8")
|
|
24
|
+
format_python_files([temp_path], line_length=line_length)
|
|
25
|
+
return temp_path.read_text(encoding="utf-8")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def format_python_files(paths: Iterable[Path], *, line_length: int | None = None) -> None:
|
|
29
|
+
path_list = [Path(path).resolve() for path in paths]
|
|
30
|
+
if not path_list:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
effective_line_length = line_length if line_length is not None else get_python_line_length()
|
|
34
|
+
file_args = [str(path) for path in path_list]
|
|
35
|
+
run_ruff_tool(
|
|
36
|
+
[
|
|
37
|
+
"-m",
|
|
38
|
+
"ruff",
|
|
39
|
+
"check",
|
|
40
|
+
"--select",
|
|
41
|
+
"I",
|
|
42
|
+
"--fix",
|
|
43
|
+
"--line-length",
|
|
44
|
+
str(effective_line_length),
|
|
45
|
+
*file_args,
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
run_ruff_tool(
|
|
49
|
+
[
|
|
50
|
+
"-m",
|
|
51
|
+
"ruff",
|
|
52
|
+
"format",
|
|
53
|
+
"--line-length",
|
|
54
|
+
str(effective_line_length),
|
|
55
|
+
*file_args,
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def format_python_tree(root: Path, *, line_length: int | None = None) -> None:
|
|
61
|
+
python_files = [
|
|
62
|
+
path
|
|
63
|
+
for path in root.rglob("*.py")
|
|
64
|
+
if "__pycache__" not in path.parts and path.is_file()
|
|
65
|
+
]
|
|
66
|
+
format_python_files(python_files, line_length=line_length)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def run_ruff_tool(arguments: list[str]) -> None:
|
|
70
|
+
process = subprocess.run(
|
|
71
|
+
[sys.executable, *arguments],
|
|
72
|
+
capture_output=True,
|
|
73
|
+
text=True,
|
|
74
|
+
encoding="utf-8",
|
|
75
|
+
errors="replace",
|
|
76
|
+
check=False,
|
|
77
|
+
)
|
|
78
|
+
if process.returncode == 0:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
stderr = process.stderr.strip()
|
|
82
|
+
stdout = process.stdout.strip()
|
|
83
|
+
details = stderr or stdout or "ruff formatting command failed without output"
|
|
84
|
+
raise RuntimeError(
|
|
85
|
+
f"Formatting command failed with exit code {process.returncode}.\n"
|
|
86
|
+
f"Command: {sys.executable} {' '.join(arguments)}\n"
|
|
87
|
+
f"{details}"
|
|
88
|
+
)
|