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,1690 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import textwrap
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from .core_naming import (
9
+ abstraction_module_name_from_path,
10
+ class_name_for_group,
11
+ field_name_for_group,
12
+ module_file_name_for_group,
13
+ module_import_name_for_group,
14
+ module_output_dir_for_group,
15
+ module_package_depth_for_group,
16
+ root_client_class_name,
17
+ normalize_abstraction_path,
18
+ snake_case,
19
+ )
20
+ from .file_utils import write_text
21
+ from .project_model import top_level_groups
22
+ from .python_render import (
23
+ escape_regex_literal,
24
+ get_plain_value,
25
+ header_names_from_wait_sniffer,
26
+ regex_class_name,
27
+ render_expr,
28
+ render_request_cors_mode,
29
+ render_request_credentials,
30
+ render_request_headers,
31
+ render_request_referrer,
32
+ render_simple_value,
33
+ wait_source_expr,
34
+ )
35
+ from .template_engine import render_template
36
+ from .typespec import (
37
+ escape_docstring,
38
+ inline_array_to_list,
39
+ is_list_type_expr,
40
+ match_to_error,
41
+ match_to_pattern,
42
+ match_to_range,
43
+ match_to_values,
44
+ match_to_check_expr,
45
+ normalize_name,
46
+ primary_type_name,
47
+ ref_input_name,
48
+ selectable_values_from_plain_values,
49
+ should_render_setter,
50
+ is_abstraction_reference_expr,
51
+ type_annotation_from_expr,
52
+ type_annotation_from_types,
53
+ type_names_from_expr,
54
+ variable_type_names,
55
+ )
56
+
57
+
58
+ def render_init(project: dict[str, Any], package_name: str) -> str:
59
+ client_class_name = root_client_class_name(project)
60
+ return render_template(
61
+ "init.py.tpl",
62
+ {
63
+ "client_class_name": client_class_name,
64
+ "version": project["app"]["version"],
65
+ },
66
+ )
67
+
68
+
69
+ def collect_abstraction_scripts(project: dict[str, Any]) -> list[str]:
70
+ app = project.get("app") or {}
71
+ abstractions = app.get("abstractions") or []
72
+ if not isinstance(abstractions, list):
73
+ raise TypeError("app.abstractions must be a list of abstraction script paths.")
74
+ scripts: list[str] = []
75
+ for path in abstractions:
76
+ if not isinstance(path, str):
77
+ raise TypeError("app.abstractions entries must be strings.")
78
+ text = path.strip()
79
+ if text:
80
+ scripts.append(text)
81
+ return scripts
82
+
83
+
84
+ def collect_public_abstraction_class_names(source_path: Path) -> list[str]:
85
+ source_text = source_path.read_text(encoding="utf-8")
86
+ module = ast.parse(source_text, filename=str(source_path))
87
+ public_class_names: list[str] = []
88
+ for node in module.body:
89
+ if isinstance(node, ast.ClassDef) and not node.name.startswith("_"):
90
+ public_class_names.append(node.name)
91
+ return public_class_names
92
+
93
+
94
+ def resolve_abstraction_source_path(source_root: Path, abstraction_path: str) -> Path:
95
+ source = Path(normalize_abstraction_path(abstraction_path))
96
+ if not source.is_absolute():
97
+ source = source_root / source
98
+ return source
99
+
100
+
101
+ def build_abstraction_package_context(project: dict[str, Any]) -> dict[str, Any]:
102
+ abstraction_scripts = collect_abstraction_scripts(project)
103
+ source_root = Path(project["source_path"]).resolve().parent
104
+ external_modules = []
105
+ external_import_lines: list[str] = []
106
+ for path in abstraction_scripts:
107
+ module_name = abstraction_module_name_from_path(path)
108
+ public_class_names = collect_public_abstraction_class_names(
109
+ resolve_abstraction_source_path(source_root, path)
110
+ )
111
+ external_modules.append(
112
+ {
113
+ "module_name": module_name,
114
+ "source_path": path,
115
+ "public_class_names": public_class_names,
116
+ }
117
+ )
118
+ if public_class_names:
119
+ external_import_lines.append(
120
+ f"from .{module_name} import {', '.join(public_class_names)}"
121
+ )
122
+ return {
123
+ "regexes": [
124
+ {
125
+ "name": regex["name"],
126
+ "class_name": regex_class_name(regex["name"]),
127
+ "pattern": escape_regex_literal(str(regex["regex"])),
128
+ "raise_message": render_simple_value(regex["raise"]) if regex["raise"] else None,
129
+ "description": regex["description"],
130
+ }
131
+ for regex in project["regexes"]
132
+ ],
133
+ "external_modules": external_modules,
134
+ "external_import_lines": external_import_lines,
135
+ "has_regexes": bool(project["regexes"]),
136
+ "has_external_abstractions": bool(abstraction_scripts),
137
+ }
138
+
139
+
140
+ def build_variable_context(variable: dict[str, Any]) -> dict[str, Any]:
141
+ type_names = variable_type_names(variable)
142
+ non_null_type_names = {name for name in type_names if name != "null"}
143
+ nullable = bool(variable.get("nullable", False))
144
+ match_values = match_to_values(variable.get("match"))
145
+ match_range = match_to_range(variable.get("match"))
146
+ has_null = nullable or "null" in type_names or (match_values is not None and any(value is None for value in match_values))
147
+ context = {
148
+ "name": variable["name"],
149
+ "description": escape_docstring(variable["description"]) if variable["description"] else "",
150
+ "backing_name": f"_{variable['name']}",
151
+ "capture_expr": render_expr(variable.get("from"), self_ref="self"),
152
+ "capture_kind": primary_type_name(non_null_type_names) or "string",
153
+ "getter_return": build_variable_type_annotation(non_null_type_names, nullable=has_null, match_values=match_values),
154
+ "has_integer": "integer" in non_null_type_names,
155
+ "has_boolean": "boolean" in non_null_type_names,
156
+ "has_number": "number" in non_null_type_names,
157
+ "has_string": "string" in non_null_type_names or not non_null_type_names,
158
+ "has_null": has_null,
159
+ "setter_enabled": should_render_setter(variable),
160
+ "match_values": match_values,
161
+ "match_values_expr": render_simple_value(match_values) if match_values is not None else None,
162
+ "match_pattern": match_to_pattern(variable.get("match")),
163
+ "match_check_expr": match_to_check_expr(variable.get("match"), "value"),
164
+ "match_error": match_to_error(variable.get("match")),
165
+ "match_range": match_range,
166
+ "match_range_lower": match_range[0] if match_range is not None else None,
167
+ "match_range_upper": match_range[1] if match_range is not None else None,
168
+ }
169
+ context["warmup_code"] = build_variable_warmup_code(context)
170
+ return context
171
+
172
+
173
+ def build_variable_type_annotation(
174
+ type_names: set[str],
175
+ *,
176
+ nullable: bool,
177
+ match_values: list[Any] | None,
178
+ ) -> str:
179
+ if match_values:
180
+ literal_values = ", ".join(render_simple_value(value) for value in match_values)
181
+ annotation = f"Literal[{literal_values}]"
182
+ if nullable:
183
+ return f"{annotation} | None"
184
+ return annotation
185
+ return type_annotation_from_types(type_names, nullable=nullable)
186
+
187
+
188
+ def numeric_range_error_message(label: str, lower: int | float | None, upper: int | float | None) -> str:
189
+ if lower is not None and upper is not None:
190
+ return f"`{label}` must be between {lower} and {upper}"
191
+ if lower is not None:
192
+ return f"`{label}` must be greater than or equal to {lower}"
193
+ if upper is not None:
194
+ return f"`{label}` must be less than or equal to {upper}"
195
+ return f"`{label}` must be within the configured numeric range"
196
+
197
+
198
+ def build_numeric_range_validation_lines(
199
+ value_name: str,
200
+ match_range: tuple[int | float | None, int | float | None] | None,
201
+ *,
202
+ required: bool,
203
+ label: str,
204
+ ) -> list[str]:
205
+ if match_range is None:
206
+ return []
207
+ lower, upper = match_range
208
+ comparisons: list[str] = []
209
+ if lower is not None:
210
+ comparisons.append(f"float({value_name}) < {lower}")
211
+ if upper is not None:
212
+ comparisons.append(f"float({value_name}) > {upper}")
213
+ if not comparisons:
214
+ return []
215
+ condition = " or ".join(comparisons)
216
+ if len(comparisons) > 1:
217
+ condition = f"({condition})"
218
+ prefix = "" if required else f"{value_name} is not None and "
219
+ return [
220
+ f"if {prefix}{condition}:",
221
+ f" raise ValueError({numeric_range_error_message(label, lower, upper)!r})",
222
+ ]
223
+
224
+
225
+ def build_variable_warmup_code(variable: dict[str, Any]) -> str:
226
+ raw_name = f"_{variable['name']}_raw"
227
+ value_name = f"_{variable['name']}_value"
228
+ target_expr = f"self.{variable['name']}" if variable.get("setter_enabled") else f"self.{variable['backing_name']}"
229
+ lines = [
230
+ f"{raw_name} = {variable['capture_expr']}",
231
+ f"if {raw_name} is None:",
232
+ f" {target_expr} = None",
233
+ "else:",
234
+ ]
235
+ if variable.get("setter_enabled"):
236
+ value_lines = build_setter_variable_value_lines(variable, raw_name, value_name, target_expr)
237
+ else:
238
+ value_lines = build_variable_value_lines(variable, raw_name, value_name, target_expr)
239
+ lines.extend(f" {line}" for line in value_lines)
240
+ return textwrap.indent("\n".join(lines), " ")
241
+
242
+
243
+ def build_setter_variable_value_lines(
244
+ variable: dict[str, Any],
245
+ raw_name: str,
246
+ value_name: str,
247
+ target_expr: str,
248
+ ) -> list[str]:
249
+ label = variable["name"]
250
+ kind = str(variable.get("capture_kind") or "string")
251
+ lines: list[str] = []
252
+ if kind == "integer":
253
+ lines.append(f"{value_name} = int({raw_name})")
254
+ elif kind == "number":
255
+ lines.append(f"{value_name} = float({raw_name})")
256
+ elif kind == "boolean":
257
+ lines.extend(
258
+ [
259
+ f"if isinstance({raw_name}, bool):",
260
+ f" {value_name} = {raw_name}",
261
+ f"elif isinstance({raw_name}, str):",
262
+ f" lowered = {raw_name}.strip().lower()",
263
+ ' if lowered in {"true", "1", "yes", "on"}:',
264
+ f" {value_name} = True",
265
+ ' elif lowered in {"false", "0", "no", "off"}:',
266
+ f" {value_name} = False",
267
+ " else:",
268
+ f' raise ValueError(f"`{label}` must be boolean-like")',
269
+ "else:",
270
+ f" {value_name} = bool({raw_name})",
271
+ ]
272
+ )
273
+ elif kind == "null":
274
+ lines.append(f"{value_name} = None")
275
+ else:
276
+ lines.append(f"{value_name} = {raw_name} if isinstance({raw_name}, str) else str({raw_name})")
277
+ if not variable.get("setter_enabled"):
278
+ lines.extend(build_variable_validation_lines(variable, value_name))
279
+ lines.append(f"{target_expr} = cast({variable['getter_return']}, {value_name})")
280
+ return lines
281
+
282
+
283
+ def build_variable_value_lines(
284
+ variable: dict[str, Any],
285
+ raw_name: str,
286
+ value_name: str,
287
+ target_expr: str,
288
+ ) -> list[str]:
289
+ label = variable["name"]
290
+ kind = str(variable.get("capture_kind") or "string")
291
+ lines: list[str] = []
292
+ if kind == "integer":
293
+ lines.append(f"{value_name} = int({raw_name})")
294
+ elif kind == "number":
295
+ lines.append(f"{value_name} = float({raw_name})")
296
+ elif kind == "boolean":
297
+ lines.extend(
298
+ [
299
+ f"if isinstance({raw_name}, bool):",
300
+ f" {value_name} = {raw_name}",
301
+ f"elif isinstance({raw_name}, str):",
302
+ f" lowered = {raw_name}.strip().lower()",
303
+ ' if lowered in {"true", "1", "yes", "on"}:',
304
+ f" {value_name} = True",
305
+ ' elif lowered in {"false", "0", "no", "off"}:',
306
+ f" {value_name} = False",
307
+ " else:",
308
+ f' raise ValueError(f"`{label}` must be boolean-like")',
309
+ "else:",
310
+ f" {value_name} = bool({raw_name})",
311
+ ]
312
+ )
313
+ elif kind == "null":
314
+ lines.append(f"{value_name} = None")
315
+ else:
316
+ lines.append(f"{value_name} = {raw_name} if isinstance({raw_name}, str) else str({raw_name})")
317
+ if not variable.get("setter_enabled"):
318
+ lines.extend(build_variable_validation_lines(variable, value_name))
319
+ lines.append(f"{target_expr} = {value_name}")
320
+ return lines
321
+
322
+
323
+ def build_variable_validation_lines(variable: dict[str, Any], value_name: str) -> list[str]:
324
+ label = variable["name"]
325
+ lines: list[str] = []
326
+ check_expr = match_to_check_expr(variable.get("match"), value_name)
327
+ if check_expr:
328
+ lines.append(f"if not ({check_expr}):")
329
+ if variable.get("match_error"):
330
+ lines.append(f" raise ValueError({variable['match_error']})")
331
+ else:
332
+ lines.append(f' raise ValueError("`{label}` does not match the expected format")')
333
+ elif variable.get("match_range") is not None:
334
+ lines.extend(build_numeric_range_validation_lines(value_name, variable.get("match_range"), required=True, label=label))
335
+ elif variable.get("match_values_expr") is not None:
336
+ lines.append(f"allowed_values = {variable['match_values_expr']}")
337
+ lines.append(f"if {value_name} not in allowed_values:")
338
+ lines.append(f' raise ValueError(f"`{label}` must be one of {{allowed_values}}")')
339
+ return lines
340
+
341
+
342
+ def build_pipeline_step_context(step_expr: dict[str, Any], *, page_ref: str, sniffer_ref: str | None, test_mode_ref: str | None) -> dict[str, Any]:
343
+ step = step_expr if isinstance(step_expr, dict) else get_plain_value(step_expr)
344
+ if not isinstance(step, dict):
345
+ step = {}
346
+ action = step.get("action")
347
+ nested_then = step.get("then")
348
+ then_step = None
349
+ if isinstance(nested_then, dict):
350
+ then_step = build_pipeline_step_context(nested_then, page_ref=page_ref, sniffer_ref=sniffer_ref, test_mode_ref=test_mode_ref)
351
+ elif isinstance(nested_then, str):
352
+ then_step = {
353
+ "kind": "step",
354
+ "action": nested_then,
355
+ "for_tests": bool(step.get("for_tests", False)),
356
+ "state": step.get("state", "load"),
357
+ "state_expr": render_simple_value(step.get("state", "load")),
358
+ "what_expr": render_expr(step.get("what"), self_ref="self._parent"),
359
+ "sniffer_source_expr": wait_source_expr(step.get("source", "request")),
360
+ "sniffer_headers": header_names_from_wait_sniffer(step),
361
+ "then_step": None,
362
+ "then": None,
363
+ }
364
+ return {
365
+ "kind": "step",
366
+ "action": action,
367
+ "for_tests": bool(step.get("for_tests", False)),
368
+ "state": step.get("state", "load"),
369
+ "state_expr": render_simple_value(step.get("state", "load")),
370
+ "what_expr": render_expr(step.get("what"), self_ref="self._parent"),
371
+ "sniffer_source_expr": wait_source_expr(step.get("source", "request")),
372
+ "sniffer_headers": header_names_from_wait_sniffer(step),
373
+ "sniffer_headers_expr": render_simple_value(header_names_from_wait_sniffer(step)),
374
+ "then_step": then_step,
375
+ "then": nested_then if isinstance(nested_then, str) else None,
376
+ "page_ref": page_ref,
377
+ "sniffer_ref": sniffer_ref,
378
+ "test_mode_ref": test_mode_ref,
379
+ }
380
+
381
+
382
+ def group_consecutive_test_pipeline_steps(steps: list[dict[str, Any]], *, enabled: bool) -> list[dict[str, Any]]:
383
+ if not enabled:
384
+ return steps
385
+ grouped: list[dict[str, Any]] = []
386
+ buffer: list[dict[str, Any]] = []
387
+ for step in steps:
388
+ if step.get("for_tests"):
389
+ buffer.append(step)
390
+ continue
391
+ if buffer:
392
+ grouped.append({"kind": "test_block", "steps": buffer})
393
+ buffer = []
394
+ grouped.append(step)
395
+ if buffer:
396
+ grouped.append({"kind": "test_block", "steps": buffer})
397
+ return grouped
398
+
399
+
400
+ def build_pipeline_steps_context(
401
+ pipeline_expr: dict[str, Any] | None,
402
+ *,
403
+ page_ref: str,
404
+ sniffer_ref: str | None,
405
+ test_mode_ref: str | None,
406
+ ) -> list[dict[str, Any]]:
407
+ steps = inline_array_to_list(pipeline_expr)
408
+ built_steps = [
409
+ build_pipeline_step_context(step, page_ref=page_ref, sniffer_ref=sniffer_ref, test_mode_ref=test_mode_ref)
410
+ for step in steps
411
+ ]
412
+ return group_consecutive_test_pipeline_steps(built_steps, enabled=test_mode_ref is not None)
413
+
414
+
415
+ def build_function_context(
416
+ func: dict[str, Any],
417
+ root_client_name: str,
418
+ *,
419
+ autotest_enabled: bool = False,
420
+ root_import_prefix: str,
421
+ package_root_expr: str,
422
+ ) -> dict[str, Any]:
423
+ transport = str(func.get("transport", "fetch"))
424
+ method = str(func.get("method", "GET"))
425
+ method_name = str(func.get("name", func["id"]))
426
+ overload_names = list(dict.fromkeys(func.get("overload_names", [])))
427
+ has_overloads = bool(overload_names)
428
+ inputs = [dict(input_spec) for input_spec in func.get("inputs", [])]
429
+ input_allowed_values = build_input_allowed_values_map(func)
430
+ input_overrides: dict[str, dict[str, dict[str, Any]]] = {}
431
+ first_explicit_defaults: dict[str, Any] = {}
432
+ for input_spec in inputs:
433
+ allowed_values = input_allowed_values.get(input_spec["name"])
434
+ if allowed_values is not None:
435
+ input_spec["allowed_values"] = allowed_values
436
+ overrides = {
437
+ str(overload_name): dict(overload_spec)
438
+ for overload_name, overload_spec in (input_spec.get("overloads") or {}).items()
439
+ if isinstance(overload_spec, dict)
440
+ }
441
+ input_spec["overloads"] = overrides
442
+ input_overrides[input_spec["name"]] = overrides
443
+ fallback_default_expr = None
444
+ for overload_name in overload_names:
445
+ override_spec = overrides.get(overload_name) or {}
446
+ if override_spec.get("const") is not None:
447
+ fallback_default_expr = override_spec["const"]
448
+ break
449
+ if override_spec.get("default") is not None:
450
+ fallback_default_expr = override_spec["default"]
451
+ break
452
+ first_explicit_defaults[input_spec["name"]] = fallback_default_expr
453
+
454
+ def merge_input_for_overload(input_spec: dict[str, Any], overload_name: str) -> dict[str, Any]:
455
+ merged = dict(input_spec)
456
+ merged.pop("overloads", None)
457
+ override_spec = input_overrides.get(input_spec["name"], {}).get(overload_name) or {}
458
+ if has_overloads:
459
+ merged["required"] = False
460
+ if override_spec.get("type") is not None:
461
+ merged["type"] = override_spec["type"]
462
+ if override_spec.get("description") is not None:
463
+ merged["description"] = override_spec["description"]
464
+ if "required" in override_spec:
465
+ merged["required"] = bool(get_plain_value(override_spec["required"]))
466
+ if "default" in override_spec and override_spec["default"] is not None:
467
+ merged["default"] = override_spec["default"]
468
+ merged["required"] = False
469
+ if "const" in override_spec and override_spec["const"] is not None:
470
+ merged["const"] = override_spec["const"]
471
+ merged["required"] = False
472
+ if override_spec.get("values") is not None:
473
+ merged["values"] = override_spec["values"]
474
+ if override_spec.get("from") is not None:
475
+ merged["from"] = override_spec["from"]
476
+ if override_spec.get("match") is not None:
477
+ merged["match"] = override_spec["match"]
478
+ if override_spec.get("read_only") is not None:
479
+ merged["read_only"] = override_spec["read_only"]
480
+ if overload_name == "default" and merged.get("default") is None and merged.get("const") is None:
481
+ fallback_default_expr = first_explicit_defaults.get(input_spec["name"])
482
+ if fallback_default_expr is not None:
483
+ merged["default"] = fallback_default_expr
484
+ merged["required"] = False
485
+ if merged.get("const") is not None or merged.get("default") is not None:
486
+ merged["required"] = False
487
+ return merged
488
+
489
+ def render_signature(inputs_to_render: list[dict[str, Any]], *, omit_const: bool = False, force_optional: bool = False) -> tuple[list[str], list[dict[str, Any]]]:
490
+ signature_parts: list[str] = []
491
+ signature_specs: list[dict[str, Any]] = []
492
+ for input_spec in inputs_to_render:
493
+ if omit_const and input_spec.get("const") is not None:
494
+ continue
495
+ rendered_spec = dict(input_spec)
496
+ rendered_spec.pop("overloads", None)
497
+ if force_optional:
498
+ rendered_spec["required"] = False
499
+ rendered_spec["default"] = None
500
+ rendered_spec["const"] = None
501
+ annotation = render_input_annotation(rendered_spec)
502
+ default_expr = render_input_default(rendered_spec)
503
+ signature_specs.append(
504
+ {
505
+ "name": rendered_spec["name"],
506
+ "annotation": annotation,
507
+ "default_expr": default_expr,
508
+ }
509
+ )
510
+ if default_expr is None:
511
+ signature_parts.append(f"{rendered_spec['name']}: {annotation}")
512
+ else:
513
+ signature_parts.append(f"{rendered_spec['name']}: {annotation} = {default_expr}")
514
+ return signature_parts, signature_specs
515
+
516
+ def matched_overload_expr(names: list[str]) -> str:
517
+ quoted_names = [render_simple_value(name) for name in names]
518
+ if len(quoted_names) == 1:
519
+ return f"matched_overload == {quoted_names[0]}"
520
+ return f"matched_overload in [{', '.join(quoted_names)}]"
521
+
522
+ def render_selection_conditions(overload_name: str, overload_inputs: list[dict[str, Any]], all_overloads: dict[str, list[dict[str, Any]]]) -> list[str]:
523
+ conditions: list[str] = []
524
+ for input_spec in overload_inputs:
525
+ input_name = input_spec["name"]
526
+ merged_required_like = bool(input_spec.get("required", False)) or input_spec.get("const") is not None
527
+ other_required_like = any(
528
+ (other_spec.get("required", False) or other_spec.get("const") is not None)
529
+ for name, specs in all_overloads.items()
530
+ if name != overload_name
531
+ for other_spec in specs
532
+ if other_spec["name"] == input_name
533
+ )
534
+ if input_spec.get("const") is not None:
535
+ conditions.append(f"{input_name} is None")
536
+ elif merged_required_like:
537
+ conditions.append(f"{input_name} is not None")
538
+ elif other_required_like:
539
+ conditions.append(f"{input_name} is None")
540
+ return conditions
541
+
542
+ def render_specialization_lines(overload_name: str, overload_inputs: list[dict[str, Any]]) -> list[str]:
543
+ lines: list[str] = []
544
+ for input_spec in overload_inputs:
545
+ input_name = input_spec["name"]
546
+ const_expr = input_spec.get("const")
547
+ default_expr = input_spec.get("default")
548
+ if const_expr is not None:
549
+ lines.append(f"{input_name} = {render_expr(const_expr, self_ref='self._parent')}")
550
+ elif default_expr is not None:
551
+ lines.append(
552
+ f"{input_name} = {render_expr(default_expr, self_ref='self._parent')} if {input_name} is None else {input_name}"
553
+ )
554
+ return lines
555
+
556
+ def collect_input_names(expr: Any) -> list[str]:
557
+ names: list[str] = []
558
+ seen: set[str] = set()
559
+
560
+ def walk(node: Any) -> None:
561
+ if isinstance(node, list):
562
+ for item in node:
563
+ walk(item)
564
+ return
565
+ if not isinstance(node, dict):
566
+ return
567
+ kind = node.get("kind")
568
+ if kind == "ref":
569
+ input_name = ref_input_name(node)
570
+ if input_name and input_name not in seen:
571
+ seen.add(input_name)
572
+ names.append(input_name)
573
+ for part in node.get("parts", []):
574
+ walk(part.get("value"))
575
+ return
576
+ if kind == "inline_table":
577
+ for item in node.get("items", []):
578
+ walk(item.get("value"))
579
+ return
580
+ if kind == "array":
581
+ for item in node.get("items", []):
582
+ walk(item)
583
+ return
584
+ if kind == "sequence":
585
+ for item in node.get("items", []):
586
+ walk(item)
587
+ return
588
+ if kind == "merge":
589
+ for part in node.get("parts", []):
590
+ walk(part)
591
+ return
592
+ if kind == "call":
593
+ walk(node.get("callee"))
594
+ for arg in node.get("args", []):
595
+ walk(arg.get("value"))
596
+ return
597
+ if kind == "index":
598
+ walk(node.get("value"))
599
+ walk(node.get("index"))
600
+
601
+ walk(expr)
602
+ return names
603
+
604
+ def render_wants_condition(wants_value: Any) -> str | None:
605
+ plain_value = get_plain_value(wants_value)
606
+ if isinstance(plain_value, list):
607
+ return f"in {render_simple_value([get_plain_value(item) for item in plain_value])}"
608
+ if plain_value is None:
609
+ return "is None"
610
+ if isinstance(plain_value, bool):
611
+ return "is True" if plain_value else "is False"
612
+ return f"== {render_simple_value(plain_value)}"
613
+
614
+ def render_request_url_code(url_spec: dict[str, Any]) -> str:
615
+ url_entries = list(url_spec.get("entries") or [])
616
+ base_url_expr = render_expr(url_spec.get("base"), self_ref="self._parent")
617
+ if not url_entries:
618
+ if base_url_expr == "None":
619
+ return f"raise TypeError({render_simple_value(method_name + '() requires `url.base` to be configured')})"
620
+ return f"request_url = {base_url_expr}"
621
+
622
+ explicit_entries: list[tuple[int, dict[str, Any], list[str]]] = []
623
+ fallback_entries: list[tuple[int, dict[str, Any]]] = []
624
+ for index, entry in enumerate(url_entries):
625
+ conditions = [f"{input_name} is not None" for input_name in collect_input_names(entry.get("base"))]
626
+ wants = get_plain_value(entry.get("wants"))
627
+ if isinstance(wants, dict):
628
+ for key, value in wants.items():
629
+ wants_condition = render_wants_condition(value)
630
+ if wants_condition is None:
631
+ continue
632
+ conditions.append(f"{key} {wants_condition}")
633
+ if conditions:
634
+ explicit_entries.append((index, entry, conditions))
635
+ else:
636
+ fallback_entries.append((index, entry))
637
+
638
+ if len(fallback_entries) > 1:
639
+ raise ValueError(f"Function {method_name} defines multiple URL fallbacks without selection conditions.")
640
+ if not explicit_entries:
641
+ if fallback_entries:
642
+ return f"request_url = {render_expr(fallback_entries[0][1].get('base'), self_ref='self._parent')}"
643
+ if base_url_expr == "None":
644
+ return f"raise TypeError({render_simple_value(method_name + '() requires `url.base` to be configured')})"
645
+ return f"request_url = {base_url_expr}"
646
+
647
+ ordered_explicit_entries = sorted(
648
+ explicit_entries,
649
+ key=lambda item: (-float(get_plain_value(item[1].get("priority", 0)) or 0), item[0]),
650
+ )
651
+ lines: list[str] = []
652
+ for index, (_entry_index, entry, conditions) in enumerate(ordered_explicit_entries):
653
+ keyword = "if" if index == 0 else "elif"
654
+ lines.append(f"{keyword} {' and '.join(conditions)}:")
655
+ lines.append(f" request_url = {render_expr(entry.get('base'), self_ref='self._parent')}")
656
+
657
+ if fallback_entries:
658
+ lines.append("else:")
659
+ lines.append(f" request_url = {render_expr(fallback_entries[0][1].get('base'), self_ref='self._parent')}")
660
+ else:
661
+ lines.append("else:")
662
+ lines.append(f" raise TypeError({render_simple_value(method_name + '() call is ambiguous; URL cannot be collected')})")
663
+ return "\n".join(lines)
664
+
665
+ def render_validation_code(base_inputs: list[dict[str, Any]], overload_inputs_by_name: dict[str, list[dict[str, Any]]]) -> str | None:
666
+ if not overload_inputs_by_name:
667
+ return None
668
+ lines: list[str] = []
669
+ for base_input in base_inputs:
670
+ input_name = base_input["name"]
671
+ validation_spec = dict(base_input)
672
+ validation_spec.pop("overloads", None)
673
+ validation_spec["required"] = False
674
+ fallback_values = get_plain_value(validation_spec.get("values"))
675
+ if not isinstance(fallback_values, list):
676
+ fallback_values = []
677
+ special_value_overloads: list[str] = []
678
+ for overload_name, overload_inputs in overload_inputs_by_name.items():
679
+ overload_spec = next((item for item in overload_inputs if item["name"] == input_name), None)
680
+ if overload_spec is None:
681
+ continue
682
+ values = get_plain_value(overload_spec.get("values"))
683
+ if isinstance(values, list) and values != fallback_values:
684
+ special_value_overloads.append(overload_name)
685
+ required_overloads = [
686
+ overload_name
687
+ for overload_name, overload_inputs in overload_inputs_by_name.items()
688
+ if next((item for item in overload_inputs if item["name"] == input_name and item.get("required", False)), None)
689
+ ]
690
+ has_checks = bool(
691
+ validation_spec.get("type")
692
+ or validation_spec.get("values")
693
+ or validation_spec.get("match")
694
+ )
695
+ if not has_checks and not required_overloads and not special_value_overloads:
696
+ continue
697
+ if validation_spec.get("type") or validation_spec.get("values") or validation_spec.get("match"):
698
+ item_validation = build_input_validation_context({"inputs": [validation_spec]})[0]
699
+ if item_validation.get("has_checks"):
700
+ # Reuse the existing non-overloaded validation builder for the fallback branch.
701
+ if required_overloads:
702
+ if item_validation.get("required"):
703
+ item_validation["required"] = False
704
+ lines.extend(render_validation_item_lines(item_validation))
705
+ if required_overloads:
706
+ match_expr = matched_overload_expr(required_overloads)
707
+ lines.append(f"elif {match_expr}:")
708
+ lines.append(f" raise ValueError(\"`{input_name}` is required\")")
709
+ if special_value_overloads:
710
+ for overload_name in special_value_overloads:
711
+ overload_spec = next(item for item in overload_inputs_by_name[overload_name] if item["name"] == input_name)
712
+ override_values = get_plain_value(overload_spec.get("values")) or []
713
+ if not isinstance(override_values, list):
714
+ continue
715
+ values_expr = render_simple_value(override_values)
716
+ lines.append(f"if {matched_overload_expr([overload_name])} and {input_name} not in {values_expr}:")
717
+ if overload_spec.get("values") is not None:
718
+ lines.append(
719
+ f" raise ValueError(\"{input_name} for overload {overload_name} must be one of {values_expr}\")"
720
+ )
721
+ if fallback_values:
722
+ lines.append(f"elif {input_name} not in {render_simple_value(fallback_values)}:")
723
+ lines.append(
724
+ f" raise ValueError(\"`{input_name}` must be any of {render_simple_value(fallback_values)}\")"
725
+ )
726
+ return "\n".join(lines) if lines else None
727
+
728
+ def render_validation_item_lines(validation_item: dict[str, Any]) -> list[str]:
729
+ lines: list[str] = []
730
+ if not validation_item.get("has_checks"):
731
+ return lines
732
+ if validation_item.get("required"):
733
+ lines.append(f"if {validation_item['name']} is None:")
734
+ lines.append(f" raise ValueError(\"`{validation_item['name']}` is required\")")
735
+ abstraction_type_expr = validation_item.get("abstraction_type_expr")
736
+ if abstraction_type_expr:
737
+ if validation_item.get("required"):
738
+ lines.append(
739
+ f"abstraction.validate_allowed_value({validation_item['name']}, {abstraction_type_expr})"
740
+ )
741
+ else:
742
+ lines.append(f"if {validation_item['name']} is not None:")
743
+ lines.append(
744
+ f" abstraction.validate_allowed_value({validation_item['name']}, {abstraction_type_expr})"
745
+ )
746
+ return lines
747
+ if validation_item.get("is_list"):
748
+ if validation_item.get("required"):
749
+ lines.append(f"if not isinstance({validation_item['name']}, list):")
750
+ lines.append(f" raise TypeError(\"`{validation_item['name']}` must be list\")")
751
+ lines.append(f"for __item in {validation_item['name']}:")
752
+ else:
753
+ lines.append(
754
+ f"if {validation_item['name']} is not None and not isinstance({validation_item['name']}, list):"
755
+ )
756
+ lines.append(f" raise TypeError(\"`{validation_item['name']}` must be list\")")
757
+ lines.append(f"if {validation_item['name']} is not None:")
758
+ lines.append(f" for __item in {validation_item['name']}:")
759
+ target_indent = " " if validation_item.get("required") else " "
760
+ if "integer" in validation_item["item_type_names"]:
761
+ lines.append(f"{target_indent}if not isinstance(__item, int) or isinstance(__item, bool):")
762
+ lines.append(f"{target_indent} raise TypeError(\"`{validation_item['name']}` items must be int\")")
763
+ elif "boolean" in validation_item["item_type_names"]:
764
+ lines.append(f"{target_indent}if not isinstance(__item, bool):")
765
+ lines.append(f"{target_indent} raise TypeError(\"`{validation_item['name']}` items must be bool\")")
766
+ elif "number" in validation_item["item_type_names"]:
767
+ lines.append(f"{target_indent}if not isinstance(__item, (int, float)) or isinstance(__item, bool):")
768
+ lines.append(f"{target_indent} raise TypeError(\"`{validation_item['name']}` items must be number\")")
769
+ elif "string" in validation_item["item_type_names"]:
770
+ lines.append(f"{target_indent}if not isinstance(__item, str):")
771
+ lines.append(f"{target_indent} raise TypeError(\"`{validation_item['name']}` items must be str\")")
772
+ else:
773
+ if "integer" in validation_item["type_names"]:
774
+ if validation_item.get("required"):
775
+ lines.append(f"if not isinstance({validation_item['name']}, int) or isinstance({validation_item['name']}, bool):")
776
+ lines.append(f" raise TypeError(\"`{validation_item['name']}` must be int\")")
777
+ else:
778
+ lines.append(
779
+ f"if {validation_item['name']} is not None and (not isinstance({validation_item['name']}, int) or isinstance({validation_item['name']}, bool)):"
780
+ )
781
+ lines.append(f" raise TypeError(\"`{validation_item['name']}` must be int\")")
782
+ elif "boolean" in validation_item["type_names"]:
783
+ if validation_item.get("required"):
784
+ lines.append(f"if not isinstance({validation_item['name']}, bool):")
785
+ lines.append(f" raise TypeError(\"`{validation_item['name']}` must be bool\")")
786
+ else:
787
+ lines.append(f"if {validation_item['name']} is not None and not isinstance({validation_item['name']}, bool):")
788
+ lines.append(f" raise TypeError(\"`{validation_item['name']}` must be bool\")")
789
+ elif "number" in validation_item["type_names"]:
790
+ if validation_item.get("required"):
791
+ lines.append(f"if not isinstance({validation_item['name']}, (int, float)) or isinstance({validation_item['name']}, bool):")
792
+ lines.append(f" raise TypeError(\"`{validation_item['name']}` must be number\")")
793
+ else:
794
+ lines.append(
795
+ f"if {validation_item['name']} is not None and (not isinstance({validation_item['name']}, (int, float)) or isinstance({validation_item['name']}, bool)):"
796
+ )
797
+ lines.append(f" raise TypeError(\"`{validation_item['name']}` must be number\")")
798
+ elif "string" in validation_item["type_names"]:
799
+ if validation_item.get("required"):
800
+ lines.append(f"if not isinstance({validation_item['name']}, str):")
801
+ lines.append(f" raise TypeError(\"`{validation_item['name']}` must be str\")")
802
+ else:
803
+ lines.append(f"if {validation_item['name']} is not None and not isinstance({validation_item['name']}, str):")
804
+ lines.append(f" raise TypeError(\"`{validation_item['name']}` must be str\")")
805
+ if validation_item.get("match_check_expr") and not validation_item.get("is_list"):
806
+ if validation_item.get("required"):
807
+ lines.append(f"if not ({validation_item['match_check_expr']}):")
808
+ else:
809
+ lines.append(f"if {validation_item['name']} is not None and not ({validation_item['match_check_expr']}):")
810
+ if validation_item.get("match_error"):
811
+ lines.append(f" raise ValueError({validation_item['match_error']})")
812
+ else:
813
+ lines.append(
814
+ f" raise ValueError(\"`{validation_item['name']}` does not match the expected format\")"
815
+ )
816
+ elif validation_item.get("match_range") and not validation_item.get("is_list"):
817
+ lines.extend(
818
+ build_numeric_range_validation_lines(
819
+ validation_item["name"],
820
+ validation_item["match_range"],
821
+ required=bool(validation_item.get("required")),
822
+ label=validation_item["name"],
823
+ )
824
+ )
825
+ if validation_item.get("values_expr"):
826
+ if validation_item.get("is_list"):
827
+ if validation_item.get("required"):
828
+ lines.append(f"for __item in {validation_item['name']}:")
829
+ lines.append(f" if __item not in {validation_item['values_expr']}:")
830
+ lines.append(
831
+ f" raise ValueError(\"`{validation_item['name']}` items must be one of {validation_item['values_expr']}\")"
832
+ )
833
+ else:
834
+ lines.append(f"if {validation_item['name']} is not None:")
835
+ lines.append(f" for __item in {validation_item['name']}:")
836
+ lines.append(f" if __item not in {validation_item['values_expr']}:")
837
+ lines.append(
838
+ f" raise ValueError(\"`{validation_item['name']}` items must be one of {validation_item['values_expr']}\")"
839
+ )
840
+ else:
841
+ if validation_item.get("required"):
842
+ lines.append(f"if {validation_item['name']} not in {validation_item['values_expr']}:")
843
+ lines.append(
844
+ f" raise ValueError(\"`{validation_item['name']}` must be one of {validation_item['values_expr']}\")"
845
+ )
846
+ else:
847
+ lines.append(
848
+ f"if {validation_item['name']} is not None and {validation_item['name']} not in {validation_item['values_expr']}:"
849
+ )
850
+ lines.append(
851
+ f" raise ValueError(\"`{validation_item['name']}` must be one of {validation_item['values_expr']}\")"
852
+ )
853
+ return lines
854
+
855
+ def render_overload_validation_code(base_inputs: list[dict[str, Any]], overload_inputs_by_name: dict[str, list[dict[str, Any]]]) -> str | None:
856
+ if not overload_inputs_by_name:
857
+ return None
858
+ lines: list[str] = []
859
+ for base_input in base_inputs:
860
+ input_name = base_input["name"]
861
+ type_expr = base_input.get("type")
862
+ type_names = type_names_from_expr(type_expr)
863
+ is_list = is_list_type_expr(type_expr)
864
+ abstraction_type_expr = render_expr(type_expr, self_ref="self._parent") if is_abstraction_reference_expr(type_expr) else None
865
+ match_check_expr = match_to_check_expr(base_input.get("match"), input_name)
866
+ match_error = match_to_error(base_input.get("match"))
867
+ match_range = match_to_range(base_input.get("match"))
868
+ base_values = selectable_values_from_plain_values(get_plain_value(base_input.get("allowed_values")))
869
+ if not base_values:
870
+ base_values = selectable_values_from_plain_values(get_plain_value(base_input.get("values")))
871
+ required_overloads = [
872
+ overload_name
873
+ for overload_name, overload_inputs in overload_inputs_by_name.items()
874
+ if next((item for item in overload_inputs if item["name"] == input_name and item.get("required", False)), None)
875
+ ]
876
+ value_override_overloads: list[tuple[str, list[Any]]] = []
877
+ for overload_name, overload_inputs in overload_inputs_by_name.items():
878
+ overload_spec = next((item for item in overload_inputs if item["name"] == input_name), None)
879
+ if overload_spec is None:
880
+ continue
881
+ override_values = selectable_values_from_plain_values(get_plain_value(overload_spec.get("values")))
882
+ if override_values and override_values != base_values:
883
+ value_override_overloads.append((overload_name, override_values))
884
+ if is_list:
885
+ lines.append(f"if {input_name} is not None and not isinstance({input_name}, list):")
886
+ lines.append(f" raise TypeError(\"`{input_name}` must be list\")")
887
+ lines.append(f"if {input_name} is not None:")
888
+ lines.append(f" for __item in {input_name}:")
889
+ if "integer" in type_names:
890
+ lines.append(f" if not isinstance(__item, int) or isinstance(__item, bool):")
891
+ lines.append(f" raise TypeError(\"`{input_name}` items must be int\")")
892
+ elif "boolean" in type_names:
893
+ lines.append(f" if not isinstance(__item, bool):")
894
+ lines.append(f" raise TypeError(\"`{input_name}` items must be bool\")")
895
+ elif "number" in type_names:
896
+ lines.append(f" if not isinstance(__item, (int, float)) or isinstance(__item, bool):")
897
+ lines.append(f" raise TypeError(\"`{input_name}` items must be number\")")
898
+ elif "string" in type_names:
899
+ lines.append(f" if not isinstance(__item, str):")
900
+ lines.append(f" raise TypeError(\"`{input_name}` items must be str\")")
901
+ if required_overloads:
902
+ lines.append(f"if {input_name} is None and {matched_overload_expr(required_overloads)}:")
903
+ lines.append(f" raise ValueError(\"`{input_name}` is required\")")
904
+ else:
905
+ if "integer" in type_names:
906
+ lines.append(f"if {input_name} is not None and (not isinstance({input_name}, int) or isinstance({input_name}, bool)):")
907
+ lines.append(f" raise TypeError(\"`{input_name}` must be int\")")
908
+ elif "boolean" in type_names:
909
+ lines.append(f"if {input_name} is not None and not isinstance({input_name}, bool):")
910
+ lines.append(f" raise TypeError(\"`{input_name}` must be bool\")")
911
+ elif "number" in type_names:
912
+ lines.append(f"if {input_name} is not None and (not isinstance({input_name}, (int, float)) or isinstance({input_name}, bool)):")
913
+ lines.append(f" raise TypeError(\"`{input_name}` must be number\")")
914
+ elif "string" in type_names:
915
+ lines.append(f"if {input_name} is not None and not isinstance({input_name}, str):")
916
+ lines.append(f" raise TypeError(\"`{input_name}` must be str\")")
917
+ if abstraction_type_expr:
918
+ lines.append(f"if {input_name} is not None:")
919
+ lines.append(
920
+ f" abstraction.validate_allowed_value({input_name}, {abstraction_type_expr})"
921
+ )
922
+ if required_overloads:
923
+ lines.append(f"if {input_name} is None and {matched_overload_expr(required_overloads)}:")
924
+ lines.append(f" raise ValueError(\"`{input_name}` is required\")")
925
+ if match_check_expr:
926
+ lines.append(f"if {input_name} is not None and not ({match_check_expr}):")
927
+ if match_error:
928
+ lines.append(f" raise ValueError({match_error})")
929
+ else:
930
+ lines.append(
931
+ f" raise ValueError(\"`{input_name}` does not match the expected format\")"
932
+ )
933
+ elif match_range:
934
+ lines.extend(
935
+ build_numeric_range_validation_lines(
936
+ input_name,
937
+ match_range,
938
+ required=False,
939
+ label=input_name,
940
+ )
941
+ )
942
+ for overload_name, override_values in value_override_overloads:
943
+ lines.append(
944
+ f"if {matched_overload_expr([overload_name])} and {input_name} not in {render_simple_value(override_values)}:"
945
+ )
946
+ lines.append(
947
+ f" raise ValueError(\"{input_name} for overload {overload_name} must be one of {render_simple_value(override_values)}\")"
948
+ )
949
+ if base_values:
950
+ if value_override_overloads:
951
+ lines.append(f"elif {input_name} is not None and {input_name} not in {render_simple_value(base_values)}:")
952
+ lines.append(
953
+ f" raise ValueError(\"`{input_name}` must be any of {render_simple_value(base_values)}\")"
954
+ )
955
+ else:
956
+ lines.append(f"if {input_name} is not None and {input_name} not in {render_simple_value(base_values)}:")
957
+ lines.append(
958
+ f" raise ValueError(\"`{input_name}` must be any of {render_simple_value(base_values)}\")"
959
+ )
960
+ return "\n".join(lines) if lines else None
961
+
962
+ merged_inputs_by_overload: dict[str, list[dict[str, Any]]] = {
963
+ overload_name: [merge_input_for_overload(input_spec, overload_name) for input_spec in inputs]
964
+ for overload_name in overload_names
965
+ }
966
+
967
+ def should_include_overload_signature_input(
968
+ input_spec: dict[str, Any],
969
+ overload_name: str,
970
+ overload_inputs_by_name: dict[str, list[dict[str, Any]]],
971
+ ) -> bool:
972
+ if input_spec.get("const") is not None:
973
+ return False
974
+ if input_spec.get("required") or input_spec.get("default") is not None:
975
+ return True
976
+ other_required_like = any(
977
+ (other_spec.get("required") or other_spec.get("const") is not None)
978
+ for name, specs in overload_inputs_by_name.items()
979
+ if name != overload_name
980
+ for other_spec in specs
981
+ if other_spec["name"] == input_spec["name"]
982
+ )
983
+ return not other_required_like
984
+
985
+ overload_stub_contexts: list[dict[str, Any]] = []
986
+ for overload_name in overload_names:
987
+ merged_inputs = merged_inputs_by_overload[overload_name]
988
+ signature_parts: list[str] = []
989
+ signature_specs: list[dict[str, Any]] = []
990
+ for input_spec in merged_inputs:
991
+ if not should_include_overload_signature_input(input_spec, overload_name, merged_inputs_by_overload):
992
+ continue
993
+ rendered_spec = dict(input_spec)
994
+ rendered_spec.pop("overloads", None)
995
+ annotation = render_input_annotation(rendered_spec)
996
+ default_expr = render_input_default(rendered_spec)
997
+ signature_specs.append(
998
+ {
999
+ "name": rendered_spec["name"],
1000
+ "annotation": annotation,
1001
+ "default_expr": default_expr,
1002
+ }
1003
+ )
1004
+ if default_expr is None:
1005
+ signature_parts.append(f"{rendered_spec['name']}: {annotation}")
1006
+ else:
1007
+ signature_parts.append(f"{rendered_spec['name']}: {annotation} = {default_expr}")
1008
+ overload_stub_contexts.append(
1009
+ {
1010
+ "name": overload_name,
1011
+ "signature": ", ".join(signature_parts),
1012
+ "signature_specs": signature_specs,
1013
+ }
1014
+ )
1015
+
1016
+ impl_inputs = []
1017
+ for input_spec in inputs:
1018
+ impl_spec = dict(input_spec)
1019
+ impl_spec.pop("overloads", None)
1020
+ if has_overloads:
1021
+ impl_spec["required"] = False
1022
+ impl_spec["default"] = None
1023
+ impl_spec["const"] = None
1024
+ impl_inputs.append(impl_spec)
1025
+ signature_parts, signature_specs = render_signature(impl_inputs, force_optional=has_overloads)
1026
+ validation = build_input_validation_context({"inputs": impl_inputs}) if not has_overloads else []
1027
+ overload_validation_code = render_overload_validation_code(inputs, merged_inputs_by_overload)
1028
+ overload_selection_code = None
1029
+ overload_specialization_code = None
1030
+ if has_overloads:
1031
+ selection_lines: list[str] = ["matched = []"]
1032
+ specialization_lines: list[str] = []
1033
+ for overload_name in overload_names:
1034
+ overload_inputs = merged_inputs_by_overload[overload_name]
1035
+ conditions = render_selection_conditions(overload_name, overload_inputs, merged_inputs_by_overload)
1036
+ if conditions:
1037
+ selection_lines.append(f"if {' and '.join(conditions)}:")
1038
+ selection_lines.append(f" matched.append({render_simple_value(overload_name)})")
1039
+ else:
1040
+ selection_lines.append(f"matched.append({render_simple_value(overload_name)})")
1041
+ specialization_lines.append(f"if {matched_overload_expr([overload_name])}:")
1042
+ rendered_specialization = render_specialization_lines(overload_name, overload_inputs)
1043
+ if rendered_specialization:
1044
+ for line in rendered_specialization:
1045
+ specialization_lines.append(f" {line}")
1046
+ else:
1047
+ specialization_lines.append(" pass")
1048
+ selection_lines.append("if not matched:")
1049
+ selection_lines.append(
1050
+ f" raise TypeError({render_simple_value(method_name + '() expected one of: ' + ', '.join(overload_names))})"
1051
+ )
1052
+ selection_lines.append("elif len(matched) > 1:")
1053
+ selection_lines.append(
1054
+ f" raise TypeError(f\"{method_name}() call is ambiguous; matched overloads: {{matched}}\")"
1055
+ )
1056
+ selection_lines.append("else:")
1057
+ selection_lines.append(" matched_overload = matched[0]")
1058
+ overload_selection_code = "\n".join(selection_lines)
1059
+ overload_specialization_code = "\n".join(specialization_lines)
1060
+
1061
+ url_spec = func.get("url") or {}
1062
+ body = func.get("body")
1063
+ headers_spec = func.get("headers")
1064
+ extractor = func.get("extractor") or {}
1065
+ goto_pipeline = extractor.get("goto_pipeline") or {}
1066
+ extractor_script = extractor.get("script")
1067
+ query_params = build_query_param_context(func)
1068
+ request_url_code = render_request_url_code(url_spec)
1069
+ return_annotation = render_return_annotation(func)
1070
+ signature_text = ", ".join(signature_parts)
1071
+ uses_json_import = any(param.get("list_style_style") == "json" for param in query_params) or bool(
1072
+ goto_pipeline.get("module") and goto_pipeline.get("function")
1073
+ )
1074
+ uses_urlencode_import = bool(query_params)
1075
+ uses_path_import = bool(extractor_script)
1076
+ uses_re_import = any(
1077
+ (validation_item.get("match_check_expr") or "").startswith("re.fullmatch(")
1078
+ for validation_item in validation
1079
+ ) or bool(overload_validation_code and "re.fullmatch(" in overload_validation_code)
1080
+ uses_literal_import = "Literal[" in signature_text or "Literal[" in return_annotation or any(
1081
+ "Literal[" in overload_context["signature"] for overload_context in overload_stub_contexts
1082
+ )
1083
+ uses_http_method_import = transport != "direct"
1084
+ uses_method_pipeline_error_import = bool(goto_pipeline.get("module") and goto_pipeline.get("function"))
1085
+ if overload_selection_code:
1086
+ overload_selection_code = textwrap.indent(overload_selection_code, " ")
1087
+ if overload_specialization_code:
1088
+ overload_specialization_code = textwrap.indent(overload_specialization_code, " ")
1089
+ if overload_validation_code:
1090
+ overload_validation_code = textwrap.indent(overload_validation_code, " ")
1091
+ return {
1092
+ "method_name": method_name,
1093
+ "description": escape_docstring(func.get("description")) if func.get("description") else "",
1094
+ "autotest_enabled": autotest_enabled,
1095
+ "signature": signature_text,
1096
+ "signature_specs": signature_specs,
1097
+ "return_annotation": return_annotation,
1098
+ "root_import_prefix": root_import_prefix,
1099
+ "transport": transport,
1100
+ "method": method,
1101
+ "signature_kwonly": has_overloads,
1102
+ "request_url_code": textwrap.indent(request_url_code, " "),
1103
+ "has_overloads": has_overloads,
1104
+ "overloads": overload_stub_contexts,
1105
+ "overload_selection_code": overload_selection_code,
1106
+ "overload_specialization_code": overload_specialization_code,
1107
+ "overload_validation_code": overload_validation_code,
1108
+ "request": {
1109
+ "referrer_expr": render_request_referrer(headers_spec),
1110
+ "cors_mode_expr": render_request_cors_mode(headers_spec, default_if_missing=False),
1111
+ "credentials_expr": render_request_credentials(headers_spec, default_if_missing=False),
1112
+ "headers_expr": render_request_headers(headers_spec, default_if_missing=False),
1113
+ },
1114
+ "body_expr": render_expr(body.get("from"), self_ref="self._parent") if body else None,
1115
+ "body_type": body.get("type") if body else None,
1116
+ "validation": validation,
1117
+ "query_params": query_params,
1118
+ "extractor": {
1119
+ "render_html": bool(extractor.get("render_html", False)),
1120
+ "script_path_expr": (
1121
+ render_simple_value(str(get_plain_value(extractor_script)))
1122
+ if extractor_script
1123
+ else None
1124
+ ),
1125
+ "package_root_expr": package_root_expr,
1126
+ "goto_pipeline_module": goto_pipeline.get("module") if goto_pipeline else None,
1127
+ "goto_pipeline_function": goto_pipeline.get("function") if goto_pipeline else None,
1128
+ },
1129
+ "uses_json_import": uses_json_import,
1130
+ "uses_urlencode_import": uses_urlencode_import,
1131
+ "uses_path_import": uses_path_import,
1132
+ "uses_re_import": uses_re_import,
1133
+ "uses_literal_import": uses_literal_import,
1134
+ "uses_http_method_import": uses_http_method_import,
1135
+ "uses_method_pipeline_error_import": uses_method_pipeline_error_import,
1136
+ "uses_overload_import": has_overloads,
1137
+ }
1138
+
1139
+
1140
+ def build_input_validation_context(func: dict[str, Any]) -> list[dict[str, Any]]:
1141
+ validations: list[dict[str, Any]] = []
1142
+ for input_spec in func.get("inputs", []):
1143
+ type_expr = input_spec.get("type")
1144
+ type_names = type_names_from_expr(type_expr)
1145
+ is_list = is_list_type_expr(type_expr)
1146
+ abstraction_type_expr = render_expr(type_expr, self_ref="self._parent") if is_abstraction_reference_expr(type_expr) else None
1147
+ match_range = match_to_range(input_spec.get("match"))
1148
+ values = input_spec.get("allowed_values")
1149
+ if values is None:
1150
+ values = get_plain_value(input_spec.get("values"))
1151
+ values = selectable_values_from_plain_values(values)
1152
+ has_type_checks = is_list or abstraction_type_expr is not None or any(
1153
+ type_name in {"integer", "boolean", "number", "string"} for type_name in type_names
1154
+ )
1155
+ has_value_checks = bool(input_spec.get("match") or values)
1156
+ validations.append(
1157
+ {
1158
+ "name": input_spec["name"],
1159
+ "required": bool(input_spec.get("required", False)),
1160
+ "type_names": sorted(type_names),
1161
+ "item_type_names": sorted(type_names) if is_list else [],
1162
+ "is_list": is_list,
1163
+ "abstraction_type_expr": abstraction_type_expr,
1164
+ "match_pattern": match_to_pattern(input_spec.get("match")),
1165
+ "match_check_expr": match_to_check_expr(input_spec.get("match"), input_spec["name"]),
1166
+ "match_error": match_to_error(input_spec.get("match")),
1167
+ "match_range": match_range,
1168
+ "match_range_lower": match_range[0] if match_range is not None else None,
1169
+ "match_range_upper": match_range[1] if match_range is not None else None,
1170
+ "values_expr": render_simple_value(values) if isinstance(values, list) and values and all(not isinstance(item, dict) for item in values) else None,
1171
+ "has_checks": bool(input_spec.get("required", False)) or has_type_checks or has_value_checks,
1172
+ }
1173
+ )
1174
+ return validations
1175
+
1176
+
1177
+ def build_query_param_context(func: dict[str, Any]) -> list[dict[str, Any]]:
1178
+ url_spec = func.get("url") or {}
1179
+ params = url_spec.get("params") or []
1180
+ inputs = {normalize_name(input_spec["name"]): input_spec for input_spec in func.get("inputs", [])}
1181
+ result: list[dict[str, Any]] = []
1182
+ for param in params:
1183
+ param_name = param["name"]
1184
+ list_style = param.get("list_style") or {}
1185
+ from_expr = param.get("from")
1186
+ const_expr = param.get("const")
1187
+ const_value = get_plain_value(const_expr)
1188
+ values = get_plain_value(param.get("values"))
1189
+ source_input_name = ref_input_name(from_expr)
1190
+ matched_input = inputs.get(normalize_name(source_input_name)) if source_input_name else inputs.get(normalize_name(param_name))
1191
+ item: dict[str, Any] = {
1192
+ "name": param_name,
1193
+ "name_expr": render_simple_value(param_name),
1194
+ "source_name": source_input_name or param_name,
1195
+ "source_expr": render_expr(from_expr, self_ref="self._parent") if from_expr is not None else None,
1196
+ "is_list": bool(param.get("list", False))
1197
+ or bool(matched_input and is_list_type_expr(matched_input.get("type")))
1198
+ or isinstance(const_value, list),
1199
+ "has_value_map": False,
1200
+ "selectable_values_expr": None,
1201
+ "value_map_expr": None,
1202
+ "default_values_expr": None,
1203
+ "default_value_expr": None,
1204
+ "list_style_style": str(list_style.get("style", "repeat") or "repeat").strip().lower(),
1205
+ "list_style_delimiter_expr": render_simple_value(str(list_style.get("delimiter", ",") or ",")),
1206
+ "list_style_indexed": bool(list_style.get("indexed", False)),
1207
+ "temp_name": f"_{normalize_name(param_name)}_value",
1208
+ "temp_list_name": f"_{normalize_name(param_name)}_values",
1209
+ }
1210
+ if const_expr is not None:
1211
+ item["kind"] = "literal"
1212
+ if item["is_list"] and not isinstance(const_value, list):
1213
+ item["value_expr"] = render_simple_value([const_value])
1214
+ else:
1215
+ item["value_expr"] = render_expr(const_expr, self_ref="self._parent")
1216
+ result.append(item)
1217
+ continue
1218
+ if from_expr is not None:
1219
+ item["kind"] = "from"
1220
+ if isinstance(values, list) and values and all(isinstance(entry, dict) for entry in values):
1221
+ selectable_entries = [entry for entry in values if entry.get("value") is not None]
1222
+ default_entries = [entry for entry in values if entry.get("default")]
1223
+ if selectable_entries:
1224
+ item["has_value_map"] = True
1225
+ item["selectable_values_expr"] = render_simple_value([entry["value"] for entry in selectable_entries])
1226
+ item["value_map_expr"] = render_simple_value(
1227
+ {entry["value"]: entry["value_in_url"] for entry in selectable_entries}
1228
+ )
1229
+ if default_entries:
1230
+ item["default_values_expr"] = render_simple_value([entry["value_in_url"] for entry in default_entries])
1231
+ if len(default_entries) == 1:
1232
+ item["default_value_expr"] = render_simple_value(default_entries[0]["value_in_url"])
1233
+ if not selectable_entries and default_entries:
1234
+ if param.get("list", False) or (matched_input and is_list_type_expr(matched_input.get("type"))):
1235
+ item["value_expr"] = render_simple_value([entry["value_in_url"] for entry in default_entries])
1236
+ else:
1237
+ item["value_expr"] = render_simple_value(default_entries[0]["value_in_url"])
1238
+ else:
1239
+ item["value_expr"] = render_expr(from_expr, self_ref="self._parent")
1240
+ result.append(item)
1241
+ continue
1242
+ if isinstance(values, list) and values and all(isinstance(entry, dict) for entry in values):
1243
+ default_entries = [entry for entry in values if entry.get("default")]
1244
+ selectable_entries = [entry for entry in values if entry.get("value") is not None]
1245
+ if default_entries:
1246
+ item["kind"] = "literal"
1247
+ if param.get("list", False):
1248
+ item["value_expr"] = render_simple_value([entry["value_in_url"] for entry in default_entries])
1249
+ else:
1250
+ item["value_expr"] = render_simple_value(default_entries[0]["value_in_url"])
1251
+ result.append(item)
1252
+ continue
1253
+ if selectable_entries:
1254
+ item["kind"] = "literal"
1255
+ if param.get("list", False):
1256
+ item["value_expr"] = render_simple_value([entry["value_in_url"] for entry in selectable_entries])
1257
+ else:
1258
+ item["value_expr"] = render_simple_value(selectable_entries[0]["value_in_url"])
1259
+ result.append(item)
1260
+ continue
1261
+ if matched_input:
1262
+ item.update({"kind": "input_passthrough", "input_name": matched_input["name"]})
1263
+ result.append(item)
1264
+ return result
1265
+
1266
+
1267
+ def build_input_allowed_values_map(func: dict[str, Any]) -> dict[str, list[Any]]:
1268
+ url_spec = func.get("url") or {}
1269
+ params = url_spec.get("params") or []
1270
+ result: dict[str, list[Any]] = {}
1271
+ for param in params:
1272
+ source_input_name = ref_input_name(param.get("from"))
1273
+ if not source_input_name or param.get("match") is not None:
1274
+ continue
1275
+ values = get_plain_value(param.get("values"))
1276
+ if not isinstance(values, list):
1277
+ continue
1278
+ selectable_values = [
1279
+ entry.get("value")
1280
+ for entry in values
1281
+ if isinstance(entry, dict) and entry.get("value") is not None
1282
+ ]
1283
+ if selectable_values:
1284
+ result[source_input_name] = selectable_values
1285
+ return result
1286
+
1287
+
1288
+ def render_variable_block(variable: dict[str, Any]) -> str:
1289
+ return render_template("variable.py.tpl", build_variable_context(variable))
1290
+
1291
+
1292
+ def render_function_block(
1293
+ func: dict[str, Any],
1294
+ root_client_name: str,
1295
+ *,
1296
+ autotest_enabled: bool = False,
1297
+ root_import_prefix: str,
1298
+ package_root_expr: str,
1299
+ ) -> str:
1300
+ return render_template(
1301
+ "function.py.tpl",
1302
+ build_function_context(
1303
+ func,
1304
+ root_client_name,
1305
+ autotest_enabled=autotest_enabled,
1306
+ root_import_prefix=root_import_prefix,
1307
+ package_root_expr=package_root_expr,
1308
+ ),
1309
+ )
1310
+
1311
+
1312
+ def build_manager_context(
1313
+ project: dict[str, Any],
1314
+ package_name: str,
1315
+ group_tree: dict[str, Any],
1316
+ *,
1317
+ autotest_function_ids: set[str] | None = None,
1318
+ ) -> dict[str, Any]:
1319
+ app = project["app"]
1320
+ headers_spec = project.get("headers")
1321
+ warmup = project.get("warmup") or {}
1322
+ warmup_script = warmup.get("script") or {}
1323
+ top_groups = top_level_groups(group_tree)
1324
+ root_functions = sorted(group_tree.get("functions", []), key=lambda item: item["name"])
1325
+ autotest_function_ids = set(autotest_function_ids or set())
1326
+ package_root_expr = "Path(__file__).resolve().parent"
1327
+ function_contexts = []
1328
+ for func in root_functions:
1329
+ function_context = build_function_context(
1330
+ func,
1331
+ root_client_class_name(project),
1332
+ autotest_enabled=str(func["id"]) in autotest_function_ids,
1333
+ root_import_prefix=".",
1334
+ package_root_expr=package_root_expr,
1335
+ )
1336
+ function_contexts.append(
1337
+ {
1338
+ "code": render_template("function.py.tpl", function_context),
1339
+ "autotest_enabled": function_context["autotest_enabled"],
1340
+ "uses_http_method_import": function_context["uses_http_method_import"],
1341
+ "uses_json_import": function_context["uses_json_import"],
1342
+ "uses_urlencode_import": function_context["uses_urlencode_import"],
1343
+ "uses_path_import": function_context["uses_path_import"],
1344
+ "uses_re_import": function_context["uses_re_import"],
1345
+ "uses_literal_import": function_context["uses_literal_import"],
1346
+ "uses_method_pipeline_error_import": function_context["uses_method_pipeline_error_import"],
1347
+ "uses_overload_import": function_context["uses_overload_import"],
1348
+ "has_overloads": function_context["has_overloads"],
1349
+ "overloads": function_context["overloads"],
1350
+ "overload_selection_code": function_context["overload_selection_code"],
1351
+ "overload_specialization_code": function_context["overload_specialization_code"],
1352
+ "overload_validation_code": function_context["overload_validation_code"],
1353
+ }
1354
+ )
1355
+ variable_contexts = [
1356
+ {
1357
+ **build_variable_context(variable),
1358
+ "code": render_variable_block(variable),
1359
+ }
1360
+ for variable in project["variables"]
1361
+ ]
1362
+ return {
1363
+ "client_class_name": root_client_class_name(project),
1364
+ "app": app,
1365
+ "app_description": escape_docstring(app["description"]) if app.get("description") else "",
1366
+ "package_name": package_name,
1367
+ "uses_classvar_import": bool(project["prefixes"]),
1368
+ "has_root_functions": bool(function_contexts),
1369
+ "functions": function_contexts,
1370
+ "has_autotests": any(func_context["autotest_enabled"] for func_context in function_contexts),
1371
+ "uses_literal_import": any(func_context.get("uses_literal_import", False) for func_context in function_contexts)
1372
+ or any("Literal[" in variable_context["code"] for variable_context in variable_contexts),
1373
+ "imports": {
1374
+ "http_method": any(func_context.get("uses_http_method_import", False) for func_context in function_contexts),
1375
+ "json": any(func_context.get("uses_json_import", False) for func_context in function_contexts),
1376
+ "urlencode": any(func_context.get("uses_urlencode_import", False) for func_context in function_contexts),
1377
+ "path": any(func_context.get("uses_path_import", False) for func_context in function_contexts),
1378
+ "re": any(func_context.get("uses_re_import", False) for func_context in function_contexts),
1379
+ "overload": any(func_context.get("uses_overload_import", False) for func_context in function_contexts),
1380
+ "method_pipeline_error": any(
1381
+ func_context.get("uses_method_pipeline_error_import", False) for func_context in function_contexts
1382
+ ),
1383
+ },
1384
+ "uses_warmup_error_import": bool(warmup_script.get("path") and warmup_script.get("module") and warmup_script.get("function")),
1385
+ "prefixes": [
1386
+ {
1387
+ "name": prefix_name,
1388
+ "attr_name": f"_{prefix_name}",
1389
+ "value": render_simple_value(prefix_value),
1390
+ }
1391
+ for prefix_name, prefix_value in project["prefixes"].items()
1392
+ ],
1393
+ "top_groups": [
1394
+ {
1395
+ "field_name": field_name_for_group(group["path"]),
1396
+ "class_name": class_name_for_group(group["path"]),
1397
+ "module_name": module_import_name_for_group(group),
1398
+ "description": escape_docstring(group.get("description")) if group.get("description") else "",
1399
+ }
1400
+ for group in top_groups
1401
+ ],
1402
+ "variables": variable_contexts,
1403
+ "warmup": {
1404
+ "headers_sniffer": bool(warmup.get("headers_sniffer", False)),
1405
+ "on_error_screenshot_path": render_simple_value(
1406
+ warmup.get("on_error_screenshot_path", "screenshot.png")
1407
+ ),
1408
+ "script_path_expr": render_simple_value(warmup_script.get("path")) if warmup_script else None,
1409
+ "script_module": warmup_script.get("module") if warmup_script else None,
1410
+ "script_function": warmup_script.get("function") if warmup_script else None,
1411
+ },
1412
+ "request": {
1413
+ "referrer_expr": render_request_referrer(headers_spec),
1414
+ "cors_mode_expr": render_request_cors_mode(headers_spec, default_if_missing=True),
1415
+ "credentials_expr": render_request_credentials(headers_spec, default_if_missing=True),
1416
+ "headers_expr": render_request_headers(headers_spec, default_if_missing=True),
1417
+ },
1418
+ }
1419
+
1420
+
1421
+ def build_group_context(
1422
+ group_node: dict[str, Any],
1423
+ project: dict[str, Any],
1424
+ package_name: str,
1425
+ *,
1426
+ autotest_function_ids: set[str] | None = None,
1427
+ ) -> dict[str, Any]:
1428
+ root_client_name = root_client_class_name(project)
1429
+ child_nodes = list(group_node.get("children", {}).values())
1430
+ module_depth = module_package_depth_for_group(group_node)
1431
+ package_root_expr = f"Path(__file__).resolve().parents[{module_depth}]"
1432
+ autotest_function_ids = set(autotest_function_ids or set())
1433
+ function_contexts = []
1434
+ for func in sorted(group_node.get("functions", []), key=lambda item: item["name"]):
1435
+ function_context = build_function_context(
1436
+ func,
1437
+ root_client_name,
1438
+ autotest_enabled=str(func["id"]) in autotest_function_ids,
1439
+ root_import_prefix="." * (module_depth + 1),
1440
+ package_root_expr=package_root_expr,
1441
+ )
1442
+ function_contexts.append(
1443
+ {
1444
+ "code": render_template("function.py.tpl", function_context),
1445
+ "autotest_enabled": function_context["autotest_enabled"],
1446
+ "uses_http_method_import": function_context["uses_http_method_import"],
1447
+ "uses_json_import": function_context["uses_json_import"],
1448
+ "uses_urlencode_import": function_context["uses_urlencode_import"],
1449
+ "uses_path_import": function_context["uses_path_import"],
1450
+ "uses_re_import": function_context["uses_re_import"],
1451
+ "uses_literal_import": function_context["uses_literal_import"],
1452
+ "uses_method_pipeline_error_import": function_context["uses_method_pipeline_error_import"],
1453
+ "uses_overload_import": function_context["uses_overload_import"],
1454
+ "has_overloads": function_context["has_overloads"],
1455
+ "overloads": function_context["overloads"],
1456
+ "overload_selection_code": function_context["overload_selection_code"],
1457
+ "overload_specialization_code": function_context["overload_specialization_code"],
1458
+ "overload_validation_code": function_context["overload_validation_code"],
1459
+ }
1460
+ )
1461
+ return {
1462
+ "package_name": package_name,
1463
+ "root_client_name": root_client_name,
1464
+ "root_import_prefix": "." * (module_depth + 1),
1465
+ "package_root_expr": package_root_expr,
1466
+ "group_name": ".".join(group_node["path"]) or "MSRA",
1467
+ "class_name": class_name_for_group(group_node["path"]),
1468
+ "module_name": module_file_name_for_group(group_node["path"]),
1469
+ "module_stem": module_file_name_for_group(group_node["path"])[:-3],
1470
+ "description": escape_docstring(group_node.get("description")) if group_node.get("description") else "",
1471
+ "child_imports": [
1472
+ {
1473
+ "package_name": module_import_name_for_group(child),
1474
+ "class_name": class_name_for_group(child["path"]),
1475
+ "description": escape_docstring(child.get("description")) if child.get("description") else "",
1476
+ }
1477
+ for child in child_nodes
1478
+ ],
1479
+ "children": [
1480
+ {
1481
+ "field_name": field_name_for_group(child["path"]),
1482
+ "class_name": class_name_for_group(child["path"]),
1483
+ "description": escape_docstring(child.get("description")) if child.get("description") else "",
1484
+ }
1485
+ for child in child_nodes
1486
+ ],
1487
+ "functions": function_contexts,
1488
+ "has_autotests": any(func_context["autotest_enabled"] for func_context in function_contexts),
1489
+ "uses_overload_import": any(func_context.get("uses_overload_import", False) for func_context in function_contexts),
1490
+ "imports": {
1491
+ "http_method": any(func_context.get("uses_http_method_import", False) for func_context in function_contexts),
1492
+ "json": any(func_context.get("uses_json_import", False) for func_context in function_contexts),
1493
+ "urlencode": any(func_context.get("uses_urlencode_import", False) for func_context in function_contexts),
1494
+ "path": any(func_context.get("uses_path_import", False) for func_context in function_contexts),
1495
+ "re": any(func_context.get("uses_re_import", False) for func_context in function_contexts),
1496
+ "literal": any(func_context.get("uses_literal_import", False) for func_context in function_contexts),
1497
+ "method_pipeline_error": any(
1498
+ func_context.get("uses_method_pipeline_error_import", False) for func_context in function_contexts
1499
+ ),
1500
+ "overload": any(func_context.get("uses_overload_import", False) for func_context in function_contexts),
1501
+ },
1502
+ }
1503
+
1504
+
1505
+ def render_group_block(
1506
+ group_node: dict[str, Any],
1507
+ project: dict[str, Any],
1508
+ package_name: str,
1509
+ *,
1510
+ autotest_function_ids: set[str] | None = None,
1511
+ ) -> str:
1512
+ return render_template(
1513
+ "group.py.tpl",
1514
+ build_group_context(
1515
+ group_node,
1516
+ project,
1517
+ package_name,
1518
+ autotest_function_ids=autotest_function_ids,
1519
+ ),
1520
+ )
1521
+
1522
+
1523
+ def render_manager_template(
1524
+ project: dict[str, Any],
1525
+ package_name: str,
1526
+ group_tree: dict[str, Any],
1527
+ *,
1528
+ autotest_function_ids: set[str] | None = None,
1529
+ ) -> str:
1530
+ return render_template(
1531
+ "manager.py.tpl",
1532
+ build_manager_context(
1533
+ project,
1534
+ package_name,
1535
+ group_tree,
1536
+ autotest_function_ids=autotest_function_ids,
1537
+ ),
1538
+ )
1539
+
1540
+
1541
+ def render_group_template(
1542
+ group_node: dict[str, Any],
1543
+ project: dict[str, Any],
1544
+ package_name: str,
1545
+ *,
1546
+ autotest_function_ids: set[str] | None = None,
1547
+ ) -> str:
1548
+ return render_group_block(
1549
+ group_node,
1550
+ project,
1551
+ package_name,
1552
+ autotest_function_ids=autotest_function_ids,
1553
+ )
1554
+
1555
+
1556
+ def render_group_init_template(
1557
+ group_node: dict[str, Any],
1558
+ project: dict[str, Any],
1559
+ package_name: str,
1560
+ *,
1561
+ autotest_function_ids: set[str] | None = None,
1562
+ ) -> str:
1563
+ return render_template(
1564
+ "group_init.py.tpl",
1565
+ build_group_context(
1566
+ group_node,
1567
+ project,
1568
+ package_name,
1569
+ autotest_function_ids=autotest_function_ids,
1570
+ ),
1571
+ )
1572
+
1573
+
1574
+ def render_endpoints_init(project: dict[str, Any], package_name: str, group_tree: dict[str, Any]) -> str:
1575
+ return render_template(
1576
+ "endpoints_init.py.tpl",
1577
+ {
1578
+ "package_name": package_name,
1579
+ "top_groups": [
1580
+ {
1581
+ "package_name": module_import_name_for_group(group),
1582
+ "class_name": class_name_for_group(group["path"]),
1583
+ "description": escape_docstring(group.get("description") or group["name"]),
1584
+ }
1585
+ for group in top_level_groups(group_tree)
1586
+ ],
1587
+ },
1588
+ )
1589
+
1590
+
1591
+ def write_group_package(
1592
+ group_node: dict[str, Any],
1593
+ project: dict[str, Any],
1594
+ package_name: str,
1595
+ endpoints_root: Path,
1596
+ *,
1597
+ autotest_function_ids: set[str] | None = None,
1598
+ ) -> None:
1599
+ package_dir = module_output_dir_for_group(group_node, endpoints_root)
1600
+ if group_node.get("children"):
1601
+ package_dir.mkdir(parents=True, exist_ok=True)
1602
+ context = build_group_context(
1603
+ group_node,
1604
+ project,
1605
+ package_name,
1606
+ autotest_function_ids=autotest_function_ids,
1607
+ )
1608
+ if group_node.get("children"):
1609
+ write_text(
1610
+ package_dir / "__init__.py",
1611
+ render_template("group_init.py.tpl", context),
1612
+ )
1613
+ write_text(
1614
+ package_dir / module_file_name_for_group(group_node["path"]),
1615
+ render_template("group.py.tpl", context),
1616
+ )
1617
+ for child in group_node.get("children", {}).values():
1618
+ write_group_package(
1619
+ child,
1620
+ project,
1621
+ package_name,
1622
+ endpoints_root,
1623
+ autotest_function_ids=autotest_function_ids,
1624
+ )
1625
+ def collect_extractor_scripts(project: dict[str, Any]) -> list[str]:
1626
+ scripts: list[str] = []
1627
+ for func in project["functions"]:
1628
+ extractor = func.get("extractor")
1629
+ if not extractor:
1630
+ continue
1631
+ script = extractor.get("script")
1632
+ if isinstance(script, str) and script:
1633
+ scripts.append(script)
1634
+ return scripts
1635
+
1636
+
1637
+ def collect_goto_pipeline_scripts(project: dict[str, Any]) -> list[str]:
1638
+ scripts: list[str] = []
1639
+ for func in project["functions"]:
1640
+ extractor = func.get("extractor")
1641
+ if not extractor:
1642
+ continue
1643
+ goto_pipeline = extractor.get("goto_pipeline")
1644
+ if isinstance(goto_pipeline, dict):
1645
+ path = goto_pipeline.get("path")
1646
+ if isinstance(path, str) and path:
1647
+ scripts.append(path)
1648
+ return scripts
1649
+
1650
+
1651
+ def collect_warmup_scripts(project: dict[str, Any]) -> list[str]:
1652
+ warmup = project.get("warmup") or {}
1653
+ script = warmup.get("script") or {}
1654
+ path = script.get("path")
1655
+ if isinstance(path, str) and path:
1656
+ return [path]
1657
+ return []
1658
+
1659
+
1660
+ def render_return_annotation(func: dict[str, Any]) -> str:
1661
+ return "abstraction.Output"
1662
+
1663
+
1664
+ def render_input_annotation(input_spec: dict[str, Any]) -> str:
1665
+ base = type_annotation_from_expr(input_spec.get("type"))
1666
+ values = input_spec.get("allowed_values")
1667
+ if values is None:
1668
+ values = get_plain_value(input_spec.get("values"))
1669
+ values = selectable_values_from_plain_values(values)
1670
+ if input_spec.get("match") is None and values:
1671
+ literal_values = ", ".join(render_simple_value(item) for item in values)
1672
+ if base.startswith("list[") and base.endswith("]"):
1673
+ base = f"list[Literal[{literal_values}]]"
1674
+ else:
1675
+ base = f"Literal[{literal_values}]"
1676
+ default_expr = input_spec.get("default")
1677
+ has_explicit_default = default_expr is not None and get_plain_value(default_expr) is not None
1678
+ if not input_spec.get("required", False) and not has_explicit_default and "| None" not in base:
1679
+ base = f"{base} | None"
1680
+ return base
1681
+
1682
+
1683
+ def render_input_default(input_spec: dict[str, Any]) -> str | None:
1684
+ if "const" in input_spec and input_spec["const"] is not None:
1685
+ return render_expr(input_spec["const"], self_ref="self._parent")
1686
+ if "default" in input_spec and input_spec["default"] is not None:
1687
+ return render_expr(input_spec["default"], self_ref="self._parent")
1688
+ if not input_spec.get("required", False):
1689
+ return "None"
1690
+ return None