persona-dsl 26.1.20.8__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.
- persona_dsl/__init__.py +35 -0
- persona_dsl/components/action.py +10 -0
- persona_dsl/components/base_step.py +251 -0
- persona_dsl/components/combined_step.py +68 -0
- persona_dsl/components/expectation.py +10 -0
- persona_dsl/components/fact.py +10 -0
- persona_dsl/components/goal.py +10 -0
- persona_dsl/components/ops.py +7 -0
- persona_dsl/components/step.py +75 -0
- persona_dsl/expectations/generic/__init__.py +15 -0
- persona_dsl/expectations/generic/contains_item.py +19 -0
- persona_dsl/expectations/generic/contains_the_text.py +15 -0
- persona_dsl/expectations/generic/has_entries.py +21 -0
- persona_dsl/expectations/generic/is_equal.py +24 -0
- persona_dsl/expectations/generic/is_greater_than.py +18 -0
- persona_dsl/expectations/generic/path_equal.py +27 -0
- persona_dsl/expectations/web/__init__.py +5 -0
- persona_dsl/expectations/web/is_displayed.py +13 -0
- persona_dsl/expectations/web/matches_aria_snapshot.py +221 -0
- persona_dsl/expectations/web/matches_screenshot.py +160 -0
- persona_dsl/generators/__init__.py +5 -0
- persona_dsl/generators/api_generator.py +423 -0
- persona_dsl/generators/cli.py +431 -0
- persona_dsl/generators/page_generator.py +1140 -0
- persona_dsl/ops/api/__init__.py +5 -0
- persona_dsl/ops/api/json_as.py +104 -0
- persona_dsl/ops/api/json_response.py +48 -0
- persona_dsl/ops/api/send_request.py +41 -0
- persona_dsl/ops/db/__init__.py +5 -0
- persona_dsl/ops/db/execute_sql.py +22 -0
- persona_dsl/ops/db/fetch_all.py +29 -0
- persona_dsl/ops/db/fetch_one.py +22 -0
- persona_dsl/ops/kafka/__init__.py +4 -0
- persona_dsl/ops/kafka/message_in_topic.py +89 -0
- persona_dsl/ops/kafka/send_message.py +35 -0
- persona_dsl/ops/soap/__init__.py +4 -0
- persona_dsl/ops/soap/call_operation.py +24 -0
- persona_dsl/ops/soap/operation_result.py +24 -0
- persona_dsl/ops/web/__init__.py +37 -0
- persona_dsl/ops/web/aria_snapshot.py +87 -0
- persona_dsl/ops/web/click.py +30 -0
- persona_dsl/ops/web/current_path.py +17 -0
- persona_dsl/ops/web/element_attribute.py +24 -0
- persona_dsl/ops/web/element_is_visible.py +27 -0
- persona_dsl/ops/web/element_text.py +28 -0
- persona_dsl/ops/web/elements_count.py +42 -0
- persona_dsl/ops/web/fill.py +41 -0
- persona_dsl/ops/web/generate_page_object.py +118 -0
- persona_dsl/ops/web/input_value.py +23 -0
- persona_dsl/ops/web/navigate.py +52 -0
- persona_dsl/ops/web/press_key.py +37 -0
- persona_dsl/ops/web/rich_aria_snapshot.py +146 -0
- persona_dsl/ops/web/screenshot.py +68 -0
- persona_dsl/ops/web/table_data.py +43 -0
- persona_dsl/ops/web/wait_for_navigation.py +23 -0
- persona_dsl/pages/__init__.py +133 -0
- persona_dsl/pages/elements.py +998 -0
- persona_dsl/pages/page.py +44 -0
- persona_dsl/pages/virtual_page.py +94 -0
- persona_dsl/persona.py +125 -0
- persona_dsl/pytest_plugin.py +1064 -0
- persona_dsl/runtime/dist/persona_bundle.js +1077 -0
- persona_dsl/skills/__init__.py +7 -0
- persona_dsl/skills/core/base.py +41 -0
- persona_dsl/skills/core/skill_definition.py +30 -0
- persona_dsl/skills/use_api.py +251 -0
- persona_dsl/skills/use_browser.py +78 -0
- persona_dsl/skills/use_database.py +129 -0
- persona_dsl/skills/use_kafka.py +135 -0
- persona_dsl/skills/use_soap.py +66 -0
- persona_dsl/utils/__init__.py +0 -0
- persona_dsl/utils/artifacts.py +22 -0
- persona_dsl/utils/config.py +54 -0
- persona_dsl/utils/data_providers.py +159 -0
- persona_dsl/utils/decorators.py +80 -0
- persona_dsl/utils/metrics.py +69 -0
- persona_dsl/utils/naming.py +14 -0
- persona_dsl/utils/path.py +202 -0
- persona_dsl/utils/retry.py +51 -0
- persona_dsl/utils/taas_integration.py +124 -0
- persona_dsl/utils/waits.py +112 -0
- persona_dsl-26.1.20.8.dist-info/METADATA +35 -0
- persona_dsl-26.1.20.8.dist-info/RECORD +86 -0
- persona_dsl-26.1.20.8.dist-info/WHEEL +5 -0
- persona_dsl-26.1.20.8.dist-info/entry_points.txt +6 -0
- persona_dsl-26.1.20.8.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, List, Tuple, Optional, Set
|
|
5
|
+
from persona_dsl.utils.naming import to_pascal_case, to_snake_case
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ApiGenerator:
|
|
11
|
+
"""Генерирует модели (Pydantic) и типизированные Step-классы API на основе OpenAPI."""
|
|
12
|
+
|
|
13
|
+
def _load_spec(self, spec_path: str) -> Dict[str, Any]:
|
|
14
|
+
path = Path(spec_path)
|
|
15
|
+
if not path.exists():
|
|
16
|
+
raise FileNotFoundError(f"Файл спецификации не найден: {spec_path}")
|
|
17
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
18
|
+
if path.suffix.lower() in (".yaml", ".yml"):
|
|
19
|
+
return yaml.safe_load(f)
|
|
20
|
+
if path.suffix.lower() == ".json":
|
|
21
|
+
return json.load(f)
|
|
22
|
+
raise ValueError(
|
|
23
|
+
f"Неподдерживаемый формат файла спецификации: {path.suffix}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# --- Генерация моделей ---
|
|
27
|
+
|
|
28
|
+
def _ref_name(self, ref: str) -> str:
|
|
29
|
+
if not ref.startswith("#/components/schemas/"):
|
|
30
|
+
raise ValueError(
|
|
31
|
+
f"Поддерживаются только $ref на components/schemas, получено: {ref}"
|
|
32
|
+
)
|
|
33
|
+
return ref.split("/")[-1]
|
|
34
|
+
|
|
35
|
+
def _emit_enum(self, name: str, schema: Dict[str, Any]) -> str:
|
|
36
|
+
base = "str" if schema.get("type") == "string" else "int"
|
|
37
|
+
values = schema.get("enum") or []
|
|
38
|
+
if not isinstance(values, list) or not values:
|
|
39
|
+
raise ValueError(f"Enum '{name}' должен содержать непустой массив 'enum'")
|
|
40
|
+
lines = [f"class {name}({base}, Enum):"]
|
|
41
|
+
for v in values:
|
|
42
|
+
if isinstance(v, str):
|
|
43
|
+
member = to_snake_case(v).upper() or "VALUE"
|
|
44
|
+
lines.append(f" {member} = {v!r}")
|
|
45
|
+
else:
|
|
46
|
+
lines.append(f" VALUE_{v} = {v}")
|
|
47
|
+
return "\n".join(lines) + "\n"
|
|
48
|
+
|
|
49
|
+
def _python_type(self, schema: Dict[str, Any]) -> str:
|
|
50
|
+
t = schema.get("type")
|
|
51
|
+
if "$ref" in schema:
|
|
52
|
+
return self._ref_name(schema["$ref"])
|
|
53
|
+
if t == "string":
|
|
54
|
+
return "str"
|
|
55
|
+
if t == "integer":
|
|
56
|
+
return "int"
|
|
57
|
+
if t == "number":
|
|
58
|
+
return "float"
|
|
59
|
+
if t == "boolean":
|
|
60
|
+
return "bool"
|
|
61
|
+
if t == "array":
|
|
62
|
+
items = schema.get("items") or {}
|
|
63
|
+
inner = self._python_type(items)
|
|
64
|
+
return f"list[{inner}]"
|
|
65
|
+
if t == "object":
|
|
66
|
+
addl = schema.get("additionalProperties")
|
|
67
|
+
if addl is True or addl == {}:
|
|
68
|
+
return "dict[str, Any]"
|
|
69
|
+
if isinstance(addl, dict):
|
|
70
|
+
vtype = self._python_type(addl)
|
|
71
|
+
return f"dict[str, {vtype}]"
|
|
72
|
+
return "dict[str, Any]"
|
|
73
|
+
if "enum" in schema:
|
|
74
|
+
raise ValueError("Enum без имени модели не поддержан в этом контексте")
|
|
75
|
+
raise ValueError(f"Неподдерживаемая схема: {schema}")
|
|
76
|
+
|
|
77
|
+
def _emit_model(
|
|
78
|
+
self, name: str, schema: Dict[str, Any], generated: Set[str], models: List[str]
|
|
79
|
+
) -> None:
|
|
80
|
+
if name in generated:
|
|
81
|
+
return
|
|
82
|
+
if "enum" in schema:
|
|
83
|
+
code = self._emit_enum(name, schema)
|
|
84
|
+
models.append(code)
|
|
85
|
+
generated.add(name)
|
|
86
|
+
return
|
|
87
|
+
if schema.get("type") == "object" or schema.get("properties"):
|
|
88
|
+
props: Dict[str, Any] = schema.get("properties", {})
|
|
89
|
+
required = set(schema.get("required", []) or [])
|
|
90
|
+
lines = [f"class {name}(BaseModel):"]
|
|
91
|
+
if not props:
|
|
92
|
+
lines.append(" pass")
|
|
93
|
+
for prop_name, prop_schema in props.items():
|
|
94
|
+
if "$ref" in prop_schema:
|
|
95
|
+
ann_type = self._ref_name(prop_schema["$ref"])
|
|
96
|
+
elif prop_schema.get("type") == "object" and prop_schema.get(
|
|
97
|
+
"properties"
|
|
98
|
+
):
|
|
99
|
+
nested_name = f"{name}{to_pascal_case(prop_name)}"
|
|
100
|
+
self._emit_model(nested_name, prop_schema, generated, models)
|
|
101
|
+
ann_type = nested_name
|
|
102
|
+
elif prop_schema.get("type") == "array":
|
|
103
|
+
items = prop_schema.get("items") or {}
|
|
104
|
+
if "$ref" in items:
|
|
105
|
+
inner = self._ref_name(items["$ref"])
|
|
106
|
+
elif items.get("type") == "object" and items.get("properties"):
|
|
107
|
+
inner = f"{name}{to_pascal_case(prop_name)}Item"
|
|
108
|
+
self._emit_model(inner, items, generated, models)
|
|
109
|
+
else:
|
|
110
|
+
inner = self._python_type(items)
|
|
111
|
+
ann_type = f"list[{inner}]"
|
|
112
|
+
elif "enum" in prop_schema:
|
|
113
|
+
enum_name = f"{name}{to_pascal_case(prop_name)}Enum"
|
|
114
|
+
models.append(self._emit_enum(enum_name, prop_schema))
|
|
115
|
+
ann_type = enum_name
|
|
116
|
+
else:
|
|
117
|
+
ann_type = self._python_type(prop_schema)
|
|
118
|
+
opt = "" if prop_name in required else "Optional["
|
|
119
|
+
opt_close = "" if prop_name in required else "]"
|
|
120
|
+
lines.append(
|
|
121
|
+
f" {to_snake_case(prop_name)}: {opt}{ann_type}{opt_close}"
|
|
122
|
+
)
|
|
123
|
+
models.append("\n".join(lines) + "\n")
|
|
124
|
+
generated.add(name)
|
|
125
|
+
return
|
|
126
|
+
generated.add(name)
|
|
127
|
+
|
|
128
|
+
def _ensure_package(self, output_path: Path) -> None:
|
|
129
|
+
(output_path / "__init__.py").write_text(
|
|
130
|
+
"# Generated API package\n", encoding="utf-8"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _select_response_schema(
|
|
134
|
+
self, operation: Dict[str, Any]
|
|
135
|
+
) -> Optional[Dict[str, Any]]:
|
|
136
|
+
responses = operation.get("responses") or {}
|
|
137
|
+
for code in ("200", "201", "default"):
|
|
138
|
+
if code in responses:
|
|
139
|
+
content = (responses[code] or {}).get("content") or {}
|
|
140
|
+
app_json = content.get("application/json")
|
|
141
|
+
if app_json and isinstance(app_json, dict):
|
|
142
|
+
return app_json.get("schema")
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
def _request_body_schema(
|
|
146
|
+
self, operation: Dict[str, Any]
|
|
147
|
+
) -> Optional[Dict[str, Any]]:
|
|
148
|
+
rb = operation.get("requestBody") or {}
|
|
149
|
+
content = rb.get("content") or {}
|
|
150
|
+
app_json = content.get("application/json")
|
|
151
|
+
if app_json and isinstance(app_json, dict):
|
|
152
|
+
return app_json.get("schema")
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
def _resolve_type_for_operation(
|
|
156
|
+
self,
|
|
157
|
+
op_id: str,
|
|
158
|
+
role: str,
|
|
159
|
+
schema: Dict[str, Any],
|
|
160
|
+
generated: Set[str],
|
|
161
|
+
models: List[str],
|
|
162
|
+
) -> str:
|
|
163
|
+
# role: "Response" | "Request"
|
|
164
|
+
if "$ref" in schema:
|
|
165
|
+
return self._ref_name(schema["$ref"])
|
|
166
|
+
t = schema.get("type")
|
|
167
|
+
if t == "array":
|
|
168
|
+
items = schema.get("items") or {}
|
|
169
|
+
inner = self._resolve_type_for_operation(
|
|
170
|
+
op_id, role + "Item", items, generated, models
|
|
171
|
+
)
|
|
172
|
+
return f"list[{inner}]"
|
|
173
|
+
if t == "object" or schema.get("properties"):
|
|
174
|
+
model_name = f"{to_pascal_case(op_id)}{role}"
|
|
175
|
+
self._emit_model(model_name, schema, generated, models)
|
|
176
|
+
return model_name
|
|
177
|
+
if "enum" in schema:
|
|
178
|
+
enum_name = f"{to_pascal_case(op_id)}{role}Enum"
|
|
179
|
+
models.append(self._emit_enum(enum_name, schema))
|
|
180
|
+
generated.add(enum_name)
|
|
181
|
+
return enum_name
|
|
182
|
+
return self._python_type(schema)
|
|
183
|
+
|
|
184
|
+
def _generate_step_class(
|
|
185
|
+
self, path: str, method: str, operation: Dict[str, Any]
|
|
186
|
+
) -> Tuple[str, str]:
|
|
187
|
+
operation_id = operation.get("operationId")
|
|
188
|
+
if not operation_id:
|
|
189
|
+
clean_path = re.sub(r"[^a-zA-Z0-9]", "_", path)
|
|
190
|
+
operation_id = f"{method.lower()}{clean_path}"
|
|
191
|
+
class_name = to_pascal_case(operation_id)
|
|
192
|
+
file_name = f"{to_snake_case(class_name)}.py"
|
|
193
|
+
|
|
194
|
+
is_fact = method.upper() == "GET"
|
|
195
|
+
base_class = "Fact" if is_fact else "Action"
|
|
196
|
+
|
|
197
|
+
# Параметры
|
|
198
|
+
params = operation.get("parameters", [])
|
|
199
|
+
path_params = [p["name"] for p in params if p.get("in") == "path"]
|
|
200
|
+
query_params = [p["name"] for p in params if p.get("in") == "query"]
|
|
201
|
+
header_params = [p["name"] for p in params if p.get("in") == "header"]
|
|
202
|
+
|
|
203
|
+
# Описание
|
|
204
|
+
summary = operation.get("summary", f"Выполняет {method.upper()} {path}")
|
|
205
|
+
|
|
206
|
+
# Форматируем путь
|
|
207
|
+
formatted_path = path
|
|
208
|
+
for p in path_params:
|
|
209
|
+
formatted_path = formatted_path.replace(
|
|
210
|
+
f"{{{p}}}", f"{{self.{to_snake_case(p)}}}"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Типы ответа/тела
|
|
214
|
+
imports_models: Set[str] = set()
|
|
215
|
+
response_schema = self._select_response_schema(operation)
|
|
216
|
+
response_type = "Any"
|
|
217
|
+
if response_schema:
|
|
218
|
+
if "$ref" in response_schema:
|
|
219
|
+
response_type = self._ref_name(response_schema["$ref"])
|
|
220
|
+
imports_models.add(response_type)
|
|
221
|
+
elif response_schema.get("type") == "array":
|
|
222
|
+
items = response_schema.get("items") or {}
|
|
223
|
+
if "$ref" in items:
|
|
224
|
+
inner = self._ref_name(items["$ref"])
|
|
225
|
+
response_type = f"list[{inner}]"
|
|
226
|
+
imports_models.add(inner)
|
|
227
|
+
else:
|
|
228
|
+
response_type = f"list[{to_pascal_case(operation_id)}ResponseItem]"
|
|
229
|
+
imports_models.add(f"{to_pascal_case(operation_id)}ResponseItem")
|
|
230
|
+
elif response_schema.get("type") == "object" or response_schema.get(
|
|
231
|
+
"properties"
|
|
232
|
+
):
|
|
233
|
+
response_type = f"{to_pascal_case(operation_id)}Response"
|
|
234
|
+
imports_models.add(response_type)
|
|
235
|
+
elif "enum" in response_schema:
|
|
236
|
+
response_type = f"{to_pascal_case(operation_id)}ResponseEnum"
|
|
237
|
+
imports_models.add(response_type)
|
|
238
|
+
else:
|
|
239
|
+
t = response_schema.get("type")
|
|
240
|
+
if t == "string":
|
|
241
|
+
response_type = "str"
|
|
242
|
+
elif t == "integer":
|
|
243
|
+
response_type = "int"
|
|
244
|
+
elif t == "number":
|
|
245
|
+
response_type = "float"
|
|
246
|
+
elif t == "boolean":
|
|
247
|
+
response_type = "bool"
|
|
248
|
+
else:
|
|
249
|
+
response_type = "Any"
|
|
250
|
+
|
|
251
|
+
request_schema = self._request_body_schema(operation)
|
|
252
|
+
request_type: Optional[str] = None
|
|
253
|
+
if request_schema:
|
|
254
|
+
if "$ref" in request_schema:
|
|
255
|
+
request_type = self._ref_name(request_schema["$ref"])
|
|
256
|
+
imports_models.add(request_type)
|
|
257
|
+
elif request_schema.get("type") == "array":
|
|
258
|
+
items = request_schema.get("items") or {}
|
|
259
|
+
if "$ref" in items:
|
|
260
|
+
inner = self._ref_name(items["$ref"])
|
|
261
|
+
request_type = f"list[{inner}]"
|
|
262
|
+
imports_models.add(inner)
|
|
263
|
+
else:
|
|
264
|
+
request_type = f"list[{to_pascal_case(operation_id)}RequestItem]"
|
|
265
|
+
imports_models.add(f"{to_pascal_case(operation_id)}RequestItem")
|
|
266
|
+
elif request_schema.get("type") == "object" or request_schema.get(
|
|
267
|
+
"properties"
|
|
268
|
+
):
|
|
269
|
+
request_type = f"{to_pascal_case(operation_id)}Request"
|
|
270
|
+
imports_models.add(request_type)
|
|
271
|
+
elif "enum" in request_schema:
|
|
272
|
+
request_type = f"{to_pascal_case(operation_id)}RequestEnum"
|
|
273
|
+
imports_models.add(request_type)
|
|
274
|
+
else:
|
|
275
|
+
t = request_schema.get("type")
|
|
276
|
+
if t == "string":
|
|
277
|
+
request_type = "str"
|
|
278
|
+
elif t == "integer":
|
|
279
|
+
request_type = "int"
|
|
280
|
+
elif t == "number":
|
|
281
|
+
request_type = "float"
|
|
282
|
+
elif t == "boolean":
|
|
283
|
+
request_type = "bool"
|
|
284
|
+
else:
|
|
285
|
+
request_type = "Any"
|
|
286
|
+
|
|
287
|
+
# Импорты
|
|
288
|
+
imports: List[str] = ["from typing import Any"]
|
|
289
|
+
if base_class == "Fact":
|
|
290
|
+
imports.append("from persona_dsl.components.fact import Fact")
|
|
291
|
+
else:
|
|
292
|
+
imports.append("from persona_dsl.components.action import Action")
|
|
293
|
+
imports.append("from persona_dsl.ops.api import JsonAs")
|
|
294
|
+
if imports_models:
|
|
295
|
+
imports.append("from .models import " + ", ".join(sorted(imports_models)))
|
|
296
|
+
imports_block = "\n".join(imports) + "\n\n"
|
|
297
|
+
|
|
298
|
+
# __init__
|
|
299
|
+
init_args: List[str] = []
|
|
300
|
+
init_body: List[str] = []
|
|
301
|
+
for p in path_params + query_params + header_params:
|
|
302
|
+
init_args.append(f"{to_snake_case(p)}: Any")
|
|
303
|
+
init_body.append(f" self.{to_snake_case(p)} = {to_snake_case(p)}")
|
|
304
|
+
if request_type:
|
|
305
|
+
init_args.append(f"json_body: {request_type}")
|
|
306
|
+
init_body.append(" self.json_body = json_body")
|
|
307
|
+
init_sig = ", ".join(init_args) if init_args else ""
|
|
308
|
+
init_method = f" def __init__(self, {init_sig}):\n" + (
|
|
309
|
+
"\n".join(init_body) + "\n" if init_body else " pass\n"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# _perform
|
|
313
|
+
ops_args = [
|
|
314
|
+
f" method='{method.upper()}',",
|
|
315
|
+
f" path=f'{formatted_path}',",
|
|
316
|
+
f" model_type={response_type},",
|
|
317
|
+
]
|
|
318
|
+
if query_params:
|
|
319
|
+
params_dict = ", ".join(
|
|
320
|
+
[f"'{p}': self.{to_snake_case(p)}" for p in query_params]
|
|
321
|
+
)
|
|
322
|
+
ops_args.append(f" params={{{{ {params_dict} }}}},")
|
|
323
|
+
if header_params:
|
|
324
|
+
headers_dict = ", ".join(
|
|
325
|
+
[f"'{p}': self.{to_snake_case(p)}" for p in header_params]
|
|
326
|
+
)
|
|
327
|
+
ops_args.append(f" headers={{{{ {headers_dict} }}}},")
|
|
328
|
+
if request_type:
|
|
329
|
+
ops_args.append(" json=self.json_body,")
|
|
330
|
+
perform_body = (
|
|
331
|
+
" return JsonAs(\n"
|
|
332
|
+
+ "\n".join(ops_args)
|
|
333
|
+
+ "\n ).execute(persona)"
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
code = f"""{imports_block}class {class_name}({base_class}):
|
|
337
|
+
'''
|
|
338
|
+
{summary}
|
|
339
|
+
'''
|
|
340
|
+
|
|
341
|
+
{init_method}
|
|
342
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
343
|
+
return f"{{persona}} {summary.lower()}"
|
|
344
|
+
|
|
345
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> Any:
|
|
346
|
+
{perform_body}
|
|
347
|
+
"""
|
|
348
|
+
return file_name, code
|
|
349
|
+
|
|
350
|
+
def generate_from_spec(self, spec_path: str, output_dir: str) -> None:
|
|
351
|
+
spec = self._load_spec(spec_path)
|
|
352
|
+
output_path = Path(output_dir)
|
|
353
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
354
|
+
self._ensure_package(output_path)
|
|
355
|
+
|
|
356
|
+
# 1) Генерация моделей из components/schemas
|
|
357
|
+
generated: Set[str] = set()
|
|
358
|
+
models: List[str] = [
|
|
359
|
+
"# Generated models\n",
|
|
360
|
+
"from enum import Enum\n",
|
|
361
|
+
"from typing import Any, Optional, List, Dict\n",
|
|
362
|
+
"from pydantic import BaseModel\n",
|
|
363
|
+
"\n",
|
|
364
|
+
]
|
|
365
|
+
components = spec.get("components", {})
|
|
366
|
+
schemas = components.get("schemas", {})
|
|
367
|
+
for name, schema in schemas.items():
|
|
368
|
+
self._emit_model(name, schema or {}, generated, models)
|
|
369
|
+
|
|
370
|
+
# 2) Предварительная генерация инлайн-моделей для операций
|
|
371
|
+
for path, path_item in (spec.get("paths") or {}).items():
|
|
372
|
+
for method, operation in (path_item or {}).items():
|
|
373
|
+
if method.lower() not in [
|
|
374
|
+
"get",
|
|
375
|
+
"post",
|
|
376
|
+
"put",
|
|
377
|
+
"patch",
|
|
378
|
+
"delete",
|
|
379
|
+
"head",
|
|
380
|
+
"options",
|
|
381
|
+
]:
|
|
382
|
+
continue
|
|
383
|
+
op_id = (
|
|
384
|
+
operation.get("operationId")
|
|
385
|
+
or f"{method.lower()}_{to_snake_case(path.strip('/').replace('/', '_'))}"
|
|
386
|
+
)
|
|
387
|
+
resp_schema = self._select_response_schema(operation)
|
|
388
|
+
if resp_schema:
|
|
389
|
+
self._resolve_type_for_operation(
|
|
390
|
+
op_id, "Response", resp_schema, generated, models
|
|
391
|
+
)
|
|
392
|
+
req_schema = self._request_body_schema(operation)
|
|
393
|
+
if req_schema:
|
|
394
|
+
self._resolve_type_for_operation(
|
|
395
|
+
op_id, "Request", req_schema, generated, models
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
# Записываем models.py
|
|
399
|
+
(output_path / "models.py").write_text("\n".join(models), encoding="utf-8")
|
|
400
|
+
|
|
401
|
+
print(f"Генерация API Steps из '{spec_path}' в '{output_dir}'...")
|
|
402
|
+
|
|
403
|
+
# 3) Генерация Step-классов
|
|
404
|
+
for path, path_item in (spec.get("paths") or {}).items():
|
|
405
|
+
for method, operation in (path_item or {}).items():
|
|
406
|
+
if method.lower() not in [
|
|
407
|
+
"get",
|
|
408
|
+
"post",
|
|
409
|
+
"put",
|
|
410
|
+
"patch",
|
|
411
|
+
"delete",
|
|
412
|
+
"head",
|
|
413
|
+
"options",
|
|
414
|
+
]:
|
|
415
|
+
continue
|
|
416
|
+
try:
|
|
417
|
+
file_name, code = self._generate_step_class(path, method, operation)
|
|
418
|
+
(output_path / file_name).write_text(code, encoding="utf-8")
|
|
419
|
+
print(f" - Создан файл: {file_name}")
|
|
420
|
+
except Exception as e:
|
|
421
|
+
print(f" - Ошибка генерации для {method.upper()} {path}: {e}")
|
|
422
|
+
|
|
423
|
+
print("Генерация завершена.")
|