persona-dsl 26.1.21.44__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 +222 -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 +159 -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 +1230 -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.21.44.dist-info/METADATA +233 -0
- persona_dsl-26.1.21.44.dist-info/RECORD +86 -0
- persona_dsl-26.1.21.44.dist-info/WHEEL +5 -0
- persona_dsl-26.1.21.44.dist-info/entry_points.txt +6 -0
- persona_dsl-26.1.21.44.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from typing import Any, Optional, Dict
|
|
2
|
+
|
|
3
|
+
from persona_dsl.components.ops import Ops
|
|
4
|
+
from persona_dsl.skills.core.skill_definition import SkillId
|
|
5
|
+
from persona_dsl.skills.use_api import UseAPI
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class JsonAs(Ops):
|
|
9
|
+
"""
|
|
10
|
+
Низкоуровневая операция: делает HTTP-запрос и валидирует JSON-ответ как указанный тип.
|
|
11
|
+
Использует типизированные методы UseAPI (get_json_as/post_json_as/...).
|
|
12
|
+
Возвращает типизированный объект согласно model_type.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
method: str,
|
|
18
|
+
path: str,
|
|
19
|
+
model_type: Any,
|
|
20
|
+
json: Optional[Any] = None,
|
|
21
|
+
params: Optional[Dict[str, Any]] = None,
|
|
22
|
+
headers: Optional[Dict[str, str]] = None,
|
|
23
|
+
**kwargs: Any,
|
|
24
|
+
):
|
|
25
|
+
self.method = method
|
|
26
|
+
self.path = path
|
|
27
|
+
self.model_type = model_type
|
|
28
|
+
self.json = json
|
|
29
|
+
self.params = params
|
|
30
|
+
self.headers = headers
|
|
31
|
+
self.kwargs = kwargs
|
|
32
|
+
|
|
33
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
34
|
+
return f"{persona} запрашивает {self.method.upper()} '{self.path}' и парсит JSON как {getattr(self.model_type, '__name__', str(self.model_type))}"
|
|
35
|
+
|
|
36
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> Any:
|
|
37
|
+
api: UseAPI = persona.skill(SkillId.API)
|
|
38
|
+
m = self.method.upper()
|
|
39
|
+
if m == "GET":
|
|
40
|
+
return api.get_json_as(
|
|
41
|
+
self.path,
|
|
42
|
+
self.model_type,
|
|
43
|
+
params=self.params,
|
|
44
|
+
headers=self.headers,
|
|
45
|
+
json=self.json,
|
|
46
|
+
**self.kwargs,
|
|
47
|
+
)
|
|
48
|
+
if m == "POST":
|
|
49
|
+
return api.post_json_as(
|
|
50
|
+
self.path,
|
|
51
|
+
self.model_type,
|
|
52
|
+
params=self.params,
|
|
53
|
+
headers=self.headers,
|
|
54
|
+
json=self.json,
|
|
55
|
+
**self.kwargs,
|
|
56
|
+
)
|
|
57
|
+
if m == "PUT":
|
|
58
|
+
return api.put_json_as(
|
|
59
|
+
self.path,
|
|
60
|
+
self.model_type,
|
|
61
|
+
params=self.params,
|
|
62
|
+
headers=self.headers,
|
|
63
|
+
json=self.json,
|
|
64
|
+
**self.kwargs,
|
|
65
|
+
)
|
|
66
|
+
if m == "PATCH":
|
|
67
|
+
return api.patch_json_as(
|
|
68
|
+
self.path,
|
|
69
|
+
self.model_type,
|
|
70
|
+
params=self.params,
|
|
71
|
+
headers=self.headers,
|
|
72
|
+
json=self.json,
|
|
73
|
+
**self.kwargs,
|
|
74
|
+
)
|
|
75
|
+
if m == "DELETE":
|
|
76
|
+
return api.delete_json_as(
|
|
77
|
+
self.path,
|
|
78
|
+
self.model_type,
|
|
79
|
+
params=self.params,
|
|
80
|
+
headers=self.headers,
|
|
81
|
+
json=self.json,
|
|
82
|
+
**self.kwargs,
|
|
83
|
+
)
|
|
84
|
+
# Для прочих методов (HEAD/OPTIONS) пробуем соответствующие typed-методы, если они вернут None — так и вернём
|
|
85
|
+
if m == "HEAD":
|
|
86
|
+
return api.head_json_as(
|
|
87
|
+
self.path,
|
|
88
|
+
self.model_type,
|
|
89
|
+
params=self.params,
|
|
90
|
+
headers=self.headers,
|
|
91
|
+
json=self.json,
|
|
92
|
+
**self.kwargs,
|
|
93
|
+
)
|
|
94
|
+
if m == "OPTIONS":
|
|
95
|
+
return api.options_json_as(
|
|
96
|
+
self.path,
|
|
97
|
+
self.model_type,
|
|
98
|
+
params=self.params,
|
|
99
|
+
headers=self.headers,
|
|
100
|
+
json=self.json,
|
|
101
|
+
**self.kwargs,
|
|
102
|
+
)
|
|
103
|
+
# Явная ошибка для неподдерживаемых методов
|
|
104
|
+
raise ValueError(f"JsonAs: неподдерживаемый метод {self.method}")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Any, Optional, Dict
|
|
2
|
+
|
|
3
|
+
from persona_dsl.components.ops import Ops
|
|
4
|
+
from persona_dsl.skills.core.skill_definition import SkillId
|
|
5
|
+
from persona_dsl.skills.use_api import UseAPI
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class JsonResponse(Ops):
|
|
9
|
+
"""
|
|
10
|
+
Низкоуровневая операция: получает тело JSON-ответа после отправки запроса.
|
|
11
|
+
Этот факт предполагает, что запрос УЖЕ был отправлен (например, через SendRequest),
|
|
12
|
+
и мы работаем с последним ответом. В реальной реализации лучше объединить
|
|
13
|
+
отправку и получение ответа в один Fact.
|
|
14
|
+
|
|
15
|
+
Примечание: Для практического использования лучше создать Fact, который
|
|
16
|
+
сам делает запрос и возвращает результат.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
method: str,
|
|
22
|
+
path: str,
|
|
23
|
+
json: Optional[Any] = None,
|
|
24
|
+
params: Optional[Dict[str, Any]] = None,
|
|
25
|
+
headers: Optional[Dict[str, str]] = None,
|
|
26
|
+
**kwargs: Any,
|
|
27
|
+
):
|
|
28
|
+
self.method = method
|
|
29
|
+
self.path = path
|
|
30
|
+
self.json = json
|
|
31
|
+
self.params = params
|
|
32
|
+
self.headers = headers
|
|
33
|
+
self.kwargs = kwargs
|
|
34
|
+
|
|
35
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
36
|
+
return f"{persona} получает JSON-ответ от {self.method.upper()} '{self.path}'"
|
|
37
|
+
|
|
38
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> Any:
|
|
39
|
+
api: UseAPI = persona.skill(SkillId.API)
|
|
40
|
+
response = api._request(
|
|
41
|
+
method=self.method,
|
|
42
|
+
path=self.path,
|
|
43
|
+
json=self.json,
|
|
44
|
+
params=self.params,
|
|
45
|
+
headers=self.headers,
|
|
46
|
+
**self.kwargs,
|
|
47
|
+
)
|
|
48
|
+
return response.json()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Any, Optional, Dict
|
|
2
|
+
|
|
3
|
+
from persona_dsl.components.ops import Ops
|
|
4
|
+
from persona_dsl.skills.core.skill_definition import SkillId
|
|
5
|
+
from persona_dsl.skills.use_api import UseAPI
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SendRequest(Ops):
|
|
9
|
+
"""
|
|
10
|
+
Низкоуровневая операция: отправляет HTTP-запрос.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
method: str,
|
|
16
|
+
path: str,
|
|
17
|
+
json: Optional[Any] = None,
|
|
18
|
+
params: Optional[Dict[str, Any]] = None,
|
|
19
|
+
headers: Optional[Dict[str, str]] = None,
|
|
20
|
+
**kwargs: Any,
|
|
21
|
+
):
|
|
22
|
+
self.method = method
|
|
23
|
+
self.path = path
|
|
24
|
+
self.json = json
|
|
25
|
+
self.params = params
|
|
26
|
+
self.headers = headers
|
|
27
|
+
self.kwargs = kwargs
|
|
28
|
+
|
|
29
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
30
|
+
return f"{persona} отправляет {self.method.upper()} запрос на '{self.path}'"
|
|
31
|
+
|
|
32
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
|
|
33
|
+
api: UseAPI = persona.skill(SkillId.API)
|
|
34
|
+
api._request(
|
|
35
|
+
method=self.method,
|
|
36
|
+
path=self.path,
|
|
37
|
+
json=self.json,
|
|
38
|
+
params=self.params,
|
|
39
|
+
headers=self.headers,
|
|
40
|
+
**self.kwargs,
|
|
41
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from persona_dsl.components.ops import Ops
|
|
4
|
+
from persona_dsl.skills.core.skill_definition import SkillId
|
|
5
|
+
from persona_dsl.skills.use_database import UseDatabase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExecuteSQL(Ops):
|
|
9
|
+
"""
|
|
10
|
+
Низкоуровневая операция: выполняет SQL-запрос без возврата результата.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, query: str, params: Optional[Any] = None):
|
|
14
|
+
self.query = query
|
|
15
|
+
self.params = params
|
|
16
|
+
|
|
17
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
18
|
+
return f"{persona} выполняет SQL-запрос: {self.query[:100]}"
|
|
19
|
+
|
|
20
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
|
|
21
|
+
db: UseDatabase = persona.skill(SkillId.DB)
|
|
22
|
+
db.execute(self.query, self.params)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Any, Optional, List
|
|
2
|
+
|
|
3
|
+
from persona_dsl.components.ops import Ops
|
|
4
|
+
from persona_dsl.skills.core.skill_definition import SkillId
|
|
5
|
+
from persona_dsl.skills.use_database import UseDatabase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FetchAll(Ops):
|
|
9
|
+
"""
|
|
10
|
+
Низкоуровневая операция: выполняет SQL-запрос и возвращает все строки.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, query: str, params: Optional[Any] = None):
|
|
14
|
+
self.query = query
|
|
15
|
+
self.params = params
|
|
16
|
+
|
|
17
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
18
|
+
return f"{persona} получает все записи по SQL-запросу: {self.query[:100]}"
|
|
19
|
+
|
|
20
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> List[Any]:
|
|
21
|
+
db: UseDatabase = persona.skill(SkillId.DB)
|
|
22
|
+
conn = db.get_connection()
|
|
23
|
+
cursor = conn.cursor()
|
|
24
|
+
try:
|
|
25
|
+
cursor.execute(self.query, self.params or ())
|
|
26
|
+
result = cursor.fetchall()
|
|
27
|
+
return result
|
|
28
|
+
finally:
|
|
29
|
+
cursor.close()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from persona_dsl.components.ops import Ops
|
|
4
|
+
from persona_dsl.skills.core.skill_definition import SkillId
|
|
5
|
+
from persona_dsl.skills.use_database import UseDatabase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FetchOne(Ops):
|
|
9
|
+
"""
|
|
10
|
+
Низкоуровневая операция: выполняет SQL-запрос и возвращает первую строку.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, query: str, params: Optional[Any] = None):
|
|
14
|
+
self.query = query
|
|
15
|
+
self.params = params
|
|
16
|
+
|
|
17
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
18
|
+
return f"{persona} получает одну запись по SQL-запросу: {self.query[:100]}"
|
|
19
|
+
|
|
20
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> Any:
|
|
21
|
+
db: UseDatabase = persona.skill(SkillId.DB)
|
|
22
|
+
return db.execute(self.query, self.params)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any, List
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from persona_dsl.components.ops import Ops
|
|
5
|
+
from persona_dsl.skills.core.skill_definition import SkillId
|
|
6
|
+
from persona_dsl.skills.use_kafka import UseKafka
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MessageInTopic(Ops):
|
|
10
|
+
"""
|
|
11
|
+
Факт: получить сообщение из Kafka-топика.
|
|
12
|
+
Опционально фильтрует по ключу и простому подмножеству полей JSON-пейлоада.
|
|
13
|
+
Возвращает словарь:
|
|
14
|
+
{ topic, partition, offset, timestamp, key, value_raw, value_json, headers }.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
topic: str,
|
|
20
|
+
key: Optional[str] = None,
|
|
21
|
+
filter: Optional[Dict[str, Any]] = None,
|
|
22
|
+
max_messages: int = 10,
|
|
23
|
+
timeout: Optional[float] = None,
|
|
24
|
+
):
|
|
25
|
+
self.topic = topic
|
|
26
|
+
self.key = key
|
|
27
|
+
self.filter = filter
|
|
28
|
+
self.max_messages = max_messages
|
|
29
|
+
self.timeout = timeout
|
|
30
|
+
|
|
31
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
32
|
+
base = f"{persona} читает сообщения из Kafka топика '{self.topic}'"
|
|
33
|
+
if self.key:
|
|
34
|
+
base += f" с ключом '{self.key}'"
|
|
35
|
+
return base
|
|
36
|
+
|
|
37
|
+
def _perform(
|
|
38
|
+
self, persona: Any, *args: Any, **kwargs: Any
|
|
39
|
+
) -> Optional[Dict[str, Any]]:
|
|
40
|
+
kafka: UseKafka = persona.skill(SkillId.KAFKA)
|
|
41
|
+
msgs: List[Dict[str, Any]] = kafka.consume(
|
|
42
|
+
self.topic, max_messages=self.max_messages, timeout=self.timeout
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def _to_str(b: Any) -> Optional[str]:
|
|
46
|
+
if b is None:
|
|
47
|
+
return None
|
|
48
|
+
if isinstance(b, (bytes, bytearray)):
|
|
49
|
+
try:
|
|
50
|
+
return b.decode("utf-8")
|
|
51
|
+
except Exception:
|
|
52
|
+
return repr(bytes(b))
|
|
53
|
+
return str(b)
|
|
54
|
+
|
|
55
|
+
def _matches_filter(payload_obj: Any) -> bool:
|
|
56
|
+
if not self.filter:
|
|
57
|
+
return True
|
|
58
|
+
if not isinstance(payload_obj, dict):
|
|
59
|
+
return False
|
|
60
|
+
for k, v in self.filter.items():
|
|
61
|
+
if payload_obj.get(k) != v:
|
|
62
|
+
return False
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
for m in msgs:
|
|
66
|
+
key_str = _to_str(m.get("key"))
|
|
67
|
+
if self.key is not None and key_str != str(self.key):
|
|
68
|
+
continue
|
|
69
|
+
raw_val = m.get("value")
|
|
70
|
+
text_val = _to_str(raw_val)
|
|
71
|
+
parsed = None
|
|
72
|
+
if isinstance(text_val, str):
|
|
73
|
+
try:
|
|
74
|
+
parsed = json.loads(text_val)
|
|
75
|
+
except Exception:
|
|
76
|
+
parsed = None
|
|
77
|
+
if not _matches_filter(parsed if parsed is not None else text_val):
|
|
78
|
+
continue
|
|
79
|
+
return {
|
|
80
|
+
"topic": m.get("topic"),
|
|
81
|
+
"partition": m.get("partition"),
|
|
82
|
+
"offset": m.get("offset"),
|
|
83
|
+
"timestamp": m.get("timestamp"),
|
|
84
|
+
"key": key_str,
|
|
85
|
+
"value_raw": text_val,
|
|
86
|
+
"value_json": parsed,
|
|
87
|
+
"headers": m.get("headers") or {},
|
|
88
|
+
}
|
|
89
|
+
return None
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Any, Optional, List
|
|
2
|
+
|
|
3
|
+
from persona_dsl.components.ops import Ops
|
|
4
|
+
from persona_dsl.skills.core.skill_definition import SkillId
|
|
5
|
+
from persona_dsl.skills.use_kafka import UseKafka
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SendMessage(Ops):
|
|
9
|
+
"""
|
|
10
|
+
Низкоуровневая операция: отправляет сообщение в Kafka.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
topic: str,
|
|
16
|
+
value: bytes,
|
|
17
|
+
key: Optional[bytes] = None,
|
|
18
|
+
headers: Optional[List[tuple[str, bytes]]] = None,
|
|
19
|
+
):
|
|
20
|
+
self.topic = topic
|
|
21
|
+
self.value = value
|
|
22
|
+
self.key = key
|
|
23
|
+
self.headers = headers
|
|
24
|
+
|
|
25
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
26
|
+
return f"{persona} отправляет сообщение в Kafka топик '{self.topic}'"
|
|
27
|
+
|
|
28
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
|
|
29
|
+
kafka: UseKafka = persona.skill(SkillId.KAFKA)
|
|
30
|
+
kafka.send_message(
|
|
31
|
+
topic=self.topic,
|
|
32
|
+
message=self.value,
|
|
33
|
+
key=self.key,
|
|
34
|
+
headers=self.headers,
|
|
35
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from persona_dsl.components.ops import Ops
|
|
4
|
+
from persona_dsl.skills.core.skill_definition import SkillId
|
|
5
|
+
from persona_dsl.skills.use_soap import UseSOAP
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CallOperation(Ops):
|
|
9
|
+
"""
|
|
10
|
+
Низкоуровневая операция: вызывает SOAP-операцию без возврата результата.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, service: str, operation: str, *args: Any, **kwargs: Any):
|
|
14
|
+
self.service = service
|
|
15
|
+
self.operation = operation
|
|
16
|
+
self.args = args
|
|
17
|
+
self.kwargs = kwargs
|
|
18
|
+
|
|
19
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
20
|
+
return f"{persona} вызывает SOAP операцию '{self.operation}' сервиса '{self.service}'"
|
|
21
|
+
|
|
22
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
|
|
23
|
+
soap: UseSOAP = persona.skill(SkillId.SOAP)
|
|
24
|
+
soap.call(self.service, self.operation, *self.args, **self.kwargs)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from persona_dsl.components.ops import Ops
|
|
4
|
+
from persona_dsl.skills.core.skill_definition import SkillId
|
|
5
|
+
from persona_dsl.skills.use_soap import UseSOAP
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OperationResult(Ops):
|
|
9
|
+
"""
|
|
10
|
+
Низкоуровневая операция: вызывает SOAP-операцию и возвращает результат.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, service: str, operation: str, *args: Any, **kwargs: Any):
|
|
14
|
+
self.service = service
|
|
15
|
+
self.operation = operation
|
|
16
|
+
self.args = args
|
|
17
|
+
self.kwargs = kwargs
|
|
18
|
+
|
|
19
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
20
|
+
return f"{persona} получает результат SOAP операции '{self.operation}' сервиса '{self.service}'"
|
|
21
|
+
|
|
22
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> Any:
|
|
23
|
+
soap: UseSOAP = persona.skill(SkillId.SOAP)
|
|
24
|
+
return soap.call(self.service, self.operation, *self.args, **self.kwargs)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from .click import Click
|
|
2
|
+
from .fill import Fill
|
|
3
|
+
from .navigate import NavigateTo
|
|
4
|
+
from .press_key import PressKey
|
|
5
|
+
from .current_path import CurrentPath
|
|
6
|
+
from .screenshot import PageScreenshot, ElementScreenshot
|
|
7
|
+
from .aria_snapshot import PageAriaSnapshot, ElementAriaSnapshot
|
|
8
|
+
from .rich_aria_snapshot import RichAriaSnapshot
|
|
9
|
+
from .element_text import ElementText
|
|
10
|
+
from .element_is_visible import ElementIsVisible
|
|
11
|
+
from .generate_page_object import GeneratePageObject
|
|
12
|
+
from .wait_for_navigation import WaitForNavigation
|
|
13
|
+
from .element_attribute import ElementAttribute
|
|
14
|
+
from .input_value import InputValue
|
|
15
|
+
from .elements_count import ElementsCount
|
|
16
|
+
from .table_data import TableData
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Click",
|
|
20
|
+
"Fill",
|
|
21
|
+
"NavigateTo",
|
|
22
|
+
"PressKey",
|
|
23
|
+
"CurrentPath",
|
|
24
|
+
"PageScreenshot",
|
|
25
|
+
"ElementScreenshot",
|
|
26
|
+
"PageAriaSnapshot",
|
|
27
|
+
"ElementAriaSnapshot",
|
|
28
|
+
"RichAriaSnapshot",
|
|
29
|
+
"ElementText",
|
|
30
|
+
"ElementIsVisible",
|
|
31
|
+
"GeneratePageObject",
|
|
32
|
+
"WaitForNavigation",
|
|
33
|
+
"ElementAttribute",
|
|
34
|
+
"InputValue",
|
|
35
|
+
"ElementsCount",
|
|
36
|
+
"TableData",
|
|
37
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import allure
|
|
4
|
+
from persona_dsl.components.ops import Ops
|
|
5
|
+
from persona_dsl.skills.core.skill_definition import SkillId
|
|
6
|
+
from persona_dsl.pages.elements import Element
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PageAriaSnapshot(Ops):
|
|
10
|
+
"""
|
|
11
|
+
Факт: получить ARIA-снепшот всей страницы (page.locator("body").aria_snapshot()).
|
|
12
|
+
Возвращает YAML-строку с ролями/именами/состояниями.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, timeout: float | None = None):
|
|
16
|
+
"""
|
|
17
|
+
Args:
|
|
18
|
+
timeout: Необязательный таймаут Playwright-операции.
|
|
19
|
+
"""
|
|
20
|
+
self.timeout = timeout
|
|
21
|
+
|
|
22
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
23
|
+
return f"{persona} получает ARIA-снепшот страницы"
|
|
24
|
+
|
|
25
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> Any:
|
|
26
|
+
page = persona.skill(SkillId.BROWSER).page
|
|
27
|
+
if not hasattr(page.locator("body"), "aria_snapshot"):
|
|
28
|
+
raise RuntimeError(
|
|
29
|
+
"Эта версия Playwright не поддерживает Locator.aria_snapshot(). Обновите до 1.55+."
|
|
30
|
+
)
|
|
31
|
+
snapshot = page.locator("body").aria_snapshot(timeout=self.timeout)
|
|
32
|
+
try:
|
|
33
|
+
allure.attach(
|
|
34
|
+
snapshot,
|
|
35
|
+
name="aria-page",
|
|
36
|
+
attachment_type=allure.attachment_type.TEXT,
|
|
37
|
+
)
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
return snapshot
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ElementAriaSnapshot(Ops):
|
|
44
|
+
"""
|
|
45
|
+
Факт: получить ARIA-снепшот элемента.
|
|
46
|
+
Возвращает YAML-строку.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
element: Element,
|
|
52
|
+
timeout: float | None = None,
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Args:
|
|
56
|
+
element: Экземпляр элемента страницы.
|
|
57
|
+
timeout: Необязательный таймаут Playwright-операции.
|
|
58
|
+
"""
|
|
59
|
+
self.element = element
|
|
60
|
+
self.timeout = timeout
|
|
61
|
+
|
|
62
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
63
|
+
target = f"элемента '{self.element.name}'"
|
|
64
|
+
return f"{persona} получает ARIA-снепшот {target}"
|
|
65
|
+
|
|
66
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> Any:
|
|
67
|
+
page = persona.skill(SkillId.BROWSER).page
|
|
68
|
+
|
|
69
|
+
locator = self.element.resolve(page)
|
|
70
|
+
|
|
71
|
+
if not hasattr(locator, "aria_snapshot"):
|
|
72
|
+
raise RuntimeError(
|
|
73
|
+
"Эта версия Playwright не поддерживает Locator.aria_snapshot(). Обновите до 1.55+."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
snapshot = locator.aria_snapshot(timeout=self.timeout)
|
|
77
|
+
attach_name = f"aria-element:{self.element.name}"
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
allure.attach(
|
|
81
|
+
snapshot,
|
|
82
|
+
name=attach_name,
|
|
83
|
+
attachment_type=allure.attachment_type.TEXT,
|
|
84
|
+
)
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
return snapshot
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from persona_dsl.components.ops import Ops
|
|
4
|
+
from persona_dsl.skills.core.skill_definition import SkillId
|
|
5
|
+
from persona_dsl.pages.elements import Element
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Click(Ops):
|
|
9
|
+
"""
|
|
10
|
+
Атомарное действие: кликнуть по элементу.
|
|
11
|
+
Приоритет поиска: по ARIA-роли и имени, затем по `locator`.
|
|
12
|
+
Поддерживает проброс аргументов в Playwright (force, timeout, etc).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, element: Element, **kwargs: Any):
|
|
16
|
+
if not isinstance(element, Element):
|
|
17
|
+
raise TypeError(
|
|
18
|
+
f"Click ожидает экземпляр Element, получено: {type(element)}"
|
|
19
|
+
)
|
|
20
|
+
self.element = element
|
|
21
|
+
self.kwargs = kwargs
|
|
22
|
+
|
|
23
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
24
|
+
extra = f" с параметрами {self.kwargs}" if self.kwargs else ""
|
|
25
|
+
return f"{persona} кликает по элементу '{self.element.name}'{extra}"
|
|
26
|
+
|
|
27
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> None:
|
|
28
|
+
page = persona.skill(SkillId.BROWSER).page
|
|
29
|
+
locator = self.element.resolve(page)
|
|
30
|
+
locator.click(**self.kwargs)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from urllib.parse import urlparse
|
|
3
|
+
|
|
4
|
+
from persona_dsl.components.ops import Ops
|
|
5
|
+
from persona_dsl.skills.core.skill_definition import SkillId
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CurrentPath(Ops):
|
|
9
|
+
"""Бизнес-факт: текущий path активной страницы браузера."""
|
|
10
|
+
|
|
11
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
12
|
+
return f"{persona} запрашивает текущий путь страницы"
|
|
13
|
+
|
|
14
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> str:
|
|
15
|
+
"""Возвращает текущий путь URL."""
|
|
16
|
+
full_url = persona.skill(SkillId.BROWSER).page.url
|
|
17
|
+
return urlparse(full_url).path
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from persona_dsl.components.ops import Ops
|
|
4
|
+
from persona_dsl.skills.core.skill_definition import SkillId
|
|
5
|
+
from persona_dsl.pages.elements import Element
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ElementAttribute(Ops):
|
|
9
|
+
"""
|
|
10
|
+
Бизнес-операция: получить значение HTML-атрибута у элемента.
|
|
11
|
+
Пример: href для ссылок, src для изображений, data-* и т.д.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, element: Element, attribute_name: str):
|
|
15
|
+
self.element = element
|
|
16
|
+
self.attribute_name = attribute_name
|
|
17
|
+
|
|
18
|
+
def _get_step_description(self, persona: Any) -> str:
|
|
19
|
+
return f"{persona} запрашивает атрибут '{self.attribute_name}' у элемента '{self.element.name}'"
|
|
20
|
+
|
|
21
|
+
def _perform(self, persona: Any, *args: Any, **kwargs: Any) -> Optional[str]:
|
|
22
|
+
page = persona.skill(SkillId.BROWSER).page
|
|
23
|
+
locator = self.element.resolve(page)
|
|
24
|
+
return locator.get_attribute(self.attribute_name)
|