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.
Files changed (86) hide show
  1. persona_dsl/__init__.py +35 -0
  2. persona_dsl/components/action.py +10 -0
  3. persona_dsl/components/base_step.py +251 -0
  4. persona_dsl/components/combined_step.py +68 -0
  5. persona_dsl/components/expectation.py +10 -0
  6. persona_dsl/components/fact.py +10 -0
  7. persona_dsl/components/goal.py +10 -0
  8. persona_dsl/components/ops.py +7 -0
  9. persona_dsl/components/step.py +75 -0
  10. persona_dsl/expectations/generic/__init__.py +15 -0
  11. persona_dsl/expectations/generic/contains_item.py +19 -0
  12. persona_dsl/expectations/generic/contains_the_text.py +15 -0
  13. persona_dsl/expectations/generic/has_entries.py +21 -0
  14. persona_dsl/expectations/generic/is_equal.py +24 -0
  15. persona_dsl/expectations/generic/is_greater_than.py +18 -0
  16. persona_dsl/expectations/generic/path_equal.py +27 -0
  17. persona_dsl/expectations/web/__init__.py +5 -0
  18. persona_dsl/expectations/web/is_displayed.py +13 -0
  19. persona_dsl/expectations/web/matches_aria_snapshot.py +221 -0
  20. persona_dsl/expectations/web/matches_screenshot.py +160 -0
  21. persona_dsl/generators/__init__.py +5 -0
  22. persona_dsl/generators/api_generator.py +423 -0
  23. persona_dsl/generators/cli.py +431 -0
  24. persona_dsl/generators/page_generator.py +1140 -0
  25. persona_dsl/ops/api/__init__.py +5 -0
  26. persona_dsl/ops/api/json_as.py +104 -0
  27. persona_dsl/ops/api/json_response.py +48 -0
  28. persona_dsl/ops/api/send_request.py +41 -0
  29. persona_dsl/ops/db/__init__.py +5 -0
  30. persona_dsl/ops/db/execute_sql.py +22 -0
  31. persona_dsl/ops/db/fetch_all.py +29 -0
  32. persona_dsl/ops/db/fetch_one.py +22 -0
  33. persona_dsl/ops/kafka/__init__.py +4 -0
  34. persona_dsl/ops/kafka/message_in_topic.py +89 -0
  35. persona_dsl/ops/kafka/send_message.py +35 -0
  36. persona_dsl/ops/soap/__init__.py +4 -0
  37. persona_dsl/ops/soap/call_operation.py +24 -0
  38. persona_dsl/ops/soap/operation_result.py +24 -0
  39. persona_dsl/ops/web/__init__.py +37 -0
  40. persona_dsl/ops/web/aria_snapshot.py +87 -0
  41. persona_dsl/ops/web/click.py +30 -0
  42. persona_dsl/ops/web/current_path.py +17 -0
  43. persona_dsl/ops/web/element_attribute.py +24 -0
  44. persona_dsl/ops/web/element_is_visible.py +27 -0
  45. persona_dsl/ops/web/element_text.py +28 -0
  46. persona_dsl/ops/web/elements_count.py +42 -0
  47. persona_dsl/ops/web/fill.py +41 -0
  48. persona_dsl/ops/web/generate_page_object.py +118 -0
  49. persona_dsl/ops/web/input_value.py +23 -0
  50. persona_dsl/ops/web/navigate.py +52 -0
  51. persona_dsl/ops/web/press_key.py +37 -0
  52. persona_dsl/ops/web/rich_aria_snapshot.py +146 -0
  53. persona_dsl/ops/web/screenshot.py +68 -0
  54. persona_dsl/ops/web/table_data.py +43 -0
  55. persona_dsl/ops/web/wait_for_navigation.py +23 -0
  56. persona_dsl/pages/__init__.py +133 -0
  57. persona_dsl/pages/elements.py +998 -0
  58. persona_dsl/pages/page.py +44 -0
  59. persona_dsl/pages/virtual_page.py +94 -0
  60. persona_dsl/persona.py +125 -0
  61. persona_dsl/pytest_plugin.py +1064 -0
  62. persona_dsl/runtime/dist/persona_bundle.js +1077 -0
  63. persona_dsl/skills/__init__.py +7 -0
  64. persona_dsl/skills/core/base.py +41 -0
  65. persona_dsl/skills/core/skill_definition.py +30 -0
  66. persona_dsl/skills/use_api.py +251 -0
  67. persona_dsl/skills/use_browser.py +78 -0
  68. persona_dsl/skills/use_database.py +129 -0
  69. persona_dsl/skills/use_kafka.py +135 -0
  70. persona_dsl/skills/use_soap.py +66 -0
  71. persona_dsl/utils/__init__.py +0 -0
  72. persona_dsl/utils/artifacts.py +22 -0
  73. persona_dsl/utils/config.py +54 -0
  74. persona_dsl/utils/data_providers.py +159 -0
  75. persona_dsl/utils/decorators.py +80 -0
  76. persona_dsl/utils/metrics.py +69 -0
  77. persona_dsl/utils/naming.py +14 -0
  78. persona_dsl/utils/path.py +202 -0
  79. persona_dsl/utils/retry.py +51 -0
  80. persona_dsl/utils/taas_integration.py +124 -0
  81. persona_dsl/utils/waits.py +112 -0
  82. persona_dsl-26.1.20.8.dist-info/METADATA +35 -0
  83. persona_dsl-26.1.20.8.dist-info/RECORD +86 -0
  84. persona_dsl-26.1.20.8.dist-info/WHEEL +5 -0
  85. persona_dsl-26.1.20.8.dist-info/entry_points.txt +6 -0
  86. 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("Генерация завершена.")