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.
Files changed (68) hide show
  1. msra_codegen/README.md +23 -0
  2. msra_codegen/__init__.py +6 -0
  3. msra_codegen/__main__.py +5 -0
  4. msra_codegen/bridge.py +29 -0
  5. msra_codegen/cli.py +105 -0
  6. msra_codegen/codegen_context.py +1690 -0
  7. msra_codegen/config.toml +164 -0
  8. msra_codegen/core_naming.py +155 -0
  9. msra_codegen/docs_generator.py +346 -0
  10. msra_codegen/file_utils.py +8 -0
  11. msra_codegen/funcresult.py +156 -0
  12. msra_codegen/generator.py +6 -0
  13. msra_codegen/generator_config.py +35 -0
  14. msra_codegen/github_workflows.py +129 -0
  15. msra_codegen/gitignore.py +31 -0
  16. msra_codegen/issue_templates.py +100 -0
  17. msra_codegen/logo_assets.py +99 -0
  18. msra_codegen/msra_serializer.py +205 -0
  19. msra_codegen/node_export.js +296 -0
  20. msra_codegen/package_metadata.py +306 -0
  21. msra_codegen/package_writer.py +175 -0
  22. msra_codegen/project_model.py +490 -0
  23. msra_codegen/python_formatting.py +88 -0
  24. msra_codegen/python_render.py +242 -0
  25. msra_codegen/readme_pipeline.py +519 -0
  26. msra_codegen/requirements.txt +5 -0
  27. msra_codegen/template_engine.py +26 -0
  28. msra_codegen/templates/Makefile.tpl +44 -0
  29. msra_codegen/templates/README.md.tpl +55 -0
  30. msra_codegen/templates/abstraction/__init__.py.tpl +188 -0
  31. msra_codegen/templates/abstraction/regexes.py.tpl +25 -0
  32. msra_codegen/templates/docs/requirements.txt.tpl +3 -0
  33. msra_codegen/templates/docs/source/Makefile.tpl +20 -0
  34. msra_codegen/templates/docs/source/api.rst.tpl +9 -0
  35. msra_codegen/templates/docs/source/conf.py.tpl +88 -0
  36. msra_codegen/templates/docs/source/index.rst.tpl +14 -0
  37. msra_codegen/templates/docs/source/module.rst.tpl +34 -0
  38. msra_codegen/templates/docs/source/quick_start.rst.tpl +19 -0
  39. msra_codegen/templates/endpoints_init.py.tpl +15 -0
  40. msra_codegen/templates/example.py.tpl +1 -0
  41. msra_codegen/templates/function.py.tpl +364 -0
  42. msra_codegen/templates/github/issue_templates/bug_report.yml.tpl +55 -0
  43. msra_codegen/templates/github/issue_templates/config.yml.tpl +8 -0
  44. msra_codegen/templates/github/issue_templates/documentation_issue.yml.tpl +33 -0
  45. msra_codegen/templates/github/issue_templates/feature_request.yml.tpl +36 -0
  46. msra_codegen/templates/github/workflows/publish.yml.tpl +100 -0
  47. msra_codegen/templates/github/workflows/source-sync.yml.tpl +177 -0
  48. msra_codegen/templates/github/workflows/tests.yml.tpl +69 -0
  49. msra_codegen/templates/gitignore.tpl +3 -0
  50. msra_codegen/templates/group.py.tpl +56 -0
  51. msra_codegen/templates/group_init.py.tpl +14 -0
  52. msra_codegen/templates/init.py.tpl +4 -0
  53. msra_codegen/templates/licenses/GPL-3.0-or-later.txt.tpl +674 -0
  54. msra_codegen/templates/licenses/MIT.txt.tpl +21 -0
  55. msra_codegen/templates/manager.py.tpl +257 -0
  56. msra_codegen/templates/pyproject.toml.tpl +38 -0
  57. msra_codegen/templates/tests/api_test.py.tpl +49 -0
  58. msra_codegen/templates/tests/conftest.py.tpl +21 -0
  59. msra_codegen/templates/variable.py.tpl +54 -0
  60. msra_codegen/tests_generator.py +988 -0
  61. msra_codegen/typespec.py +275 -0
  62. msra_codegen/validation.py +118 -0
  63. msra_codegen-0.1.0.dist-info/METADATA +47 -0
  64. msra_codegen-0.1.0.dist-info/RECORD +68 -0
  65. msra_codegen-0.1.0.dist-info/WHEEL +5 -0
  66. msra_codegen-0.1.0.dist-info/entry_points.txt +2 -0
  67. msra_codegen-0.1.0.dist-info/licenses/LICENSE +674 -0
  68. 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,5 @@
1
+ jinja2>=3.1
2
+ packaging>=24.0
3
+ Pillow>=10.0
4
+ ruff>=0.6
5
+ mypy>=1.11
@@ -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
+ ![Tests last run (ISO)]({{ readme.workflow_last_run_badge_url }})
6
+ [![Tests]({{ readme.workflow_badge_url }})]({{ readme.workflow_url }})
7
+ ![PyPI - Python Version]({{ readme.pypi_python_badge_url }})
8
+ ![PyPI - Package Version]({{ readme.pypi_version_badge_url }})
9
+ [![PyPI - Downloads]({{ readme.pypi_downloads_badge_url }})]({{ readme.pypi_project_url }})
10
+ [![License]({{ readme.license_badge_url }})]({{ readme.license_url }})
11
+ {% for badge in readme.badges %}
12
+ [![{{ badge.label }}]({{ badge.badge_url }})]({{ badge.url }})
13
+ {% endfor %}
14
+ {% for social in readme.socials %}
15
+ [![{{ social.label }}]({{ social.badge_url }})]({{ 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>