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,519 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .core_naming import snake_case
|
|
7
|
+
from .python_render import render_expr
|
|
8
|
+
|
|
9
|
+
FUNCRESULT_RESULT_TYPES = {"JSON", "TEXT", "IMAGE"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_readme_pipeline_note(project: dict[str, Any]) -> str | None:
|
|
13
|
+
if any(not func.get("examples") for func in project.get("functions", [])):
|
|
14
|
+
return (
|
|
15
|
+
"Functions without an `examples` block are omitted from the auto-generated "
|
|
16
|
+
"pipeline because the generator has no concrete inputs to replay."
|
|
17
|
+
)
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_readme_pipeline_code(project: dict[str, Any], package_name: str, client_class_name: str) -> str:
|
|
22
|
+
functions = [func for func in project.get("functions", []) if func.get("examples")]
|
|
23
|
+
if not functions:
|
|
24
|
+
return "\n".join(
|
|
25
|
+
[
|
|
26
|
+
"import asyncio",
|
|
27
|
+
f"from {package_name} import {client_class_name}",
|
|
28
|
+
"",
|
|
29
|
+
"async def main():",
|
|
30
|
+
f" async with {client_class_name}() as api:",
|
|
31
|
+
" assert api is not None",
|
|
32
|
+
"",
|
|
33
|
+
'if __name__ == "__main__":',
|
|
34
|
+
" asyncio.run(main())",
|
|
35
|
+
]
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
selected_examples_by_function = select_examples_by_function(functions)
|
|
39
|
+
selected_nodes = build_selected_example_nodes(functions, selected_examples_by_function)
|
|
40
|
+
node_order = [node["key"] for node in selected_nodes]
|
|
41
|
+
order_index = {node_key: index for index, node_key in enumerate(node_order)}
|
|
42
|
+
selected_node_keys = set(node_order)
|
|
43
|
+
dependencies: dict[str, set[str]] = {}
|
|
44
|
+
for node in selected_nodes:
|
|
45
|
+
deps = collect_funcresult_dependencies([node["example"].get("inputs"), node["example"].get("print")])
|
|
46
|
+
dependencies[node["key"]] = {dep for dep in deps if dep in selected_node_keys and dep != node["key"]}
|
|
47
|
+
|
|
48
|
+
referenced_node_keys = {dependency_key for deps in dependencies.values() for dependency_key in deps}
|
|
49
|
+
self_print_referenced_node_keys = {
|
|
50
|
+
node["key"]
|
|
51
|
+
for node in selected_nodes
|
|
52
|
+
if node["key"] in collect_funcresult_dependencies(node["example"].get("print"))
|
|
53
|
+
}
|
|
54
|
+
ordered_keys = topologically_order_functions(node_order, dependencies, order_index)
|
|
55
|
+
nodes_by_key = {node["key"]: node for node in selected_nodes}
|
|
56
|
+
output_var_names: dict[str, str] = {}
|
|
57
|
+
body_lines: list[str] = []
|
|
58
|
+
current_group: str | None = None
|
|
59
|
+
for node_key in ordered_keys:
|
|
60
|
+
node = nodes_by_key[node_key]
|
|
61
|
+
func = node["function"]
|
|
62
|
+
example = node["example"]
|
|
63
|
+
function_id = node["function_id"]
|
|
64
|
+
example_name = node["example_name"]
|
|
65
|
+
example_type = normalize_example_type(example)
|
|
66
|
+
group_name = str(func.get("group") or "").split(".", 1)[0] or "Ungrouped"
|
|
67
|
+
if group_name != current_group:
|
|
68
|
+
current_group = group_name
|
|
69
|
+
if body_lines:
|
|
70
|
+
body_lines.append("")
|
|
71
|
+
body_lines.append(f" # {group_name}")
|
|
72
|
+
|
|
73
|
+
comment_text = normalize_readme_comment(example.get("description")) if example.get("description") else ""
|
|
74
|
+
if comment_text:
|
|
75
|
+
body_lines.append(f" # {comment_text}")
|
|
76
|
+
|
|
77
|
+
call_path = build_readme_call_path(func)
|
|
78
|
+
output_var = readme_output_var_name(example_name)
|
|
79
|
+
assignment_var = output_var
|
|
80
|
+
call_args = build_readme_call_args(example.get("inputs"), output_var_names)
|
|
81
|
+
response_suffix = ".json()"
|
|
82
|
+
if example_type == "text":
|
|
83
|
+
response_suffix = ".text"
|
|
84
|
+
elif example_type == "image":
|
|
85
|
+
response_suffix = ".image()"
|
|
86
|
+
body_lines.append(
|
|
87
|
+
f" {assignment_var} = (await {call_path}({call_args})){response_suffix}"
|
|
88
|
+
if call_args
|
|
89
|
+
else f" {assignment_var} = (await {call_path}()){response_suffix}"
|
|
90
|
+
)
|
|
91
|
+
if node_key not in referenced_node_keys and node_key not in self_print_referenced_node_keys:
|
|
92
|
+
body_lines.append(f" _ = {output_var}")
|
|
93
|
+
output_var_names[node_key] = output_var
|
|
94
|
+
for print_line in render_readme_print_lines(example.get("print"), output_var_names):
|
|
95
|
+
body_lines.append(print_line)
|
|
96
|
+
|
|
97
|
+
lines = ["import asyncio"]
|
|
98
|
+
lines.extend(
|
|
99
|
+
[
|
|
100
|
+
f"from {package_name} import {client_class_name}",
|
|
101
|
+
"",
|
|
102
|
+
"async def main():",
|
|
103
|
+
f" async with {client_class_name}() as api:",
|
|
104
|
+
" assert api is not None",
|
|
105
|
+
]
|
|
106
|
+
)
|
|
107
|
+
if body_lines:
|
|
108
|
+
lines.extend(body_lines)
|
|
109
|
+
lines.extend(
|
|
110
|
+
[
|
|
111
|
+
"",
|
|
112
|
+
'if __name__ == "__main__":',
|
|
113
|
+
" asyncio.run(main())",
|
|
114
|
+
]
|
|
115
|
+
)
|
|
116
|
+
return "\n".join(lines)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def select_examples_by_function(
|
|
120
|
+
functions: list[dict[str, Any]],
|
|
121
|
+
) -> dict[str, list[str]]:
|
|
122
|
+
examples_by_function = collect_examples_by_function(functions)
|
|
123
|
+
selected: dict[str, set[str]] = {}
|
|
124
|
+
for func in functions:
|
|
125
|
+
function_id = str(func.get("id") or "")
|
|
126
|
+
examples = func.get("examples", [])
|
|
127
|
+
selected_names = select_initial_example_names(examples)
|
|
128
|
+
selected[function_id] = set(selected_names)
|
|
129
|
+
|
|
130
|
+
selected_ordered: dict[str, list[str]] = {}
|
|
131
|
+
for func in functions:
|
|
132
|
+
function_id = str(func.get("id") or "")
|
|
133
|
+
examples = func.get("examples", [])
|
|
134
|
+
selected_names = selected.get(function_id, set())
|
|
135
|
+
selected_ordered[function_id] = [
|
|
136
|
+
str(example.get("name"))
|
|
137
|
+
for example in examples
|
|
138
|
+
if str(example.get("name")) in selected_names
|
|
139
|
+
]
|
|
140
|
+
return selected_ordered
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def select_initial_example_names(examples: list[dict[str, Any]]) -> set[str]:
|
|
144
|
+
if not examples:
|
|
145
|
+
return set()
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
str(example.get("name"))
|
|
149
|
+
for example in examples
|
|
150
|
+
if bool(example.get("docs"))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def build_selected_example_nodes(
|
|
155
|
+
functions: list[dict[str, Any]],
|
|
156
|
+
selected_examples_by_function: dict[str, list[str]],
|
|
157
|
+
) -> list[dict[str, Any]]:
|
|
158
|
+
nodes: list[dict[str, Any]] = []
|
|
159
|
+
for func in functions:
|
|
160
|
+
function_id = str(func.get("id") or "")
|
|
161
|
+
examples = {str(example.get("name")): example for example in func.get("examples", [])}
|
|
162
|
+
for example_name in selected_examples_by_function.get(function_id, []):
|
|
163
|
+
example = examples.get(example_name)
|
|
164
|
+
if example is None:
|
|
165
|
+
continue
|
|
166
|
+
nodes.append(
|
|
167
|
+
{
|
|
168
|
+
"key": readme_example_key(function_id, example_name),
|
|
169
|
+
"function_id": function_id,
|
|
170
|
+
"function": func,
|
|
171
|
+
"example_name": example_name,
|
|
172
|
+
"example": example,
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
return nodes
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def collect_examples_by_function(functions: list[dict[str, Any]]) -> dict[str, dict[str, dict[str, Any]]]:
|
|
179
|
+
result: dict[str, dict[str, dict[str, Any]]] = {}
|
|
180
|
+
for func in functions:
|
|
181
|
+
function_id = str(func.get("id") or "")
|
|
182
|
+
examples = result.setdefault(function_id, {})
|
|
183
|
+
for example in func.get("examples", []):
|
|
184
|
+
example_name = str(example.get("name") or "")
|
|
185
|
+
if not example_name:
|
|
186
|
+
continue
|
|
187
|
+
examples[example_name] = example
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def readme_example_key(function_id: str, example_name: str) -> str:
|
|
192
|
+
return f"{function_id}::{example_name}"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def normalize_example_type(example: dict[str, Any] | None) -> str:
|
|
196
|
+
if not example:
|
|
197
|
+
return "json"
|
|
198
|
+
value = str(example.get("type") or "json").strip().lower()
|
|
199
|
+
if value not in {"text", "json", "image"}:
|
|
200
|
+
return "json"
|
|
201
|
+
return value
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def topologically_order_functions(
|
|
205
|
+
function_order: list[str],
|
|
206
|
+
dependencies: dict[str, set[str]],
|
|
207
|
+
order_index: dict[str, int],
|
|
208
|
+
) -> list[str]:
|
|
209
|
+
pending = {func_id: set(dependencies.get(func_id, set())) for func_id in function_order}
|
|
210
|
+
dependents: dict[str, set[str]] = {func_id: set() for func_id in function_order}
|
|
211
|
+
for func_id, deps in pending.items():
|
|
212
|
+
for dep in deps:
|
|
213
|
+
dependents.setdefault(dep, set()).add(func_id)
|
|
214
|
+
|
|
215
|
+
ready = sorted([func_id for func_id, deps in pending.items() if not deps], key=order_index.__getitem__)
|
|
216
|
+
ordered: list[str] = []
|
|
217
|
+
while ready:
|
|
218
|
+
func_id = ready.pop(0)
|
|
219
|
+
ordered.append(func_id)
|
|
220
|
+
for child in sorted(dependents.get(func_id, set()), key=order_index.__getitem__):
|
|
221
|
+
child_deps = pending.get(child)
|
|
222
|
+
if child_deps is None:
|
|
223
|
+
continue
|
|
224
|
+
child_deps.discard(func_id)
|
|
225
|
+
if not child_deps and child not in ordered and child not in ready:
|
|
226
|
+
insert_at = 0
|
|
227
|
+
while insert_at < len(ready) and order_index[ready[insert_at]] < order_index[child]:
|
|
228
|
+
insert_at += 1
|
|
229
|
+
ready.insert(insert_at, child)
|
|
230
|
+
|
|
231
|
+
for func_id in function_order:
|
|
232
|
+
if func_id not in ordered:
|
|
233
|
+
ordered.append(func_id)
|
|
234
|
+
return ordered
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def build_readme_call_path(func: dict[str, Any]) -> str:
|
|
238
|
+
group = str(func.get("group") or "").strip()
|
|
239
|
+
method_name = str(func.get("name") or func["id"])
|
|
240
|
+
if not group:
|
|
241
|
+
return f"api.{method_name}"
|
|
242
|
+
return "api." + ".".join(segment for segment in group.split(".") if segment) + f".{method_name}"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def readme_output_var_name(example_name: str) -> str:
|
|
246
|
+
return snake_case(str(example_name))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def build_readme_call_args(inputs_expr: dict[str, Any] | None, output_var_names: dict[str, str]) -> str:
|
|
250
|
+
items = inline_table_items(inputs_expr)
|
|
251
|
+
if not items:
|
|
252
|
+
return ""
|
|
253
|
+
return ", ".join(
|
|
254
|
+
f"{item['key']}={render_readme_expr(item['value'], output_var_names)}"
|
|
255
|
+
for item in items
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def inline_table_items(expr: dict[str, Any] | None) -> list[dict[str, Any]]:
|
|
260
|
+
if not isinstance(expr, dict) or expr.get("kind") != "inline_table":
|
|
261
|
+
return []
|
|
262
|
+
return list(expr.get("items", []))
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def normalize_readme_comment(text: Any) -> str:
|
|
266
|
+
return re.sub(r"\s+", " ", str(text)).strip()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def parse_funcresult_reference(expr: dict[str, Any]) -> tuple[str, str, str, list[dict[str, Any]]]:
|
|
270
|
+
parts = expr.get("parts", [])
|
|
271
|
+
if len(parts) < 4 or parts[0].get("kind") != "name" or str(parts[0].get("value")) != "FUNCRESULT":
|
|
272
|
+
raise ValueError("FUNCRESULT references must use the form <FUNCRESULT.<function>.<example>.<kind>>.")
|
|
273
|
+
|
|
274
|
+
function_part = parts[1]
|
|
275
|
+
example_part = parts[2]
|
|
276
|
+
result_part = parts[3]
|
|
277
|
+
if function_part.get("kind") != "name":
|
|
278
|
+
raise ValueError("FUNCRESULT references must name the source function immediately after FUNCRESULT.")
|
|
279
|
+
if example_part.get("kind") != "name":
|
|
280
|
+
raise ValueError("FUNCRESULT references must name the source example before the result kind, for example <FUNCRESULT.<function>.<example>.<kind>>.")
|
|
281
|
+
if result_part.get("kind") != "name":
|
|
282
|
+
raise ValueError("FUNCRESULT references must include a result kind after the source example: JSON, TEXT, or IMAGE.")
|
|
283
|
+
|
|
284
|
+
result_kind = str(result_part.get("value"))
|
|
285
|
+
if result_kind not in FUNCRESULT_RESULT_TYPES:
|
|
286
|
+
raise ValueError("FUNCRESULT references must use the result kind JSON, TEXT, or IMAGE.")
|
|
287
|
+
|
|
288
|
+
function_id = str(function_part.get("value"))
|
|
289
|
+
example_name = str(example_part.get("value"))
|
|
290
|
+
for tail_part in parts[4:]:
|
|
291
|
+
if tail_part.get("kind") != "key":
|
|
292
|
+
continue
|
|
293
|
+
validate_readme_key_selector(tail_part.get("value"), function_id, example_name)
|
|
294
|
+
if result_kind != "JSON" and len(parts) > 4:
|
|
295
|
+
raise ValueError(
|
|
296
|
+
f"FUNCRESULT.{function_id}.{example_name}.{result_kind} does not allow further path access. Use JSON if you need to address nested elements.",
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return function_id, example_name, result_kind, parts[4:]
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def collect_funcresult_dependencies(expr: Any) -> set[str]:
|
|
303
|
+
dependencies: set[str] = set()
|
|
304
|
+
|
|
305
|
+
def walk(node: Any) -> None:
|
|
306
|
+
if isinstance(node, list):
|
|
307
|
+
for item in node:
|
|
308
|
+
walk(item)
|
|
309
|
+
return
|
|
310
|
+
if not isinstance(node, dict):
|
|
311
|
+
return
|
|
312
|
+
kind = node.get("kind")
|
|
313
|
+
if kind == "ref":
|
|
314
|
+
parts = node.get("parts", [])
|
|
315
|
+
if parts and parts[0].get("kind") == "name" and str(parts[0].get("value")) == "FUNCRESULT":
|
|
316
|
+
function_id, example_name, _result_kind, _tail_parts = parse_funcresult_reference(node)
|
|
317
|
+
dependencies.add(readme_example_key(function_id, example_name))
|
|
318
|
+
for part in parts:
|
|
319
|
+
walk(part.get("value"))
|
|
320
|
+
return
|
|
321
|
+
if kind == "inline_table":
|
|
322
|
+
for item in node.get("items", []):
|
|
323
|
+
walk(item.get("value"))
|
|
324
|
+
return
|
|
325
|
+
if kind == "array":
|
|
326
|
+
for item in node.get("items", []):
|
|
327
|
+
walk(item)
|
|
328
|
+
return
|
|
329
|
+
if kind == "sequence":
|
|
330
|
+
for item in node.get("items", []):
|
|
331
|
+
walk(item)
|
|
332
|
+
return
|
|
333
|
+
if kind == "merge":
|
|
334
|
+
for part in node.get("parts", []):
|
|
335
|
+
walk(part)
|
|
336
|
+
return
|
|
337
|
+
if kind == "call":
|
|
338
|
+
walk(node.get("callee"))
|
|
339
|
+
for arg in node.get("args", []):
|
|
340
|
+
walk(arg.get("value"))
|
|
341
|
+
return
|
|
342
|
+
if kind == "index":
|
|
343
|
+
walk(node.get("value"))
|
|
344
|
+
walk(node.get("index"))
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
walk(expr)
|
|
348
|
+
return dependencies
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def render_readme_expr(expr: Any, output_var_names: dict[str, str]) -> str:
|
|
352
|
+
if expr is None:
|
|
353
|
+
return "None"
|
|
354
|
+
if not isinstance(expr, dict):
|
|
355
|
+
return render_expr(expr, self_ref="api")
|
|
356
|
+
kind = expr.get("kind")
|
|
357
|
+
if kind == "ref":
|
|
358
|
+
return render_readme_ref(expr, output_var_names)
|
|
359
|
+
if kind == "array":
|
|
360
|
+
return "[" + ", ".join(render_readme_expr(item, output_var_names) for item in expr.get("items", [])) + "]"
|
|
361
|
+
if kind == "inline_table":
|
|
362
|
+
items = ", ".join(
|
|
363
|
+
f"{repr(item['key'])}: {render_readme_expr(item['value'], output_var_names)}"
|
|
364
|
+
for item in expr.get("items", [])
|
|
365
|
+
)
|
|
366
|
+
return "{" + items + "}"
|
|
367
|
+
if kind == "sequence":
|
|
368
|
+
return " + ".join(render_readme_text_expr(item, output_var_names) for item in expr.get("items", []))
|
|
369
|
+
if kind == "merge":
|
|
370
|
+
parts = expr.get("parts", [])
|
|
371
|
+
inline = next((part for part in parts if isinstance(part, dict) and part.get("kind") == "inline_table"), None)
|
|
372
|
+
if inline is not None:
|
|
373
|
+
other_parts = [part for part in parts if part is not inline]
|
|
374
|
+
rendered = [render_readme_expr(inline, output_var_names)] + [
|
|
375
|
+
render_readme_text_expr(part, output_var_names) for part in other_parts
|
|
376
|
+
]
|
|
377
|
+
return " | ".join(rendered)
|
|
378
|
+
return " + ".join(render_readme_text_expr(item, output_var_names) for item in parts)
|
|
379
|
+
if kind == "call":
|
|
380
|
+
callee = render_readme_expr(expr.get("callee"), output_var_names)
|
|
381
|
+
args = ", ".join(
|
|
382
|
+
f"{arg['name']}={render_readme_expr(arg['value'], output_var_names)}"
|
|
383
|
+
for arg in expr.get("args", [])
|
|
384
|
+
)
|
|
385
|
+
return f"{callee}({args})"
|
|
386
|
+
if kind == "index":
|
|
387
|
+
return f"{render_readme_expr(expr.get('value'), output_var_names)}[{render_readme_expr(expr.get('index'), output_var_names)}]"
|
|
388
|
+
return render_expr(expr, self_ref="api")
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def render_readme_print_lines(print_expr: Any, output_var_names: dict[str, str]) -> list[str]:
|
|
392
|
+
if print_expr is None:
|
|
393
|
+
return []
|
|
394
|
+
if isinstance(print_expr, dict) and print_expr.get("kind") == "array":
|
|
395
|
+
lines: list[str] = []
|
|
396
|
+
for item in print_expr.get("items", []):
|
|
397
|
+
lines.extend(render_readme_print_lines(item, output_var_names))
|
|
398
|
+
return lines
|
|
399
|
+
return [f" print({render_readme_print_value(print_expr, output_var_names)})"]
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def render_readme_print_value(expr: Any, output_var_names: dict[str, str]) -> str:
|
|
403
|
+
if isinstance(expr, dict) and expr.get("kind") == "sequence":
|
|
404
|
+
return render_readme_fstring(expr, output_var_names)
|
|
405
|
+
if isinstance(expr, dict) and expr.get("kind") == "merge":
|
|
406
|
+
return render_readme_fstring(expr, output_var_names)
|
|
407
|
+
return render_readme_expr(expr, output_var_names)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def render_readme_fstring(expr: dict[str, Any], output_var_names: dict[str, str]) -> str:
|
|
411
|
+
parts: list[str] = []
|
|
412
|
+
|
|
413
|
+
def walk(node: Any) -> None:
|
|
414
|
+
if node is None:
|
|
415
|
+
return
|
|
416
|
+
if isinstance(node, list):
|
|
417
|
+
for item in node:
|
|
418
|
+
walk(item)
|
|
419
|
+
return
|
|
420
|
+
if not isinstance(node, dict):
|
|
421
|
+
parts.append("{" + render_readme_expr(node, output_var_names) + "}")
|
|
422
|
+
return
|
|
423
|
+
kind = node.get("kind")
|
|
424
|
+
if kind == "sequence":
|
|
425
|
+
for item in node.get("items", []):
|
|
426
|
+
walk(item)
|
|
427
|
+
return
|
|
428
|
+
if kind == "merge":
|
|
429
|
+
for part in node.get("parts", []):
|
|
430
|
+
walk(part)
|
|
431
|
+
return
|
|
432
|
+
if kind == "string":
|
|
433
|
+
parts.append(escape_fstring_literal(str(node.get("value", ""))))
|
|
434
|
+
return
|
|
435
|
+
parts.append("{" + render_readme_expr(node, output_var_names) + "}")
|
|
436
|
+
|
|
437
|
+
walk(expr)
|
|
438
|
+
return 'f"' + "".join(parts) + '"'
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def escape_fstring_literal(text: str) -> str:
|
|
442
|
+
return text.replace("\\", "\\\\").replace("{", "{{").replace("}", "}}").replace('"', '\\"')
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def render_readme_text_expr(expr: Any, output_var_names: dict[str, str]) -> str:
|
|
446
|
+
if expr is None:
|
|
447
|
+
return "None"
|
|
448
|
+
if not isinstance(expr, dict):
|
|
449
|
+
return render_expr(expr, self_ref="api")
|
|
450
|
+
kind = expr.get("kind")
|
|
451
|
+
if kind == "ref":
|
|
452
|
+
parts = expr.get("parts", [])
|
|
453
|
+
if len(parts) >= 1 and parts[0].get("kind") == "name" and str(parts[0].get("value")) == "FUNCRESULT":
|
|
454
|
+
return f"str({render_readme_ref(expr, output_var_names)})"
|
|
455
|
+
return render_expr(expr, self_ref="api")
|
|
456
|
+
if kind in {"string", "number", "bool", "null"}:
|
|
457
|
+
return render_expr(expr, self_ref="api")
|
|
458
|
+
return render_readme_expr(expr, output_var_names)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def render_readme_ref(expr: dict[str, Any], output_var_names: dict[str, str]) -> str:
|
|
462
|
+
parts = expr.get("parts", [])
|
|
463
|
+
if len(parts) < 4 or parts[0].get("kind") != "name" or str(parts[0].get("value")) != "FUNCRESULT":
|
|
464
|
+
return render_expr(expr, self_ref="api")
|
|
465
|
+
|
|
466
|
+
function_id, example_name, _result_kind, tail_parts = parse_funcresult_reference(expr)
|
|
467
|
+
base = output_var_names.get(readme_example_key(function_id, example_name))
|
|
468
|
+
if base is None:
|
|
469
|
+
raise ValueError(
|
|
470
|
+
f"Referenced example [app.func.{function_id}.examples.{example_name}] is not included in generated docs. "
|
|
471
|
+
f"Mark it @Docs or remove the FUNCRESULT reference."
|
|
472
|
+
)
|
|
473
|
+
rendered = base
|
|
474
|
+
|
|
475
|
+
for tail_part in tail_parts:
|
|
476
|
+
tail_kind = tail_part.get("kind")
|
|
477
|
+
if tail_kind == "index":
|
|
478
|
+
rendered += f"[{render_readme_expr(tail_part.get('value'), output_var_names)}]"
|
|
479
|
+
elif tail_kind == "key":
|
|
480
|
+
rendered += f"[{render_readme_key_selector(tail_part.get('value'), rendered)}]"
|
|
481
|
+
elif tail_kind == "name":
|
|
482
|
+
rendered += f".{tail_part.get('value')}"
|
|
483
|
+
return rendered
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def render_readme_key_selector(index_expr: Any, data_expr: str) -> str:
|
|
487
|
+
index_value = readme_key_selector_number(index_expr)
|
|
488
|
+
if index_value is None:
|
|
489
|
+
raise ValueError("FUNCRESULT @Key selector requires an integer id greater than or equal to -1.")
|
|
490
|
+
if index_value == 0:
|
|
491
|
+
return f"next(iter({data_expr}))"
|
|
492
|
+
if index_value == -1:
|
|
493
|
+
return f"next(reversed({data_expr}))"
|
|
494
|
+
if index_value < -1:
|
|
495
|
+
raise ValueError("FUNCRESULT @Key selector requires an integer id greater than or equal to -1.")
|
|
496
|
+
rendered_index = str(index_value)
|
|
497
|
+
return f"list({data_expr})[{rendered_index}]"
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def validate_readme_key_selector(index_expr: Any, function_id: str, example_name: str) -> None:
|
|
501
|
+
index_value = readme_key_selector_number(index_expr)
|
|
502
|
+
if index_value is None or index_value < -1:
|
|
503
|
+
raise ValueError(
|
|
504
|
+
f"FUNCRESULT.{function_id}.{example_name}.JSON uses @Key with an invalid id. "
|
|
505
|
+
f"Expected an integer greater than or equal to -1."
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def readme_key_selector_number(index_expr: Any) -> int | None:
|
|
510
|
+
if not isinstance(index_expr, dict) or index_expr.get("kind") != "number":
|
|
511
|
+
return None
|
|
512
|
+
value = index_expr.get("value")
|
|
513
|
+
if isinstance(value, bool):
|
|
514
|
+
return None
|
|
515
|
+
if not isinstance(value, (int, float)):
|
|
516
|
+
return None
|
|
517
|
+
if not float(value).is_integer():
|
|
518
|
+
return None
|
|
519
|
+
return int(value)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from jinja2 import Environment, FileSystemLoader, StrictUndefined
|
|
8
|
+
except ImportError as exc: # pragma: no cover - helpful runtime error
|
|
9
|
+
raise RuntimeError(
|
|
10
|
+
"msra_codegen requires Jinja2. Install it with `pip install jinja2`."
|
|
11
|
+
) from exc
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
TEMPLATE_ROOT = Path(__file__).resolve().parent / "templates"
|
|
15
|
+
TEMPLATE_ENV = Environment(
|
|
16
|
+
loader=FileSystemLoader(str(TEMPLATE_ROOT)),
|
|
17
|
+
autoescape=False,
|
|
18
|
+
trim_blocks=True,
|
|
19
|
+
lstrip_blocks=True,
|
|
20
|
+
keep_trailing_newline=True,
|
|
21
|
+
undefined=StrictUndefined,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def render_template(name: str, context: dict[str, Any]) -> str:
|
|
26
|
+
return TEMPLATE_ENV.get_template(name).render(**context)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
.PHONY: help install install-dev test test-quick lint format type-check clean build docs example-docs build-all-docs serve-docs serve-examples ci-test prepare-release generate-badges
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
pip install .
|
|
5
|
+
|
|
6
|
+
install-dev:
|
|
7
|
+
pip install -r requirements-dev.txt
|
|
8
|
+
|
|
9
|
+
test:
|
|
10
|
+
pytest --cov={{ package_name }} --cov-report=xml --cov-report=html --cov-report=term-missing
|
|
11
|
+
|
|
12
|
+
test-quick:
|
|
13
|
+
pytest --tb=short
|
|
14
|
+
|
|
15
|
+
lint:
|
|
16
|
+
python -m ruff check {{ package_name }} tests example.py docs/source/conf.py
|
|
17
|
+
|
|
18
|
+
type-check:
|
|
19
|
+
python -m mypy {{ package_name }}
|
|
20
|
+
|
|
21
|
+
format:
|
|
22
|
+
python -m ruff check --select I --fix {{ package_name }} tests example.py docs/source/conf.py
|
|
23
|
+
python -m ruff format {{ package_name }} tests example.py docs/source/conf.py
|
|
24
|
+
|
|
25
|
+
clean:
|
|
26
|
+
rm -rf build/ dist/ *.egg-info/
|
|
27
|
+
rm -rf docs/_build/ examples/docs/_build/
|
|
28
|
+
rm -rf htmlcov/ .coverage coverage.xml coverage.svg
|
|
29
|
+
rm -rf .pytest_cache/
|
|
30
|
+
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
|
31
|
+
find . -type f -name "*.pyc" -delete
|
|
32
|
+
|
|
33
|
+
build: clean
|
|
34
|
+
python -m build
|
|
35
|
+
|
|
36
|
+
build-install:
|
|
37
|
+
$(MAKE) build
|
|
38
|
+
$(MAKE) install
|
|
39
|
+
|
|
40
|
+
docs:
|
|
41
|
+
cd docs && sphinx-build -b html source _build/html
|
|
42
|
+
|
|
43
|
+
serve-docs:
|
|
44
|
+
cd docs/_build/html && python -m http.server 8000
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# {{ readme.title }}
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
[]({{ readme.workflow_url }})
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
[]({{ readme.pypi_project_url }})
|
|
10
|
+
[]({{ readme.license_url }})
|
|
11
|
+
{% for badge in readme.badges %}
|
|
12
|
+
[]({{ badge.url }})
|
|
13
|
+
{% endfor %}
|
|
14
|
+
{% for social in readme.socials %}
|
|
15
|
+
[]({{ social.url }})
|
|
16
|
+
{% endfor %}
|
|
17
|
+
|
|
18
|
+
{% if readme.description %}
|
|
19
|
+
{{ readme.description }}
|
|
20
|
+
{% endif %}
|
|
21
|
+
|
|
22
|
+
**[⭐ Star us on GitHub]({{ readme.repo_url }})** | **[📚 Read the Docs]({{ readme.docs_url }})** | **[🐛 Report Bug]({{ readme.issues_url }})**
|
|
23
|
+
|
|
24
|
+
### Принцип работы
|
|
25
|
+
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
> {{ readme.principle_text }}
|
|
29
|
+
|
|
30
|
+
<div align="center">
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install {{ package_name }}
|
|
38
|
+
{% if quick_start.requires_camoufox %}
|
|
39
|
+
python -m camoufox fetch
|
|
40
|
+
{% endif %}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```py
|
|
44
|
+
{{ pipeline_script_code }}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Для более подробной информации смотрите референсы [документации]({{ readme.docs_url }}).
|
|
48
|
+
|
|
49
|
+
<div align="center">
|
|
50
|
+
|
|
51
|
+
### Report
|
|
52
|
+
|
|
53
|
+
If you have any problems using it / suggestions, do not hesitate to write to the [project's GitHub]({{ readme.issues_url }})!
|
|
54
|
+
|
|
55
|
+
</div>
|