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,431 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from .page_generator import PageGenerator
|
|
9
|
+
from .api_generator import ApiGenerator
|
|
10
|
+
from persona_dsl.utils.naming import to_pascal_case, to_snake_case
|
|
11
|
+
from unidecode import unidecode
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
ENV_FILE_NAME = ".persona_gen.env"
|
|
15
|
+
ENV_KEY_HEADLESS = "PERSONA_GEN_HEADLESS"
|
|
16
|
+
ENV_KEY_BROWSER_EXEC = "PERSONA_GEN_BROWSER_EXECUTABLE"
|
|
17
|
+
ENV_KEY_DEFAULT_URL = "PERSONA_GEN_DEFAULT_URL"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _prompt_non_empty(prompt: str, default: str | None = None) -> str:
|
|
21
|
+
while True:
|
|
22
|
+
raw = input(prompt).strip()
|
|
23
|
+
if raw:
|
|
24
|
+
return raw
|
|
25
|
+
if default is not None:
|
|
26
|
+
return default
|
|
27
|
+
print("Значение не может быть пустым. Повторите ввод.")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _prompt_yes_no(prompt: str, default: bool = True) -> bool:
|
|
31
|
+
suffix = "[Y/n]" if default else "[y/N]"
|
|
32
|
+
while True:
|
|
33
|
+
raw = input(f"{prompt} {suffix}: ").strip().lower()
|
|
34
|
+
if not raw:
|
|
35
|
+
return default
|
|
36
|
+
if raw in ("y", "yes"):
|
|
37
|
+
return True
|
|
38
|
+
if raw in ("n", "no"):
|
|
39
|
+
return False
|
|
40
|
+
print("Введите 'y' или 'n'.")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _suggest_class_name_from_title(title: str | None) -> str:
|
|
44
|
+
if title:
|
|
45
|
+
ascii_title = unidecode(title)
|
|
46
|
+
candidate = to_pascal_case(ascii_title)
|
|
47
|
+
if not candidate:
|
|
48
|
+
candidate = "Generated"
|
|
49
|
+
if not candidate.endswith("Page"):
|
|
50
|
+
candidate += "Page"
|
|
51
|
+
return candidate
|
|
52
|
+
return "GeneratedPage"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _load_env_config(env_path: Path) -> Dict[str, str]:
|
|
56
|
+
cfg: Dict[str, str] = {}
|
|
57
|
+
if env_path.exists() and env_path.is_file():
|
|
58
|
+
try:
|
|
59
|
+
with open(env_path, "r", encoding="utf-8") as f:
|
|
60
|
+
for line in f:
|
|
61
|
+
line = line.strip()
|
|
62
|
+
if not line or line.startswith("#"):
|
|
63
|
+
continue
|
|
64
|
+
if "=" in line:
|
|
65
|
+
k, v = line.split("=", 1)
|
|
66
|
+
cfg[k.strip()] = v.strip()
|
|
67
|
+
except Exception:
|
|
68
|
+
# Не маскируем ошибки в логике генерации; просто игнорируем битый env и пересохраняем позже
|
|
69
|
+
pass
|
|
70
|
+
return cfg
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _save_env_config(env_path: Path, cfg: Dict[str, str]) -> None:
|
|
74
|
+
try:
|
|
75
|
+
lines = [
|
|
76
|
+
"# Persona DSL Generators settings\n",
|
|
77
|
+
f"{ENV_KEY_HEADLESS}={cfg.get(ENV_KEY_HEADLESS, 'true')}\n",
|
|
78
|
+
f"{ENV_KEY_BROWSER_EXEC}={cfg.get(ENV_KEY_BROWSER_EXEC, '')}\n",
|
|
79
|
+
f"{ENV_KEY_DEFAULT_URL}={cfg.get(ENV_KEY_DEFAULT_URL, '')}\n",
|
|
80
|
+
]
|
|
81
|
+
env_path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
with open(env_path, "w", encoding="utf-8") as f:
|
|
83
|
+
f.writelines(lines)
|
|
84
|
+
print(f"Настройки сохранены в {env_path}")
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print(
|
|
87
|
+
f"Предупреждение: не удалось сохранить настройки в {env_path}: {e}",
|
|
88
|
+
file=sys.stderr,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def page_main() -> None:
|
|
93
|
+
"""Entry point для persona-page-gen (интерактивный при отсутствии аргументов)."""
|
|
94
|
+
parser = argparse.ArgumentParser(
|
|
95
|
+
description="Генерация класса Page из ARIA-снепшота открытой страницы."
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument("--url", help="URL страницы для анализа.")
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"--output-path", help="Путь для сохранения сгенерированного Python-файла."
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument("--class-name", help="Имя для генерируемого класса страницы.")
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
"--env-file",
|
|
104
|
+
help=f"Путь к env-файлу настроек (по умолчанию ./{ENV_FILE_NAME}).",
|
|
105
|
+
)
|
|
106
|
+
args = parser.parse_args()
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
from playwright.sync_api import sync_playwright
|
|
110
|
+
except ImportError:
|
|
111
|
+
print(
|
|
112
|
+
"Ошибка: 'playwright' не установлен. Пожалуйста, установите его: pip install playwright",
|
|
113
|
+
file=sys.stderr,
|
|
114
|
+
)
|
|
115
|
+
sys.exit(1)
|
|
116
|
+
|
|
117
|
+
# 1) Загрузка и интерактивная настройка генератора до запроса URL
|
|
118
|
+
env_path = Path(args.env_file) if args.env_file else Path.cwd() / ENV_FILE_NAME
|
|
119
|
+
cfg = _load_env_config(env_path)
|
|
120
|
+
|
|
121
|
+
# Headless
|
|
122
|
+
cur_headless = cfg.get(ENV_KEY_HEADLESS, "true").lower() in (
|
|
123
|
+
"1",
|
|
124
|
+
"true",
|
|
125
|
+
"yes",
|
|
126
|
+
"y",
|
|
127
|
+
)
|
|
128
|
+
headless = _prompt_yes_no(
|
|
129
|
+
"Запускать браузер в фоне (headless)?", default=cur_headless
|
|
130
|
+
)
|
|
131
|
+
cfg[ENV_KEY_HEADLESS] = "true" if headless else "false"
|
|
132
|
+
|
|
133
|
+
# Проверка на наличие X-сервера, если пользователь выбрал не-headless режим.
|
|
134
|
+
is_display_available = bool(os.environ.get("DISPLAY"))
|
|
135
|
+
if not headless and not is_display_available:
|
|
136
|
+
print(
|
|
137
|
+
"\n\033[93mПредупреждение: Графический интерфейс (X-сервер) недоступен.\033[0m"
|
|
138
|
+
)
|
|
139
|
+
print(
|
|
140
|
+
"Браузер будет принудительно запущен в фоновом (headless) режиме, чтобы избежать ошибки."
|
|
141
|
+
)
|
|
142
|
+
headless = True
|
|
143
|
+
|
|
144
|
+
# Browser executable path
|
|
145
|
+
cur_exec = cfg.get(ENV_KEY_BROWSER_EXEC, "")
|
|
146
|
+
prompt_exec = input(
|
|
147
|
+
"Укажите путь к исполняемому файлу браузера для Playwright (пусто — использовать встроенный):\n"
|
|
148
|
+
f"[Текущее значение: '{cur_exec or 'не задано'}'] Введите новое значение или оставьте пустым: "
|
|
149
|
+
).strip()
|
|
150
|
+
browser_executable = prompt_exec if prompt_exec else cur_exec
|
|
151
|
+
cfg[ENV_KEY_BROWSER_EXEC] = browser_executable
|
|
152
|
+
|
|
153
|
+
# Новые вопросы
|
|
154
|
+
save_aria = _prompt_yes_no("Сохранить ARIA-снепшот в отдельный файл?", default=True)
|
|
155
|
+
save_screenshot = _prompt_yes_no(
|
|
156
|
+
"Сделать и сохранить скриншот страницы?", default=False
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
_save_env_config(env_path, cfg)
|
|
160
|
+
|
|
161
|
+
# 2) Теперь спрашиваем URL (если не передан)
|
|
162
|
+
cur_url = cfg.get(ENV_KEY_DEFAULT_URL, "")
|
|
163
|
+
url = args.url or _prompt_non_empty(
|
|
164
|
+
f"Введите URL страницы для анализа [по умолчанию: '{cur_url or 'не задан'}']: ",
|
|
165
|
+
default=cur_url if cur_url else None,
|
|
166
|
+
)
|
|
167
|
+
cfg[ENV_KEY_DEFAULT_URL] = url
|
|
168
|
+
_save_env_config(env_path, cfg)
|
|
169
|
+
|
|
170
|
+
# 3) Открываем страницу, чтобы считать заголовок и предложить имя класса
|
|
171
|
+
print(f"Открытие страницы {url} в браузере...")
|
|
172
|
+
with sync_playwright() as p:
|
|
173
|
+
launch_kwargs: Dict[str, Any] = {"headless": headless}
|
|
174
|
+
if browser_executable:
|
|
175
|
+
launch_kwargs["executable_path"] = browser_executable
|
|
176
|
+
browser = p.chromium.launch(**launch_kwargs)
|
|
177
|
+
page: Any = browser.new_page()
|
|
178
|
+
try:
|
|
179
|
+
page.goto(url, wait_until="domcontentloaded", timeout=60000)
|
|
180
|
+
print("Страница открыта. Вы можете взаимодействовать с ней.")
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
title = page.title()
|
|
184
|
+
except Exception:
|
|
185
|
+
title = None
|
|
186
|
+
suggested_class_name = args.class_name or _suggest_class_name_from_title(
|
|
187
|
+
title
|
|
188
|
+
)
|
|
189
|
+
class_name = args.class_name or _prompt_non_empty(
|
|
190
|
+
f"Введите имя класса страницы [по умолчанию: {suggested_class_name}]: ",
|
|
191
|
+
default=suggested_class_name,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
default_output = Path("tests/pages") / f"{to_snake_case(class_name)}.py"
|
|
195
|
+
output_path_arg = args.output_path or _prompt_non_empty(
|
|
196
|
+
f"Введите путь для сохранения Python-файла [по умолчанию: {default_output}]: ",
|
|
197
|
+
default=str(default_output),
|
|
198
|
+
)
|
|
199
|
+
output_path = Path(output_path_arg)
|
|
200
|
+
|
|
201
|
+
confirm_params = (
|
|
202
|
+
input(
|
|
203
|
+
"\nПараметры генерации:\n"
|
|
204
|
+
f"- Headless: {headless}\n"
|
|
205
|
+
f"- Browser exe: {browser_executable or 'builtin'}\n"
|
|
206
|
+
f"- URL: {url}\n"
|
|
207
|
+
f"- Class name: {class_name}\n"
|
|
208
|
+
f"- Output path: {output_path}\n"
|
|
209
|
+
f"- Save ARIA snapshot: {save_aria}\n"
|
|
210
|
+
f"- Save screenshot: {save_screenshot}\n"
|
|
211
|
+
"Продолжить с этими параметрами? [Y/n]: "
|
|
212
|
+
)
|
|
213
|
+
.lower()
|
|
214
|
+
.strip()
|
|
215
|
+
or "y"
|
|
216
|
+
)
|
|
217
|
+
if confirm_params not in ("y", "yes"):
|
|
218
|
+
print("Отменено пользователем.")
|
|
219
|
+
sys.exit(0)
|
|
220
|
+
|
|
221
|
+
input(
|
|
222
|
+
"Нажмите Enter в этой консоли, когда будете готовы сделать ARIA-снепшот..."
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
print("Ожидание полной загрузки страницы (networkidle)...")
|
|
226
|
+
try:
|
|
227
|
+
page.wait_for_load_state("networkidle", timeout=60000)
|
|
228
|
+
except Exception as e:
|
|
229
|
+
print(
|
|
230
|
+
f"Предупреждение: не удалось дождаться состояния 'networkidle': {e}"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
print("Создание rich ARIA-снепшота...")
|
|
234
|
+
# Inject Runtime
|
|
235
|
+
try:
|
|
236
|
+
runtime_path = (
|
|
237
|
+
Path(__file__).parent.parent / "runtime" / "persona_runtime.js"
|
|
238
|
+
)
|
|
239
|
+
if runtime_path.exists():
|
|
240
|
+
js_content = runtime_path.read_text(encoding="utf-8")
|
|
241
|
+
page.evaluate(js_content)
|
|
242
|
+
else:
|
|
243
|
+
print(
|
|
244
|
+
f"Предупреждение: Persona Runtime не найден по пути {runtime_path}. Инъекция пропущена.",
|
|
245
|
+
file=sys.stderr,
|
|
246
|
+
)
|
|
247
|
+
except Exception as e:
|
|
248
|
+
print(f"Ошибка загрузки Persona Runtime: {e}", file=sys.stderr)
|
|
249
|
+
# Non-fatal? Or sys.exit?
|
|
250
|
+
# Original code exit(1). Let's keep it but allow skip if just missing?
|
|
251
|
+
# If missing -> warning. If evaluate fails -> error.
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
snapshot = page.evaluate(
|
|
255
|
+
"window.__PERSONA__ && window.__PERSONA__.getTreeForTransport()"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if not snapshot:
|
|
259
|
+
# Fallback/Debug
|
|
260
|
+
print(
|
|
261
|
+
"Warning: getTreeForTransport returned null. Trying getRichAriaSnapshot..."
|
|
262
|
+
)
|
|
263
|
+
snapshot = page.evaluate(
|
|
264
|
+
"window.__PERSONA__ && window.__PERSONA__.getRichAriaSnapshot()"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# RichAriaSnapshot has _tree_to_yaml but it is instance method?
|
|
268
|
+
# It takes root.
|
|
269
|
+
# We need standard yaml dump for saving?
|
|
270
|
+
# Existing code used yaml.dump(snapshot).
|
|
271
|
+
|
|
272
|
+
print(f"Получен rich снепшот (тип: {type(snapshot).__name__})")
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
axtree = page.accessibility.snapshot()
|
|
276
|
+
print("AXTree snapshot получен.")
|
|
277
|
+
except Exception as e:
|
|
278
|
+
raise RuntimeError(
|
|
279
|
+
f"page.accessibility.snapshot() недоступен или завершился с ошибкой: {e}"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if isinstance(snapshot, list):
|
|
283
|
+
print(f"Снепшот содержит {len(snapshot)} корневых элементов")
|
|
284
|
+
for i, item in enumerate(snapshot[:3]):
|
|
285
|
+
print(f" Элемент {i}: {type(item).__name__}")
|
|
286
|
+
if isinstance(item, dict):
|
|
287
|
+
print(f" Ключи: {list(item.keys())}")
|
|
288
|
+
elif isinstance(snapshot, dict):
|
|
289
|
+
print(f"Снепшот содержит ключи: {list(snapshot.keys())}")
|
|
290
|
+
|
|
291
|
+
if not snapshot:
|
|
292
|
+
print("Предупреждение: снепшот пустой или не содержит данных")
|
|
293
|
+
print("Снепшот создан.")
|
|
294
|
+
|
|
295
|
+
assets_dir = output_path.parent / "aria"
|
|
296
|
+
assets_dir.mkdir(parents=True, exist_ok=True)
|
|
297
|
+
aria_snapshot_rel_path: str | None = None
|
|
298
|
+
screenshot_rel_path: str | None = None
|
|
299
|
+
axtree_snapshot_rel_path: str | None = None
|
|
300
|
+
|
|
301
|
+
if save_screenshot:
|
|
302
|
+
screenshot_file = f"{output_path.stem}_screenshot.png"
|
|
303
|
+
screenshot_abs_path = assets_dir / screenshot_file
|
|
304
|
+
page.screenshot(path=screenshot_abs_path, full_page=True)
|
|
305
|
+
screenshot_rel_path = f"aria/{screenshot_file}"
|
|
306
|
+
print(f"Скриншот сохранен: {screenshot_abs_path}")
|
|
307
|
+
|
|
308
|
+
if save_aria:
|
|
309
|
+
snapshot_file = f"{output_path.stem}_snapshot.yaml"
|
|
310
|
+
snapshot_abs_path = assets_dir / snapshot_file
|
|
311
|
+
with open(snapshot_abs_path, "w", encoding="utf-8") as f:
|
|
312
|
+
yaml.dump(snapshot, f, default_flow_style=False, allow_unicode=True)
|
|
313
|
+
aria_snapshot_rel_path = f"aria/{snapshot_file}"
|
|
314
|
+
print(f"ARIA-снепшот сохранен: {snapshot_abs_path}")
|
|
315
|
+
|
|
316
|
+
axtree_file = f"{output_path.stem}_axtree.yaml"
|
|
317
|
+
axtree_abs_path = assets_dir / axtree_file
|
|
318
|
+
with open(axtree_abs_path, "w", encoding="utf-8") as f:
|
|
319
|
+
yaml.dump(axtree, f, default_flow_style=False, allow_unicode=True)
|
|
320
|
+
axtree_snapshot_rel_path = f"aria/{axtree_file}"
|
|
321
|
+
print(f"AXTree-снепшот сохранен: {axtree_abs_path}")
|
|
322
|
+
|
|
323
|
+
finally:
|
|
324
|
+
browser.close()
|
|
325
|
+
|
|
326
|
+
generator = PageGenerator()
|
|
327
|
+
from urllib.parse import urlparse
|
|
328
|
+
|
|
329
|
+
page_path = urlparse(url).path
|
|
330
|
+
|
|
331
|
+
existing_code: str | None = None
|
|
332
|
+
try:
|
|
333
|
+
if output_path.exists():
|
|
334
|
+
existing_code = output_path.read_text(encoding="utf-8")
|
|
335
|
+
except Exception:
|
|
336
|
+
existing_code = None
|
|
337
|
+
|
|
338
|
+
generated_code = generator.generate_or_update_from_aria_snapshot(
|
|
339
|
+
snapshot=snapshot,
|
|
340
|
+
class_name=class_name,
|
|
341
|
+
page_path=page_path,
|
|
342
|
+
aria_snapshot_path=aria_snapshot_rel_path,
|
|
343
|
+
screenshot_path=screenshot_rel_path,
|
|
344
|
+
existing_code=existing_code,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
_ = axtree_snapshot_rel_path
|
|
348
|
+
|
|
349
|
+
print("\n--- Сгенерированный код ---")
|
|
350
|
+
print(generated_code)
|
|
351
|
+
print("---------------------------\n")
|
|
352
|
+
|
|
353
|
+
final_output = input(
|
|
354
|
+
f"Сохранить этот код в файл '{output_path}'? [Y/n] (или укажите другой путь): "
|
|
355
|
+
).strip()
|
|
356
|
+
|
|
357
|
+
if final_output and final_output.lower() not in ("y", "yes", "n", "no"):
|
|
358
|
+
output_path = Path(final_output)
|
|
359
|
+
save_confirm = (
|
|
360
|
+
input(f"Подтвердите сохранение в '{output_path}' [Y/n]: ").lower().strip()
|
|
361
|
+
or "y"
|
|
362
|
+
)
|
|
363
|
+
if save_confirm not in ("y", "yes"):
|
|
364
|
+
print("Сохранение отменено.")
|
|
365
|
+
sys.exit(0)
|
|
366
|
+
elif not final_output or final_output.lower() in ("y", "yes"):
|
|
367
|
+
pass
|
|
368
|
+
else:
|
|
369
|
+
print("Сохранение отменено.")
|
|
370
|
+
sys.exit(0)
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
374
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
375
|
+
f.write(generated_code)
|
|
376
|
+
print(f"Код успешно сохранен в {output_path}")
|
|
377
|
+
except IOError as e:
|
|
378
|
+
print(f"Ошибка: не удалось сохранить файл: {e}", file=sys.stderr)
|
|
379
|
+
sys.exit(1)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def api_main() -> None:
|
|
383
|
+
"""Entry point для persona-api-gen (интерактивные вопросы при отсутствии аргументов)."""
|
|
384
|
+
parser = argparse.ArgumentParser(
|
|
385
|
+
description="Генерация API Steps из спецификации OpenAPI."
|
|
386
|
+
)
|
|
387
|
+
parser.add_argument(
|
|
388
|
+
"--spec", help="Путь к файлу спецификации OpenAPI (YAML или JSON)."
|
|
389
|
+
)
|
|
390
|
+
parser.add_argument(
|
|
391
|
+
"--output-dir", help="Директория для сохранения сгенерированных файлов."
|
|
392
|
+
)
|
|
393
|
+
args = parser.parse_args()
|
|
394
|
+
|
|
395
|
+
spec_path_str = args.spec or _prompt_non_empty(
|
|
396
|
+
"Введите путь к спецификации OpenAPI (YAML/JSON): "
|
|
397
|
+
)
|
|
398
|
+
spec_path = Path(spec_path_str)
|
|
399
|
+
while not spec_path.exists() or not spec_path.is_file():
|
|
400
|
+
print(f"Файл не найден: {spec_path}")
|
|
401
|
+
spec_path_str = _prompt_non_empty(
|
|
402
|
+
"Укажите корректный путь к спецификации OpenAPI: "
|
|
403
|
+
)
|
|
404
|
+
spec_path = Path(spec_path_str)
|
|
405
|
+
|
|
406
|
+
output_dir_str = args.output_dir or _prompt_non_empty(
|
|
407
|
+
"Введите директорию для генерации файлов (например, tests/api_steps): "
|
|
408
|
+
)
|
|
409
|
+
output_dir = Path(output_dir_str)
|
|
410
|
+
|
|
411
|
+
print("\nПараметры генерации:")
|
|
412
|
+
print(f"- Spec: {spec_path}")
|
|
413
|
+
print(f"- Output dir: {output_dir}")
|
|
414
|
+
confirm = input("Продолжить? [Y/n]: ").lower().strip() or "y"
|
|
415
|
+
if confirm not in ("y", "yes"):
|
|
416
|
+
print("Отменено пользователем.")
|
|
417
|
+
sys.exit(0)
|
|
418
|
+
|
|
419
|
+
generator = ApiGenerator()
|
|
420
|
+
try:
|
|
421
|
+
generator.generate_from_spec(
|
|
422
|
+
spec_path=str(spec_path), output_dir=str(output_dir)
|
|
423
|
+
)
|
|
424
|
+
print("Завершено.")
|
|
425
|
+
except (FileNotFoundError, ValueError) as e:
|
|
426
|
+
print(f"Ошибка: {e}", file=sys.stderr)
|
|
427
|
+
sys.exit(1)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
if __name__ == "__main__":
|
|
431
|
+
api_main()
|