persona-dsl 26.1.20.8__tar.gz

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 (92) hide show
  1. persona_dsl-26.1.20.8/MANIFEST.in +1 -0
  2. persona_dsl-26.1.20.8/PKG-INFO +35 -0
  3. persona_dsl-26.1.20.8/README.md +185 -0
  4. persona_dsl-26.1.20.8/pyproject.toml +90 -0
  5. persona_dsl-26.1.20.8/setup.cfg +4 -0
  6. persona_dsl-26.1.20.8/src/persona_dsl/__init__.py +35 -0
  7. persona_dsl-26.1.20.8/src/persona_dsl/components/action.py +10 -0
  8. persona_dsl-26.1.20.8/src/persona_dsl/components/base_step.py +251 -0
  9. persona_dsl-26.1.20.8/src/persona_dsl/components/combined_step.py +68 -0
  10. persona_dsl-26.1.20.8/src/persona_dsl/components/expectation.py +10 -0
  11. persona_dsl-26.1.20.8/src/persona_dsl/components/fact.py +10 -0
  12. persona_dsl-26.1.20.8/src/persona_dsl/components/goal.py +10 -0
  13. persona_dsl-26.1.20.8/src/persona_dsl/components/ops.py +7 -0
  14. persona_dsl-26.1.20.8/src/persona_dsl/components/step.py +75 -0
  15. persona_dsl-26.1.20.8/src/persona_dsl/expectations/generic/__init__.py +15 -0
  16. persona_dsl-26.1.20.8/src/persona_dsl/expectations/generic/contains_item.py +19 -0
  17. persona_dsl-26.1.20.8/src/persona_dsl/expectations/generic/contains_the_text.py +15 -0
  18. persona_dsl-26.1.20.8/src/persona_dsl/expectations/generic/has_entries.py +21 -0
  19. persona_dsl-26.1.20.8/src/persona_dsl/expectations/generic/is_equal.py +24 -0
  20. persona_dsl-26.1.20.8/src/persona_dsl/expectations/generic/is_greater_than.py +18 -0
  21. persona_dsl-26.1.20.8/src/persona_dsl/expectations/generic/path_equal.py +27 -0
  22. persona_dsl-26.1.20.8/src/persona_dsl/expectations/web/__init__.py +5 -0
  23. persona_dsl-26.1.20.8/src/persona_dsl/expectations/web/is_displayed.py +13 -0
  24. persona_dsl-26.1.20.8/src/persona_dsl/expectations/web/matches_aria_snapshot.py +221 -0
  25. persona_dsl-26.1.20.8/src/persona_dsl/expectations/web/matches_screenshot.py +160 -0
  26. persona_dsl-26.1.20.8/src/persona_dsl/generators/__init__.py +5 -0
  27. persona_dsl-26.1.20.8/src/persona_dsl/generators/api_generator.py +423 -0
  28. persona_dsl-26.1.20.8/src/persona_dsl/generators/cli.py +431 -0
  29. persona_dsl-26.1.20.8/src/persona_dsl/generators/page_generator.py +1140 -0
  30. persona_dsl-26.1.20.8/src/persona_dsl/ops/api/__init__.py +5 -0
  31. persona_dsl-26.1.20.8/src/persona_dsl/ops/api/json_as.py +104 -0
  32. persona_dsl-26.1.20.8/src/persona_dsl/ops/api/json_response.py +48 -0
  33. persona_dsl-26.1.20.8/src/persona_dsl/ops/api/send_request.py +41 -0
  34. persona_dsl-26.1.20.8/src/persona_dsl/ops/db/__init__.py +5 -0
  35. persona_dsl-26.1.20.8/src/persona_dsl/ops/db/execute_sql.py +22 -0
  36. persona_dsl-26.1.20.8/src/persona_dsl/ops/db/fetch_all.py +29 -0
  37. persona_dsl-26.1.20.8/src/persona_dsl/ops/db/fetch_one.py +22 -0
  38. persona_dsl-26.1.20.8/src/persona_dsl/ops/kafka/__init__.py +4 -0
  39. persona_dsl-26.1.20.8/src/persona_dsl/ops/kafka/message_in_topic.py +89 -0
  40. persona_dsl-26.1.20.8/src/persona_dsl/ops/kafka/send_message.py +35 -0
  41. persona_dsl-26.1.20.8/src/persona_dsl/ops/soap/__init__.py +4 -0
  42. persona_dsl-26.1.20.8/src/persona_dsl/ops/soap/call_operation.py +24 -0
  43. persona_dsl-26.1.20.8/src/persona_dsl/ops/soap/operation_result.py +24 -0
  44. persona_dsl-26.1.20.8/src/persona_dsl/ops/web/__init__.py +37 -0
  45. persona_dsl-26.1.20.8/src/persona_dsl/ops/web/aria_snapshot.py +87 -0
  46. persona_dsl-26.1.20.8/src/persona_dsl/ops/web/click.py +30 -0
  47. persona_dsl-26.1.20.8/src/persona_dsl/ops/web/current_path.py +17 -0
  48. persona_dsl-26.1.20.8/src/persona_dsl/ops/web/element_attribute.py +24 -0
  49. persona_dsl-26.1.20.8/src/persona_dsl/ops/web/element_is_visible.py +27 -0
  50. persona_dsl-26.1.20.8/src/persona_dsl/ops/web/element_text.py +28 -0
  51. persona_dsl-26.1.20.8/src/persona_dsl/ops/web/elements_count.py +42 -0
  52. persona_dsl-26.1.20.8/src/persona_dsl/ops/web/fill.py +41 -0
  53. persona_dsl-26.1.20.8/src/persona_dsl/ops/web/generate_page_object.py +118 -0
  54. persona_dsl-26.1.20.8/src/persona_dsl/ops/web/input_value.py +23 -0
  55. persona_dsl-26.1.20.8/src/persona_dsl/ops/web/navigate.py +52 -0
  56. persona_dsl-26.1.20.8/src/persona_dsl/ops/web/press_key.py +37 -0
  57. persona_dsl-26.1.20.8/src/persona_dsl/ops/web/rich_aria_snapshot.py +146 -0
  58. persona_dsl-26.1.20.8/src/persona_dsl/ops/web/screenshot.py +68 -0
  59. persona_dsl-26.1.20.8/src/persona_dsl/ops/web/table_data.py +43 -0
  60. persona_dsl-26.1.20.8/src/persona_dsl/ops/web/wait_for_navigation.py +23 -0
  61. persona_dsl-26.1.20.8/src/persona_dsl/pages/__init__.py +133 -0
  62. persona_dsl-26.1.20.8/src/persona_dsl/pages/elements.py +998 -0
  63. persona_dsl-26.1.20.8/src/persona_dsl/pages/page.py +44 -0
  64. persona_dsl-26.1.20.8/src/persona_dsl/pages/virtual_page.py +94 -0
  65. persona_dsl-26.1.20.8/src/persona_dsl/persona.py +125 -0
  66. persona_dsl-26.1.20.8/src/persona_dsl/pytest_plugin.py +1064 -0
  67. persona_dsl-26.1.20.8/src/persona_dsl/runtime/dist/persona_bundle.js +1077 -0
  68. persona_dsl-26.1.20.8/src/persona_dsl/skills/__init__.py +7 -0
  69. persona_dsl-26.1.20.8/src/persona_dsl/skills/core/base.py +41 -0
  70. persona_dsl-26.1.20.8/src/persona_dsl/skills/core/skill_definition.py +30 -0
  71. persona_dsl-26.1.20.8/src/persona_dsl/skills/use_api.py +251 -0
  72. persona_dsl-26.1.20.8/src/persona_dsl/skills/use_browser.py +78 -0
  73. persona_dsl-26.1.20.8/src/persona_dsl/skills/use_database.py +129 -0
  74. persona_dsl-26.1.20.8/src/persona_dsl/skills/use_kafka.py +135 -0
  75. persona_dsl-26.1.20.8/src/persona_dsl/skills/use_soap.py +66 -0
  76. persona_dsl-26.1.20.8/src/persona_dsl/utils/__init__.py +0 -0
  77. persona_dsl-26.1.20.8/src/persona_dsl/utils/artifacts.py +22 -0
  78. persona_dsl-26.1.20.8/src/persona_dsl/utils/config.py +54 -0
  79. persona_dsl-26.1.20.8/src/persona_dsl/utils/data_providers.py +159 -0
  80. persona_dsl-26.1.20.8/src/persona_dsl/utils/decorators.py +80 -0
  81. persona_dsl-26.1.20.8/src/persona_dsl/utils/metrics.py +69 -0
  82. persona_dsl-26.1.20.8/src/persona_dsl/utils/naming.py +14 -0
  83. persona_dsl-26.1.20.8/src/persona_dsl/utils/path.py +202 -0
  84. persona_dsl-26.1.20.8/src/persona_dsl/utils/retry.py +51 -0
  85. persona_dsl-26.1.20.8/src/persona_dsl/utils/taas_integration.py +124 -0
  86. persona_dsl-26.1.20.8/src/persona_dsl/utils/waits.py +112 -0
  87. persona_dsl-26.1.20.8/src/persona_dsl.egg-info/PKG-INFO +35 -0
  88. persona_dsl-26.1.20.8/src/persona_dsl.egg-info/SOURCES.txt +90 -0
  89. persona_dsl-26.1.20.8/src/persona_dsl.egg-info/dependency_links.txt +1 -0
  90. persona_dsl-26.1.20.8/src/persona_dsl.egg-info/entry_points.txt +6 -0
  91. persona_dsl-26.1.20.8/src/persona_dsl.egg-info/requires.txt +31 -0
  92. persona_dsl-26.1.20.8/src/persona_dsl.egg-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ recursive-include src/persona_dsl/runtime/dist *.js
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: persona-dsl
3
+ Version: 26.1.20.8
4
+ Summary: DSL для реализации паттерна Screenplay в Python-тестах.
5
+ Requires-Python: >=3.9
6
+ Requires-Dist: playwright==1.55.0
7
+ Requires-Dist: pytest
8
+ Requires-Dist: pytest-cov
9
+ Requires-Dist: allure-pytest
10
+ Requires-Dist: python-dotenv
11
+ Requires-Dist: pyyaml
12
+ Requires-Dist: pydantic<3,>=2
13
+ Requires-Dist: requests
14
+ Requires-Dist: pyhamcrest
15
+ Requires-Dist: redis
16
+ Requires-Dist: Faker
17
+ Requires-Dist: pillow
18
+ Requires-Dist: zeep
19
+ Requires-Dist: pg8000
20
+ Requires-Dist: oracledb
21
+ Requires-Dist: kafka-python
22
+ Requires-Dist: Unidecode>=1.3
23
+ Requires-Dist: black
24
+ Provides-Extra: dev
25
+ Requires-Dist: black; extra == "dev"
26
+ Requires-Dist: ruff; extra == "dev"
27
+ Requires-Dist: mypy; extra == "dev"
28
+ Requires-Dist: pytest-cov; extra == "dev"
29
+ Requires-Dist: pre-commit; extra == "dev"
30
+ Requires-Dist: types-requests; extra == "dev"
31
+ Requires-Dist: types-PyYAML; extra == "dev"
32
+ Requires-Dist: types-redis; extra == "dev"
33
+ Requires-Dist: pytest-asyncio; extra == "dev"
34
+ Requires-Dist: pytest-timeout; extra == "dev"
35
+ Requires-Dist: pytest-xdist; extra == "dev"
@@ -0,0 +1,185 @@
1
+ # persona-dsl: Руководство пользователя
2
+
3
+ `persona-dsl` — это ядро фреймворка, реализующее легковесную версию паттерна **Screenplay** для написания автотестов на Python. Оно спроектировано так, чтобы дать вам полный контроль над базовыми инструментами (`Playwright`, `Allure`), предоставляя при этом выразительный и читаемый DSL для описания тестов.
4
+
5
+ ## 1. Основные концепции
6
+
7
+ В основе фреймворка лежат шесть ключевых компонентов, которые вместе описывают любое взаимодействие с системой.
8
+
9
+ - **Persona**: "Действующее лицо" или актор, который выполняет действия в тесте. Это главный объект, через который вы будете работать.
10
+ - **Skill**: "Навык" или способность Персоны взаимодействовать с системой. Например, `UseBrowser` для работы с веб-интерфейсом или `UseAPI` для запросов к API.
11
+ - **Ops**: Низкоуровневая, техническая операция (клик, HTTP-запрос). Единственный компонент, который может напрямую использовать `Skill`.
12
+ - **Action**: "Действие", атомарная бизнес-операция, которая может изменять состояние системы. Состоит из последовательности `Ops`.
13
+ - **Fact**: "Факт" о состоянии системы. Действие, которое *не изменяет* состояние, а только запрашивает данные. Также состоит из `Ops`.
14
+ - **Expectation**: "Ожидание", которое проверяет, соответствует ли `Fact` определенному условию (например, `IsEqualTo("some text")`).
15
+ - **Goal**: "Цель", высокоуровневый бизнес-сценарий, объединяющий несколько `Action` и `Fact`.
16
+
17
+ ## 2. Стиль написания тестов
18
+
19
+ Фреймворк предлагает единый, пошаговый стиль написания тестов, который легко читать и поддерживать.
20
+
21
+ - **Выполнение действий**: `persona.make(Action(...), Goal(...))`
22
+ - **Получение данных**: `data = persona.get(Fact(...))`
23
+ - **Проверки**: `persona.check(data, Expectation(...))`
24
+
25
+ ```python
26
+ # test_login.py
27
+ from persona_dsl.ops.web import NavigateTo, Fill, Click
28
+ from persona_dsl.facts.web import CurrentPath
29
+ from persona_dsl.expectations.generic import IsEqualTo
30
+ from tests.pages.login_page import LoginPage # Пример объекта страницы
31
+
32
+ def test_login_flow(persona):
33
+ login_page = LoginPage()
34
+
35
+ # Действия
36
+ persona.make(
37
+ NavigateTo("/login"),
38
+ Fill(login_page.username_field, "testuser"),
39
+ Fill(login_page.password_field, "password"),
40
+ Click(login_page.submit_button)
41
+ )
42
+
43
+ # Получение факта
44
+ current_path = persona.get(CurrentPath())
45
+
46
+ # Проверка
47
+ persona.check(current_path, IsEqualTo("/dashboard"))
48
+ ```
49
+
50
+ ## 3. Встроенная функциональность ("Батарейки в комплекте")
51
+
52
+ Ядро поставляется с `pytest-плагином`, который предоставляет полезные фикстуры и автоматизирует рутинные задачи.
53
+
54
+ ### 3.1. Фикстуры
55
+
56
+ - `persona`: Главная фикстура. Создает экземпляр `Persona` для теста и автоматически наделяет её нужными навыками.
57
+ - `config`: Загружает конфигурацию из файлов `config/*.yaml` и секреты из `.env.*` в зависимости от окружения (`--env=...`).
58
+ - `testdata`: Удобная фабрика для загрузки тестовых данных из `tests/data/*.{yaml,yml,json}`.
59
+
60
+ ### 3.2. Автоматическая интеграция с Allure
61
+
62
+ Вам не нужно вручную создавать шаги в отчете, фреймворк делает это за вас:
63
+ - Каждый вызов `persona.make()`, `persona.get()` и `persona.check()` **автоматически** оборачивается в `allure.step()`.
64
+ - Текст шага генерируется из метода `_get_step_description()`, который вы реализуете в своих компонентах.
65
+ - Результат, полученный `Фактом` (`Fact`), автоматически логируется во вложенном шаге.
66
+ - Навык `UseAPI` по явной настройке (`log_bodies=True`) прикрепляет к отчёту полные тела запросов и ответов.
67
+
68
+ ### 3.3. Автоматические артефакты при падении теста
69
+
70
+ При падении любого теста, который использовал браузер, фреймворк автоматически собирает артефакты для отладки и прикладывает их к Allure-отчету:
71
+ - **Скриншот** последней активной страницы.
72
+ - **HTML-код** страницы (`page source`).
73
+
74
+ Оба этих параметра включены по умолчанию. Их можно отключить в файле `config/{env}.yaml` в секции `reporting`:
75
+
76
+ ```yaml
77
+ # config/dev.yaml
78
+ reporting:
79
+ screenshot_on_fail: false
80
+ pagesource_on_fail: false
81
+ ```
82
+
83
+ ### 3.4. Декораторы для тестов
84
+
85
+ Ядро предоставляет удобные декораторы для работы с `pytest`:
86
+
87
+ - `@tag("smoke", "regression")`: Позволяет назначать тестам теги (`pytest-маркеры`).
88
+ - `@data_driven(...)`: Упрощает параметризацию тестов, позволяя загружать тестовые данные из списков или файлов в `tests/data/*.{yaml,yml,json}`.
89
+ - `@parametrize_simple(...)`: Обёртка над `pytest.mark.parametrize` для простых наборов данных.
90
+
91
+ #### Динамические данные в `@data_driven` (SystemVar)
92
+ Вы можете использовать генераторы данных (`SystemVar`) прямо в ваших YAML/JSON файлах для параметризации.
93
+
94
+ Для этого используется специальная JSON-структура:
95
+ ```json
96
+ {
97
+ "kind": "system",
98
+ "name": "имя_провайдера",
99
+ "args": { "аргумент": "значение" }
100
+ }
101
+ ```
102
+
103
+ **Доступные провайдеры (`name`):**
104
+ - `uuid()`: Детерминированный UUID v4.
105
+ - `randomInt(min, max)`: Случайное целое число.
106
+ - `randomFloat(min, max, precision)`: Случайное число с плавающей точкой.
107
+ - `randomString(length, alphabet?)`: Случайная строка.
108
+ - `now(fmt?, tz?)`: Текущая дата/время.
109
+ - `dateShift(days, fmt?, tz?)`: Дата со сдвигом.
110
+ - `fromEnv(name, default?)`: Значение из переменной окружения.
111
+ - `faker(provider, locale?, ...)`: Данные из библиотеки Faker.
112
+ - `sequence(name, seed?)`: Детерминированная числовая последовательность.
113
+ - `concat(parts: list)`: Склеивает строки.
114
+ - `template(pattern, variables)`: Форматирует строку по шаблону.
115
+
116
+ **Пример (`tests/data/dynamic_users.yaml`):**
117
+ ```yaml
118
+ - test_id: "user-1"
119
+ payload:
120
+ username:
121
+ kind: system
122
+ name: randomString
123
+ args: { length: 8 }
124
+ email:
125
+ kind: system
126
+ name: faker
127
+ args: { provider: "email" }
128
+ ```
129
+
130
+ Все значения генерируются детерминированно на основе `run_id`, `env` и `variant_id` (`test_id`), что обеспечивает воспроизводимость тестов.
131
+
132
+ ## 4. Конфигурация и Декларация (`conftest.py`)
133
+
134
+ Вся настройка фреймворка — декларативная и сосредоточена в `tests/conftest.py`.
135
+
136
+ ### 4.1. Декларация в `conftest.py`
137
+
138
+ 1. **`class Roles(BaseRole)`**: Enum-класс для объявления всех кастомных ролей (например, `ADMIN`).
139
+ 2. **`class Skills(BaseSkill)`**: Класс для объявления всех именованных навыков (например, `REPORTING_API`).
140
+ 3. **`PERSONA_SKILLS = [...]`**: Список всех навыков, используемых в проекте, для строгой валидации конфигурации.
141
+ 4. **`PERSONA_ROLES = Roles`**: Регистрация ролей для валидации.
142
+
143
+ ```python
144
+ # tests/conftest.py
145
+ from persona_dsl import BaseRole, BaseSkill, SkillId
146
+
147
+ class Roles(BaseRole):
148
+ ADMIN = "admin"
149
+
150
+ class Skills(BaseSkill):
151
+ REPORTING_API = (SkillId.API, "reporting")
152
+
153
+ PERSONA_ROLES = Roles
154
+ PERSONA_SKILLS = [
155
+ BaseSkill.BROWSER,
156
+ BaseSkill.API,
157
+ Skills.REPORTING_API,
158
+ ]
159
+ ```
160
+
161
+ ### 4.2. Структура конфигурации
162
+
163
+ - **`config/{env}.yaml`**: Параметры для окружения (URL, настройки headless и т.д.).
164
+ - **`config/auth.yaml`**: Секреты и учетные данные.
165
+
166
+ При запуске плагин автоматически проверяет, что для каждой роли и навыка из `conftest.py` есть соответствующая конфигурация в YAML.
167
+
168
+ ## 5. Готовые компоненты (Ops)
169
+
170
+ Ядро `persona-dsl` поставляется с набором готовых `Ops` для всех поддерживаемых `Skill`'ов.
171
+
172
+ - **`persona_dsl.ops.web`**: `NavigateTo`, `Click`, `Fill`, `PressKey`, `ElementText`, `ElementIsVisible`, `PageScreenshot`, `PageAriaSnapshot` и др.
173
+ - **`persona_dsl.ops.api`**: `JsonAs` (для отправки запросов и валидации ответа по Pydantic модели), `SendRequest`.
174
+ - **`persona_dsl.ops.db`**: `FetchOne`, `FetchAll`, `ExecuteSQL`.
175
+ - **`persona_dsl.ops.kafka`**: `SendMessage`, `MessageInTopic`.
176
+ - **`persona_dsl.ops.soap`**: `CallOperation`, `OperationResult`.
177
+
178
+ ## 6. Генераторы кода
179
+
180
+ Для ускорения разработки `persona-dsl` включает два генератора:
181
+
182
+ - **`persona-page-gen`**: Интерактивно создает классы `Page` из ARIA-снимка веб-страницы.
183
+ - **`persona-api-gen`**: Генерирует Pydantic-модели и классы `Action`/`Fact` из спецификации OpenAPI.
184
+
185
+ Это позволяет быстро создавать основу для PageObjects и API-клиентов, сокращая рутинную работу.
@@ -0,0 +1,90 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "persona-dsl"
7
+ version = "26.01.20.008"
8
+ description = "DSL для реализации паттерна Screenplay в Python-тестах."
9
+ requires-python = ">=3.9"
10
+ dependencies = [
11
+ "playwright==1.55.0",
12
+ "pytest",
13
+ "pytest-cov",
14
+ "allure-pytest",
15
+ "python-dotenv",
16
+ "pyyaml",
17
+ "pydantic>=2,<3",
18
+ "requests",
19
+ "pyhamcrest",
20
+ "redis",
21
+ "Faker",
22
+ "pillow",
23
+ "zeep",
24
+ "pg8000",
25
+ "oracledb",
26
+ "kafka-python",
27
+ "Unidecode>=1.3",
28
+ "black",
29
+ ]
30
+ [project.optional-dependencies]
31
+ dev = [
32
+ "black",
33
+ "ruff",
34
+ "mypy",
35
+ "pytest-cov",
36
+ "pre-commit",
37
+ "types-requests",
38
+ "types-PyYAML",
39
+ "types-redis",
40
+ "pytest-asyncio",
41
+ "pytest-timeout",
42
+ "pytest-xdist",
43
+ ]
44
+
45
+ [project.entry-points.pytest11]
46
+ persona_dsl = "persona_dsl.pytest_plugin"
47
+
48
+ [project.scripts]
49
+ persona-page-gen = "persona_dsl.generators.cli:page_main"
50
+ persona-api-gen = "persona_dsl.generators.cli:api_main"
51
+
52
+ [tool.setuptools.packages.find]
53
+ where = ["src"]
54
+
55
+ [tool.coverage.run]
56
+ source = ["src/persona_dsl"]
57
+ omit = [
58
+ # --- Компоненты, сложные для юнит-тестирования (снапшоты) ---
59
+ "src/persona_dsl/expectations/web/matches_screenshot.py",
60
+ "src/persona_dsl/expectations/web/matches_aria_snapshot.py",
61
+ # --- Файлы без исполняемой логики (базовые классы, Enum'ы, импорты) ---
62
+ "src/persona_dsl/__init__.py",
63
+ "src/persona_dsl/skills/__init__.py",
64
+ "src/persona_dsl/enums.py",
65
+ "src/persona_dsl/skill_id.py",
66
+ "src/persona_dsl/expectation.py",
67
+ ]
68
+ parallel = true
69
+
70
+ [tool.coverage.report]
71
+ show_missing = true
72
+
73
+ [tool.mypy]
74
+ python_version = "3.12"
75
+ warn_unused_ignores = true
76
+ warn_redundant_casts = true
77
+ disallow_untyped_defs = true
78
+ disallow_incomplete_defs = true
79
+ no_implicit_optional = true
80
+ strict_equality = true
81
+ ignore_missing_imports = true
82
+
83
+ [[tool.mypy.overrides]]
84
+ module = "persona_dsl.*"
85
+ check_untyped_defs = true
86
+
87
+ [[tool.mypy.overrides]]
88
+ module = "tests.*"
89
+ ignore_errors = true
90
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,35 @@
1
+ from .components.action import Action
2
+ from .utils.decorators import data_driven, tag, parametrize_simple
3
+ from .persona import BaseRole
4
+ from .skills.core.skill_definition import BaseSkill
5
+ from .components.expectation import Expectation
6
+ from .components.fact import Fact
7
+ from .components.goal import Goal
8
+ from .persona import Persona
9
+ from .skills.core.skill_definition import SkillId
10
+ from .utils.waits import wait_until
11
+ from .skills.core.base import Skill
12
+ from .skills import UseAPI, UseBrowser, UseDatabase, UseKafka, UseSOAP
13
+ from .components.step import Step
14
+
15
+ __all__ = [
16
+ "Action",
17
+ "Expectation",
18
+ "Goal",
19
+ "Persona",
20
+ "Fact",
21
+ "SkillId",
22
+ "Step",
23
+ "data_driven",
24
+ "tag",
25
+ "parametrize_simple",
26
+ "BaseRole",
27
+ "BaseSkill",
28
+ "Skill",
29
+ "wait_until",
30
+ "UseAPI",
31
+ "UseBrowser",
32
+ "UseDatabase",
33
+ "UseKafka",
34
+ "UseSOAP",
35
+ ]
@@ -0,0 +1,10 @@
1
+ from .step import Step
2
+
3
+
4
+ class Action(Step):
5
+ """
6
+ Алиас для Step, предназначенный для story-стиля.
7
+ Не содержит собственной логики.
8
+ """
9
+
10
+ pass
@@ -0,0 +1,251 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import time
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any, Optional, TypeVar
7
+
8
+ from allure import step
9
+
10
+ from ..utils.artifacts import artifacts_dir
11
+ from ..utils.metrics import metrics
12
+ from ..utils.path import extract_by_path
13
+ from ..utils.taas_integration import publish_taas_event
14
+ from ..utils.retry import RetryPolicy
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ T = TypeVar("T", bound="BaseStep")
19
+
20
+
21
+ class BaseStep(ABC):
22
+ """Единый базовый класс для всех исполняемых компонентов."""
23
+
24
+ # Политика ретраев по умолчанию для класса (может быть переопределена в наследниках)
25
+ retry_policy: Optional[RetryPolicy] = None
26
+
27
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
28
+ self._instance_retry_policy: Optional[RetryPolicy] = None
29
+
30
+ def __repr__(self) -> str:
31
+ params = {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
32
+ param_str = ", ".join(f"{k}={v!r}" for k, v in params.items())
33
+ return f"{self.__class__.__name__}({param_str})"
34
+
35
+ def with_retry(self: T, policy: RetryPolicy) -> T:
36
+ """
37
+ Возвращает копию шага с установленной политикой ретраев.
38
+ Полезно для ad-hoc настройки надежности в конкретном сценарии.
39
+ """
40
+ import copy
41
+
42
+ new_step = copy.copy(self)
43
+ new_step._instance_retry_policy = policy
44
+ return new_step
45
+
46
+ def _get_effective_retry_policy(self, persona: Any) -> RetryPolicy:
47
+ """Определяет актуальную политику ретраев (Instance > Class > Global Default)."""
48
+ # 1. Instance level (через .with_retry())
49
+ instance_policy = getattr(self, "_instance_retry_policy", None)
50
+ if instance_policy:
51
+ return instance_policy
52
+
53
+ # 2. Class level (атрибут класса)
54
+ if self.retry_policy:
55
+ return self.retry_policy
56
+
57
+ # 3. Global default from Persona config
58
+ # Ожидаем, что в persona.retries_config лежит dict, например:
59
+ # {'default': {...}, 'web_actions': {...}}
60
+ if hasattr(persona, "retries_config") and persona.retries_config:
61
+ # Можно усложнить логику и искать по типу шага (web_actions и т.д.)
62
+ # Пока берем 'default'
63
+ default_cfg = persona.retries_config.get("default")
64
+ if default_cfg:
65
+ return RetryPolicy(**default_cfg)
66
+
67
+ # 4. Hard fallback
68
+ return RetryPolicy(max_attempts=1)
69
+
70
+ def _get_component_type(self) -> str:
71
+ """
72
+ Определяет тип компонента на основе иерархии классов.
73
+ """
74
+ # Локальные импорты для разрыва циклической зависимости
75
+ from .goal import Goal
76
+ from .fact import Fact
77
+ from .action import Action
78
+ from .expectation import Expectation
79
+ from .combined_step import CombinedStep
80
+ from .step import Step
81
+ from .ops import Ops
82
+
83
+ if isinstance(self, Goal):
84
+ return "goal"
85
+ if isinstance(self, Fact):
86
+ return "fact"
87
+ if isinstance(self, Action):
88
+ return "action"
89
+ if isinstance(self, Expectation):
90
+ return "check"
91
+ if isinstance(self, CombinedStep):
92
+ return "combined_step"
93
+ if isinstance(self, Step):
94
+ return "step"
95
+ if isinstance(self, Ops):
96
+ if hasattr(self, "_component_type"):
97
+ comp_type = getattr(self, "_component_type")
98
+ if isinstance(comp_type, str):
99
+ return comp_type
100
+ return "ops"
101
+ return "unknown"
102
+
103
+ def execute(self, persona: Any, *args: Any, **kwargs: Any) -> Any:
104
+ """
105
+ Универсальная обертка для выполнения шага с поддержкой ретраев.
106
+ """
107
+ description = self._get_step_description(persona)
108
+ start_time = time.time()
109
+ component_type = self._get_component_type()
110
+ policy = self._get_effective_retry_policy(persona)
111
+
112
+ params = {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
113
+ publish_taas_event(
114
+ {
115
+ "event": "step_start",
116
+ "type": component_type,
117
+ "data": {
118
+ "description": description,
119
+ "timestamp": start_time,
120
+ "component_name": self.__class__.__name__,
121
+ "component_params": params,
122
+ "max_attempts": policy.max_attempts,
123
+ },
124
+ }
125
+ )
126
+
127
+ value: Any = None
128
+ status = "passed"
129
+ attempt = 0
130
+ attempt = 0
131
+
132
+ try:
133
+ # Цикл ретраев
134
+ while True:
135
+ attempt += 1
136
+ try:
137
+ with step(
138
+ description
139
+ if attempt == 1
140
+ else f"{description} (попытка {attempt})"
141
+ ):
142
+ if attempt > 1 and policy.on_retry_action:
143
+ step("Предварительное действие перед повтором")
144
+ policy.execute_action()
145
+
146
+ value = self._perform(persona, *args, **kwargs)
147
+ break # Успех
148
+ except Exception as e:
149
+ if policy.should_retry(attempt, e):
150
+ logger.warning(
151
+ f"Шаг '{description}' упал (попытка {attempt}/{policy.max_attempts}): {e}. Ретрай через {policy.delay}с."
152
+ )
153
+ policy.wait(attempt)
154
+ else:
155
+ raise e
156
+
157
+ # Привязка результата
158
+ save_as = getattr(self, "save_as", None)
159
+ extract = getattr(self, "extract", None)
160
+ if save_as:
161
+ bound_value = (
162
+ extract_by_path(value, extract)
163
+ if isinstance(extract, str)
164
+ else value
165
+ )
166
+ persona.memory[save_as] = bound_value
167
+
168
+ if value is not None:
169
+ with step(f"Получено значение: {str(value)[:100]}"):
170
+ pass
171
+
172
+ return value
173
+
174
+ except Exception:
175
+ status = "failed"
176
+ raise
177
+ finally:
178
+ end_time = time.time()
179
+ duration = end_time - start_time
180
+ metrics.gauge(
181
+ "step.duration",
182
+ duration,
183
+ tags={
184
+ "type": component_type,
185
+ "name": self.__class__.__name__,
186
+ "status": status,
187
+ },
188
+ )
189
+
190
+ event_data_end: dict[str, Any] = {
191
+ "description": description,
192
+ "timestamp": end_time,
193
+ "duration": duration,
194
+ "status": status,
195
+ "attempts_used": attempt,
196
+ }
197
+
198
+ result_raw: Any = None
199
+ result_raw_path: str | None = None
200
+ result_raw_url: str | None = None
201
+ if isinstance(value, (dict, list)):
202
+ result_raw = value
203
+ raw_dir = artifacts_dir("result_raw")
204
+ filename = f"{int(end_time * 1000)}-{self.__class__.__name__}.json"
205
+ file_path = raw_dir / filename
206
+ try:
207
+ with open(file_path, "w", encoding="utf-8") as f:
208
+ json.dump(result_raw, f, ensure_ascii=False, indent=2)
209
+ result_raw_path = str(file_path)
210
+ run_id = os.environ.get("TAAS_RUN_ID")
211
+ result_raw_url = (
212
+ f"/artifacts/{run_id}/result_raw/{filename}" if run_id else None
213
+ )
214
+ except (IOError, TypeError) as e:
215
+ logger.warning(
216
+ f"Не удалось сохранить result_raw артефакт в {file_path}: {e}"
217
+ )
218
+ result_raw = None
219
+
220
+ event_data_end.update(
221
+ {
222
+ "result": str(value)[:100] if value is not None else "None",
223
+ "result_raw": result_raw,
224
+ "bindings": {
225
+ "save_as": getattr(self, "save_as", None),
226
+ "extract": getattr(self, "extract", None),
227
+ },
228
+ "result_raw_path": result_raw_path,
229
+ "result_raw_url": result_raw_url,
230
+ }
231
+ )
232
+
233
+ publish_taas_event(
234
+ {
235
+ "event": "step_end",
236
+ "type": component_type,
237
+ "data": event_data_end,
238
+ }
239
+ )
240
+
241
+ @abstractmethod
242
+ def _get_step_description(self, persona: Any) -> str: # pragma: no cover
243
+ """Возвращает текстовое описание шага для Allure."""
244
+ ...
245
+
246
+ @abstractmethod
247
+ def _perform(
248
+ self, persona: Any, *args: Any, **kwargs: Any
249
+ ) -> Any: # pragma: no cover
250
+ """Содержит реальную логику выполнения шага."""
251
+ ...