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.
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 +222 -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 +159 -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 +1230 -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.21.44.dist-info/METADATA +233 -0
  83. persona_dsl-26.1.21.44.dist-info/RECORD +86 -0
  84. persona_dsl-26.1.21.44.dist-info/WHEEL +5 -0
  85. persona_dsl-26.1.21.44.dist-info/entry_points.txt +6 -0
  86. persona_dsl-26.1.21.44.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ from .send_request import SendRequest
2
+ from .json_response import JsonResponse
3
+ from .json_as import JsonAs
4
+
5
+ __all__ = ["SendRequest", "JsonResponse", "JsonAs"]
@@ -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,5 @@
1
+ from .execute_sql import ExecuteSQL
2
+ from .fetch_one import FetchOne
3
+ from .fetch_all import FetchAll
4
+
5
+ __all__ = ["ExecuteSQL", "FetchOne", "FetchAll"]
@@ -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,4 @@
1
+ from .send_message import SendMessage
2
+ from .message_in_topic import MessageInTopic
3
+
4
+ __all__ = ["SendMessage", "MessageInTopic"]
@@ -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,4 @@
1
+ from .call_operation import CallOperation
2
+ from .operation_result import OperationResult
3
+
4
+ __all__ = ["CallOperation", "OperationResult"]
@@ -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)