msra-codegen 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- msra_codegen/README.md +23 -0
- msra_codegen/__init__.py +6 -0
- msra_codegen/__main__.py +5 -0
- msra_codegen/bridge.py +29 -0
- msra_codegen/cli.py +105 -0
- msra_codegen/codegen_context.py +1690 -0
- msra_codegen/config.toml +164 -0
- msra_codegen/core_naming.py +155 -0
- msra_codegen/docs_generator.py +346 -0
- msra_codegen/file_utils.py +8 -0
- msra_codegen/funcresult.py +156 -0
- msra_codegen/generator.py +6 -0
- msra_codegen/generator_config.py +35 -0
- msra_codegen/github_workflows.py +129 -0
- msra_codegen/gitignore.py +31 -0
- msra_codegen/issue_templates.py +100 -0
- msra_codegen/logo_assets.py +99 -0
- msra_codegen/msra_serializer.py +205 -0
- msra_codegen/node_export.js +296 -0
- msra_codegen/package_metadata.py +306 -0
- msra_codegen/package_writer.py +175 -0
- msra_codegen/project_model.py +490 -0
- msra_codegen/python_formatting.py +88 -0
- msra_codegen/python_render.py +242 -0
- msra_codegen/readme_pipeline.py +519 -0
- msra_codegen/requirements.txt +5 -0
- msra_codegen/template_engine.py +26 -0
- msra_codegen/templates/Makefile.tpl +44 -0
- msra_codegen/templates/README.md.tpl +55 -0
- msra_codegen/templates/abstraction/__init__.py.tpl +188 -0
- msra_codegen/templates/abstraction/regexes.py.tpl +25 -0
- msra_codegen/templates/docs/requirements.txt.tpl +3 -0
- msra_codegen/templates/docs/source/Makefile.tpl +20 -0
- msra_codegen/templates/docs/source/api.rst.tpl +9 -0
- msra_codegen/templates/docs/source/conf.py.tpl +88 -0
- msra_codegen/templates/docs/source/index.rst.tpl +14 -0
- msra_codegen/templates/docs/source/module.rst.tpl +34 -0
- msra_codegen/templates/docs/source/quick_start.rst.tpl +19 -0
- msra_codegen/templates/endpoints_init.py.tpl +15 -0
- msra_codegen/templates/example.py.tpl +1 -0
- msra_codegen/templates/function.py.tpl +364 -0
- msra_codegen/templates/github/issue_templates/bug_report.yml.tpl +55 -0
- msra_codegen/templates/github/issue_templates/config.yml.tpl +8 -0
- msra_codegen/templates/github/issue_templates/documentation_issue.yml.tpl +33 -0
- msra_codegen/templates/github/issue_templates/feature_request.yml.tpl +36 -0
- msra_codegen/templates/github/workflows/publish.yml.tpl +100 -0
- msra_codegen/templates/github/workflows/source-sync.yml.tpl +177 -0
- msra_codegen/templates/github/workflows/tests.yml.tpl +69 -0
- msra_codegen/templates/gitignore.tpl +3 -0
- msra_codegen/templates/group.py.tpl +56 -0
- msra_codegen/templates/group_init.py.tpl +14 -0
- msra_codegen/templates/init.py.tpl +4 -0
- msra_codegen/templates/licenses/GPL-3.0-or-later.txt.tpl +674 -0
- msra_codegen/templates/licenses/MIT.txt.tpl +21 -0
- msra_codegen/templates/manager.py.tpl +257 -0
- msra_codegen/templates/pyproject.toml.tpl +38 -0
- msra_codegen/templates/tests/api_test.py.tpl +49 -0
- msra_codegen/templates/tests/conftest.py.tpl +21 -0
- msra_codegen/templates/variable.py.tpl +54 -0
- msra_codegen/tests_generator.py +988 -0
- msra_codegen/typespec.py +275 -0
- msra_codegen/validation.py +118 -0
- msra_codegen-0.1.0.dist-info/METADATA +47 -0
- msra_codegen-0.1.0.dist-info/RECORD +68 -0
- msra_codegen-0.1.0.dist-info/WHEEL +5 -0
- msra_codegen-0.1.0.dist-info/entry_points.txt +2 -0
- msra_codegen-0.1.0.dist-info/licenses/LICENSE +674 -0
- msra_codegen-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|