lucius-mcp 0.2.2__py3-none-any.whl → 0.3.0__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.
- {lucius_mcp-0.2.2.dist-info → lucius_mcp-0.3.0.dist-info}/METADATA +8 -1
- {lucius_mcp-0.2.2.dist-info → lucius_mcp-0.3.0.dist-info}/RECORD +38 -19
- src/client/__init__.py +10 -0
- src/client/client.py +289 -8
- src/client/generated/README.md +11 -0
- src/client/generated/__init__.py +4 -0
- src/client/generated/api/__init__.py +2 -0
- src/client/generated/api/test_layer_controller_api.py +1746 -0
- src/client/generated/api/test_layer_schema_controller_api.py +1415 -0
- src/client/generated/docs/TestLayerControllerApi.md +407 -0
- src/client/generated/docs/TestLayerSchemaControllerApi.md +350 -0
- src/client/overridden/test_case_custom_fields_v2.py +254 -0
- src/services/__init__.py +8 -0
- src/services/launch_service.py +278 -0
- src/services/search_service.py +1 -1
- src/services/test_case_service.py +512 -92
- src/services/test_layer_service.py +416 -0
- src/tools/__init__.py +35 -0
- src/tools/create_test_case.py +38 -19
- src/tools/create_test_layer.py +33 -0
- src/tools/create_test_layer_schema.py +39 -0
- src/tools/delete_test_layer.py +31 -0
- src/tools/delete_test_layer_schema.py +31 -0
- src/tools/get_custom_fields.py +2 -1
- src/tools/get_test_case_custom_fields.py +34 -0
- src/tools/launches.py +112 -0
- src/tools/list_test_layer_schemas.py +43 -0
- src/tools/list_test_layers.py +38 -0
- src/tools/search.py +6 -3
- src/tools/test_layers.py +21 -0
- src/tools/update_test_case.py +48 -23
- src/tools/update_test_layer.py +33 -0
- src/tools/update_test_layer_schema.py +40 -0
- src/utils/__init__.py +4 -0
- src/utils/links.py +13 -0
- {lucius_mcp-0.2.2.dist-info → lucius_mcp-0.3.0.dist-info}/WHEEL +0 -0
- {lucius_mcp-0.2.2.dist-info → lucius_mcp-0.3.0.dist-info}/entry_points.txt +0 -0
- {lucius_mcp-0.2.2.dist-info → lucius_mcp-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""Service for managing Test Layers in Allure TestOps."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
6
|
+
|
|
7
|
+
from src.client import AllureClient
|
|
8
|
+
from src.client.exceptions import AllureAPIError, AllureNotFoundError, AllureValidationError
|
|
9
|
+
from src.client.generated.exceptions import ApiException
|
|
10
|
+
from src.client.generated.models.page_test_layer_dto import PageTestLayerDto
|
|
11
|
+
from src.client.generated.models.page_test_layer_schema_dto import PageTestLayerSchemaDto
|
|
12
|
+
from src.client.generated.models.test_layer_create_dto import TestLayerCreateDto
|
|
13
|
+
from src.client.generated.models.test_layer_dto import TestLayerDto
|
|
14
|
+
from src.client.generated.models.test_layer_patch_dto import TestLayerPatchDto
|
|
15
|
+
from src.client.generated.models.test_layer_schema_create_dto import TestLayerSchemaCreateDto
|
|
16
|
+
from src.client.generated.models.test_layer_schema_dto import TestLayerSchemaDto
|
|
17
|
+
from src.client.generated.models.test_layer_schema_patch_dto import TestLayerSchemaPatchDto
|
|
18
|
+
from src.utils.schema_hint import generate_schema_hint
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Constants - from Allure TestOps API constraints
|
|
23
|
+
# These limits match the database schema VARCHAR field lengths for test layer names and schema keys
|
|
24
|
+
MAX_NAME_LENGTH = 255
|
|
25
|
+
MAX_KEY_LENGTH = 255
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestLayerService:
|
|
29
|
+
"""Service for managing Test Layers and their Schemas in Allure TestOps."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, client: AllureClient) -> None:
|
|
32
|
+
"""Initialize TestLayerService.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
client: AllureClient instance
|
|
36
|
+
"""
|
|
37
|
+
self._client = client
|
|
38
|
+
self._project_id = client.get_project()
|
|
39
|
+
|
|
40
|
+
# ==========================================
|
|
41
|
+
# Test Layer CRUD Operations
|
|
42
|
+
# ==========================================
|
|
43
|
+
|
|
44
|
+
async def list_test_layers(
|
|
45
|
+
self,
|
|
46
|
+
page: int = 0,
|
|
47
|
+
size: int = 100,
|
|
48
|
+
) -> list[TestLayerDto]:
|
|
49
|
+
"""List all test layers.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
page: Page number (0-based)
|
|
53
|
+
size: Page size
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of TestLayerDto
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
AllureAPIError: If the API request fails
|
|
60
|
+
"""
|
|
61
|
+
if not self._client._test_layer_api:
|
|
62
|
+
raise AllureAPIError("Test Layer API is not initialized")
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
page_dto: PageTestLayerDto = await self._client._test_layer_api.find_all7(
|
|
66
|
+
page=page,
|
|
67
|
+
size=size,
|
|
68
|
+
)
|
|
69
|
+
return page_dto.content or []
|
|
70
|
+
except Exception as e:
|
|
71
|
+
raise AllureAPIError(f"Failed to list test layers: {e}") from e
|
|
72
|
+
|
|
73
|
+
async def create_test_layer(self, name: str) -> TestLayerDto:
|
|
74
|
+
"""Create a new test layer.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
name: Name of the test layer
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The created TestLayerDto
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
AllureValidationError: If validation fails
|
|
84
|
+
AllureAPIError: If the API request fails
|
|
85
|
+
"""
|
|
86
|
+
self._validate_name(name)
|
|
87
|
+
|
|
88
|
+
if not self._client._test_layer_api:
|
|
89
|
+
raise AllureAPIError("Test Layer API is not initialized")
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
create_dto = TestLayerCreateDto(name=name)
|
|
93
|
+
except PydanticValidationError as e:
|
|
94
|
+
hint = generate_schema_hint(TestLayerCreateDto)
|
|
95
|
+
raise AllureValidationError(f"Invalid test layer data: {e}", suggestions=[hint]) from e
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
return await self._client._test_layer_api.create9(test_layer_create_dto=create_dto)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
raise AllureAPIError(
|
|
101
|
+
f"Failed to create test layer '{name}': {e}. "
|
|
102
|
+
"Ensure the name is unique and you have project permissions."
|
|
103
|
+
) from e
|
|
104
|
+
|
|
105
|
+
async def get_test_layer(self, layer_id: int) -> TestLayerDto:
|
|
106
|
+
"""Get a test layer by ID.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
layer_id: The test layer ID
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
The TestLayerDto
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
AllureNotFoundError: If test layer doesn't exist
|
|
116
|
+
AllureAPIError: If the API request fails
|
|
117
|
+
"""
|
|
118
|
+
if not self._client._test_layer_api:
|
|
119
|
+
raise AllureAPIError("Test Layer API is not initialized")
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
return await self._client._test_layer_api.find_one8(id=layer_id)
|
|
123
|
+
except ApiException as e:
|
|
124
|
+
self._client._handle_api_exception(e)
|
|
125
|
+
raise
|
|
126
|
+
except Exception as e:
|
|
127
|
+
raise AllureAPIError(f"Failed to get test layer {layer_id}: {e}") from e
|
|
128
|
+
|
|
129
|
+
async def update_test_layer(
|
|
130
|
+
self,
|
|
131
|
+
layer_id: int,
|
|
132
|
+
name: str,
|
|
133
|
+
) -> tuple[TestLayerDto, bool]:
|
|
134
|
+
"""Update an existing test layer with idempotency support.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
layer_id: The test layer ID to update
|
|
138
|
+
name: New name for the test layer
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Tuple of (updated_test_layer, changed) where changed is True if update was applied
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
AllureNotFoundError: If test layer doesn't exist
|
|
145
|
+
AllureValidationError: If validation fails
|
|
146
|
+
AllureAPIError: If the API request fails
|
|
147
|
+
"""
|
|
148
|
+
self._validate_name(name)
|
|
149
|
+
|
|
150
|
+
# Get current state
|
|
151
|
+
current = await self.get_test_layer(layer_id)
|
|
152
|
+
|
|
153
|
+
# Check idempotency
|
|
154
|
+
if current.name == name:
|
|
155
|
+
return current, False
|
|
156
|
+
|
|
157
|
+
if not self._client._test_layer_api:
|
|
158
|
+
raise AllureAPIError("Test Layer API is not initialized")
|
|
159
|
+
|
|
160
|
+
# Build patch DTO and update
|
|
161
|
+
try:
|
|
162
|
+
patch_data = TestLayerPatchDto(name=name)
|
|
163
|
+
except PydanticValidationError as e:
|
|
164
|
+
hint = generate_schema_hint(TestLayerPatchDto)
|
|
165
|
+
raise AllureValidationError(f"Invalid patch data: {e}", suggestions=[hint]) from e
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
updated = await self._client._test_layer_api.patch9(id=layer_id, test_layer_patch_dto=patch_data)
|
|
169
|
+
return updated, True
|
|
170
|
+
except Exception as e:
|
|
171
|
+
raise AllureAPIError(
|
|
172
|
+
f"Failed to update test layer {layer_id}: {e}. "
|
|
173
|
+
"Check that the layer exists and you have update permissions."
|
|
174
|
+
) from e
|
|
175
|
+
|
|
176
|
+
async def delete_test_layer(self, layer_id: int) -> bool:
|
|
177
|
+
"""Delete a test layer with idempotent behavior.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
layer_id: The test layer ID to delete
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
True if the layer was deleted, False if it was already deleted/not found
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
AllureAPIError: If the API request fails (other than 404)
|
|
187
|
+
|
|
188
|
+
Note:
|
|
189
|
+
This operation is idempotent. If already deleted (404), returns False.
|
|
190
|
+
"""
|
|
191
|
+
if not self._client._test_layer_api:
|
|
192
|
+
raise AllureAPIError("Test Layer API is not initialized")
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
await self._client._test_layer_api.delete9(id=layer_id)
|
|
196
|
+
return True
|
|
197
|
+
except AllureNotFoundError:
|
|
198
|
+
# Idempotent: if already deleted, this is fine
|
|
199
|
+
logger.debug(f"Test layer {layer_id} already deleted or not found")
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
# ==========================================
|
|
203
|
+
# Test Layer Schema CRUD Operations
|
|
204
|
+
# ==========================================
|
|
205
|
+
|
|
206
|
+
async def list_test_layer_schemas(
|
|
207
|
+
self,
|
|
208
|
+
project_id: int | None = None,
|
|
209
|
+
page: int = 0,
|
|
210
|
+
size: int = 100,
|
|
211
|
+
) -> list[TestLayerSchemaDto]:
|
|
212
|
+
"""List all test layer schemas for a project.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
project_id: Project ID (defaults to client's project)
|
|
216
|
+
page: Page number (0-based)
|
|
217
|
+
size: Page size
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
List of TestLayerSchemaDto
|
|
221
|
+
|
|
222
|
+
Raises:
|
|
223
|
+
AllureAPIError: If the API request fails
|
|
224
|
+
"""
|
|
225
|
+
target_project_id = project_id or self._project_id
|
|
226
|
+
self._validate_project_id(target_project_id)
|
|
227
|
+
|
|
228
|
+
if not self._client._test_layer_schema_api:
|
|
229
|
+
raise AllureAPIError("Test Layer Schema API is not initialized")
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
page_dto: PageTestLayerSchemaDto = await self._client._test_layer_schema_api.find_all6(
|
|
233
|
+
project_id=target_project_id,
|
|
234
|
+
page=page,
|
|
235
|
+
size=size,
|
|
236
|
+
)
|
|
237
|
+
return page_dto.content or []
|
|
238
|
+
except Exception as e:
|
|
239
|
+
raise AllureAPIError(f"Failed to list test layer schemas: {e}") from e
|
|
240
|
+
|
|
241
|
+
async def create_test_layer_schema(
|
|
242
|
+
self,
|
|
243
|
+
project_id: int,
|
|
244
|
+
test_layer_id: int,
|
|
245
|
+
key: str,
|
|
246
|
+
) -> TestLayerSchemaDto:
|
|
247
|
+
"""Create a new test layer schema.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
project_id: The project ID
|
|
251
|
+
test_layer_id: The test layer ID to link to
|
|
252
|
+
key: The schema key
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
The created TestLayerSchemaDto
|
|
256
|
+
|
|
257
|
+
Raises:
|
|
258
|
+
AllureValidationError: If validation fails
|
|
259
|
+
AllureAPIError: If the API request fails
|
|
260
|
+
"""
|
|
261
|
+
self._validate_project_id(project_id)
|
|
262
|
+
self._validate_key(key)
|
|
263
|
+
|
|
264
|
+
if not self._client._test_layer_schema_api:
|
|
265
|
+
raise AllureAPIError("Test Layer Schema API is not initialized")
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
create_dto = TestLayerSchemaCreateDto(
|
|
269
|
+
project_id=project_id,
|
|
270
|
+
test_layer_id=test_layer_id,
|
|
271
|
+
key=key,
|
|
272
|
+
)
|
|
273
|
+
except PydanticValidationError as e:
|
|
274
|
+
hint = generate_schema_hint(TestLayerSchemaCreateDto)
|
|
275
|
+
raise AllureValidationError(f"Invalid test layer schema data: {e}", suggestions=[hint]) from e
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
return await self._client._test_layer_schema_api.create8(test_layer_schema_create_dto=create_dto)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
raise AllureAPIError(
|
|
281
|
+
f"Failed to create test layer schema '{key}': {e}. "
|
|
282
|
+
"Ensure the project_id and test_layer_id are valid, and the key is unique within the project."
|
|
283
|
+
) from e
|
|
284
|
+
|
|
285
|
+
async def get_test_layer_schema(self, schema_id: int) -> TestLayerSchemaDto:
|
|
286
|
+
"""Get a test layer schema by ID.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
schema_id: The test layer schema ID
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
The TestLayerSchemaDto
|
|
293
|
+
|
|
294
|
+
Raises:
|
|
295
|
+
AllureNotFoundError: If test layer schema doesn't exist
|
|
296
|
+
AllureAPIError: If the API request fails
|
|
297
|
+
"""
|
|
298
|
+
if not self._client._test_layer_schema_api:
|
|
299
|
+
raise AllureAPIError("Test Layer Schema API is not initialized")
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
return await self._client._test_layer_schema_api.find_one7(id=schema_id)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
raise AllureAPIError(f"Failed to get test layer schema {schema_id}: {e}") from e
|
|
305
|
+
|
|
306
|
+
async def update_test_layer_schema(
|
|
307
|
+
self,
|
|
308
|
+
schema_id: int,
|
|
309
|
+
test_layer_id: int | None = None,
|
|
310
|
+
key: str | None = None,
|
|
311
|
+
) -> tuple[TestLayerSchemaDto, bool]:
|
|
312
|
+
"""Update an existing test layer schema with idempotency support.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
schema_id: The test layer schema ID to update
|
|
316
|
+
test_layer_id: New test layer ID (optional)
|
|
317
|
+
key: New key (optional)
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
Tuple of (updated_schema, changed) where changed is True if update was applied
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
AllureNotFoundError: If test layer schema doesn't exist
|
|
324
|
+
AllureValidationError: If validation fails
|
|
325
|
+
AllureAPIError: If the API request fails
|
|
326
|
+
"""
|
|
327
|
+
# Validation
|
|
328
|
+
if key is not None:
|
|
329
|
+
self._validate_key(key)
|
|
330
|
+
|
|
331
|
+
# Get current state
|
|
332
|
+
current = await self.get_test_layer_schema(schema_id)
|
|
333
|
+
|
|
334
|
+
# Check idempotency
|
|
335
|
+
needs_update = False
|
|
336
|
+
if test_layer_id is not None and current.test_layer and current.test_layer.id != test_layer_id:
|
|
337
|
+
needs_update = True
|
|
338
|
+
if key is not None and current.key != key:
|
|
339
|
+
needs_update = True
|
|
340
|
+
|
|
341
|
+
# No changes needed
|
|
342
|
+
if not needs_update:
|
|
343
|
+
return current, False
|
|
344
|
+
|
|
345
|
+
if not self._client._test_layer_schema_api:
|
|
346
|
+
raise AllureAPIError("Test Layer Schema API is not initialized")
|
|
347
|
+
|
|
348
|
+
# Build patch DTO and update
|
|
349
|
+
try:
|
|
350
|
+
patch_data = TestLayerSchemaPatchDto(
|
|
351
|
+
test_layer_id=test_layer_id,
|
|
352
|
+
key=key,
|
|
353
|
+
)
|
|
354
|
+
except PydanticValidationError as e:
|
|
355
|
+
hint = generate_schema_hint(TestLayerSchemaPatchDto)
|
|
356
|
+
raise AllureValidationError(f"Invalid patch data: {e}", suggestions=[hint]) from e
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
updated = await self._client._test_layer_schema_api.patch8(
|
|
360
|
+
id=schema_id, test_layer_schema_patch_dto=patch_data
|
|
361
|
+
)
|
|
362
|
+
return updated, True
|
|
363
|
+
except Exception as e:
|
|
364
|
+
raise AllureAPIError(
|
|
365
|
+
f"Failed to update test layer schema {schema_id}: {e}. "
|
|
366
|
+
"Check that the schema exists and the new values are valid."
|
|
367
|
+
) from e
|
|
368
|
+
|
|
369
|
+
async def delete_test_layer_schema(self, schema_id: int) -> bool:
|
|
370
|
+
"""Delete a test layer schema with idempotent behavior.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
schema_id: The test layer schema ID to delete
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
True if the schema was deleted, False if it was already deleted/not found
|
|
377
|
+
|
|
378
|
+
Raises:
|
|
379
|
+
AllureAPIError: If the API request fails (other than 404)
|
|
380
|
+
|
|
381
|
+
Note:
|
|
382
|
+
This operation is idempotent. If already deleted (404), returns False.
|
|
383
|
+
"""
|
|
384
|
+
if not self._client._test_layer_schema_api:
|
|
385
|
+
raise AllureAPIError("Test Layer Schema API is not initialized")
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
await self._client._test_layer_schema_api.delete8(id=schema_id)
|
|
389
|
+
return True
|
|
390
|
+
except AllureNotFoundError:
|
|
391
|
+
# Idempotent: if already deleted, this is fine
|
|
392
|
+
logger.debug(f"Test layer schema {schema_id} already deleted or not found")
|
|
393
|
+
return False
|
|
394
|
+
|
|
395
|
+
# ==========================================
|
|
396
|
+
# Validation Methods
|
|
397
|
+
# ==========================================
|
|
398
|
+
|
|
399
|
+
def _validate_project_id(self, project_id: int) -> None:
|
|
400
|
+
"""Validate project ID."""
|
|
401
|
+
if not isinstance(project_id, int) or project_id <= 0:
|
|
402
|
+
raise AllureValidationError("Project ID must be a positive integer")
|
|
403
|
+
|
|
404
|
+
def _validate_name(self, name: str) -> None:
|
|
405
|
+
"""Validate test layer name."""
|
|
406
|
+
if not name or not name.strip():
|
|
407
|
+
raise AllureValidationError("Name is required")
|
|
408
|
+
if len(name) > MAX_NAME_LENGTH:
|
|
409
|
+
raise AllureValidationError(f"Name too long (max {MAX_NAME_LENGTH})")
|
|
410
|
+
|
|
411
|
+
def _validate_key(self, key: str) -> None:
|
|
412
|
+
"""Validate test layer schema key."""
|
|
413
|
+
if not key or not key.strip():
|
|
414
|
+
raise AllureValidationError("Key is required")
|
|
415
|
+
if len(key) > MAX_KEY_LENGTH:
|
|
416
|
+
raise AllureValidationError(f"Key too long (max {MAX_KEY_LENGTH})")
|
src/tools/__init__.py
CHANGED
|
@@ -3,26 +3,49 @@ from collections.abc import Awaitable, Callable
|
|
|
3
3
|
from src.tools.create_test_case import create_test_case
|
|
4
4
|
from src.tools.delete_test_case import delete_test_case
|
|
5
5
|
from src.tools.get_custom_fields import get_custom_fields
|
|
6
|
+
from src.tools.get_test_case_custom_fields import get_test_case_custom_fields
|
|
7
|
+
from src.tools.launches import create_launch, list_launches
|
|
6
8
|
from src.tools.link_shared_step import link_shared_step
|
|
7
9
|
from src.tools.search import get_test_case_details, list_test_cases, search_test_cases
|
|
8
10
|
from src.tools.shared_steps import create_shared_step, delete_shared_step, list_shared_steps, update_shared_step
|
|
11
|
+
from src.tools.test_layers import (
|
|
12
|
+
create_test_layer,
|
|
13
|
+
create_test_layer_schema,
|
|
14
|
+
delete_test_layer,
|
|
15
|
+
delete_test_layer_schema,
|
|
16
|
+
list_test_layer_schemas,
|
|
17
|
+
list_test_layers,
|
|
18
|
+
update_test_layer,
|
|
19
|
+
update_test_layer_schema,
|
|
20
|
+
)
|
|
9
21
|
from src.tools.unlink_shared_step import unlink_shared_step
|
|
10
22
|
from src.tools.update_test_case import update_test_case
|
|
11
23
|
|
|
12
24
|
__all__ = [
|
|
25
|
+
"create_launch",
|
|
13
26
|
"create_shared_step",
|
|
14
27
|
"create_test_case",
|
|
28
|
+
"create_test_layer",
|
|
29
|
+
"create_test_layer_schema",
|
|
15
30
|
"delete_shared_step",
|
|
16
31
|
"delete_test_case",
|
|
32
|
+
"delete_test_layer",
|
|
33
|
+
"delete_test_layer_schema",
|
|
17
34
|
"get_custom_fields",
|
|
35
|
+
"get_test_case_custom_fields",
|
|
18
36
|
"get_test_case_details",
|
|
19
37
|
"link_shared_step",
|
|
38
|
+
"list_launches",
|
|
20
39
|
"list_shared_steps",
|
|
21
40
|
"list_test_cases",
|
|
41
|
+
"list_test_layer_schemas",
|
|
42
|
+
"list_test_layers",
|
|
22
43
|
"search_test_cases",
|
|
23
44
|
"unlink_shared_step",
|
|
24
45
|
"update_shared_step",
|
|
25
46
|
"update_test_case",
|
|
47
|
+
"update_test_layer",
|
|
48
|
+
"update_test_layer_schema",
|
|
26
49
|
]
|
|
27
50
|
|
|
28
51
|
ToolFn = Callable[..., Awaitable[object]]
|
|
@@ -34,11 +57,23 @@ all_tools: list[ToolFn] = [
|
|
|
34
57
|
delete_test_case,
|
|
35
58
|
list_test_cases,
|
|
36
59
|
get_custom_fields,
|
|
60
|
+
get_test_case_custom_fields,
|
|
37
61
|
search_test_cases,
|
|
62
|
+
create_launch,
|
|
63
|
+
list_launches,
|
|
38
64
|
create_shared_step,
|
|
39
65
|
list_shared_steps,
|
|
40
66
|
update_shared_step,
|
|
41
67
|
delete_shared_step,
|
|
42
68
|
link_shared_step,
|
|
43
69
|
unlink_shared_step,
|
|
70
|
+
# Test Layer Tools
|
|
71
|
+
list_test_layers,
|
|
72
|
+
create_test_layer,
|
|
73
|
+
update_test_layer,
|
|
74
|
+
delete_test_layer,
|
|
75
|
+
list_test_layer_schemas,
|
|
76
|
+
create_test_layer_schema,
|
|
77
|
+
update_test_layer_schema,
|
|
78
|
+
delete_test_layer_schema,
|
|
44
79
|
]
|
src/tools/create_test_case.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Tool for creating Test Cases in Allure TestOps."""
|
|
2
2
|
|
|
3
|
-
from typing import Annotated
|
|
3
|
+
from typing import Annotated
|
|
4
4
|
|
|
5
5
|
from pydantic import Field
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ async def create_test_case(
|
|
|
12
12
|
name: Annotated[str, Field(description="The name of the test case.")],
|
|
13
13
|
description: Annotated[str | None, Field(description="A markdown description of the test case.")] = None,
|
|
14
14
|
steps: Annotated[
|
|
15
|
-
list[dict[str,
|
|
15
|
+
list[dict[str, object]] | None,
|
|
16
16
|
Field(
|
|
17
17
|
description="List of steps. Each step must be a dict with 'action' and 'expected' keys. "
|
|
18
18
|
"Example: [{'action': 'Login', 'expected': 'Dashboard visible'}]"
|
|
@@ -29,10 +29,28 @@ async def create_test_case(
|
|
|
29
29
|
),
|
|
30
30
|
] = None,
|
|
31
31
|
custom_fields: Annotated[
|
|
32
|
-
dict[str, str] | None,
|
|
32
|
+
dict[str, str | list[str]] | None,
|
|
33
33
|
Field(
|
|
34
|
-
description="Dictionary of custom field names and their values."
|
|
35
|
-
"Example: {'Layer': 'UI', '
|
|
34
|
+
description="Dictionary of custom field names and their values (string or list of strings)."
|
|
35
|
+
"Example: {'Layer': 'UI', 'Components': ['Auth', 'DB']}"
|
|
36
|
+
),
|
|
37
|
+
] = None,
|
|
38
|
+
test_layer_id: Annotated[
|
|
39
|
+
int | None,
|
|
40
|
+
Field(
|
|
41
|
+
description=(
|
|
42
|
+
"Optional test layer ID to assign (use list_test_layers to find IDs). "
|
|
43
|
+
"If provided, the layer must exist in the project."
|
|
44
|
+
)
|
|
45
|
+
),
|
|
46
|
+
] = None,
|
|
47
|
+
test_layer_name: Annotated[
|
|
48
|
+
str | None,
|
|
49
|
+
Field(
|
|
50
|
+
description=(
|
|
51
|
+
"Optional test layer name to assign (exact case-sensitive match). "
|
|
52
|
+
"Mutually exclusive with test_layer_id."
|
|
53
|
+
)
|
|
36
54
|
),
|
|
37
55
|
] = None,
|
|
38
56
|
project_id: Annotated[int | None, Field(description="Optional override for the default Project ID.")] = None,
|
|
@@ -49,8 +67,10 @@ async def create_test_case(
|
|
|
49
67
|
Example Base64: [{'name': 's.png', 'content': '<base64>', 'content_type': 'image/png'}]
|
|
50
68
|
Example URL: [{'name': 'report.pdf', 'url': 'http://example.com/report.pdf',
|
|
51
69
|
'content_type': 'application/pdf'}]
|
|
52
|
-
custom_fields: Dictionary of custom field names and their values.
|
|
53
|
-
Example: {'Layer': 'UI', '
|
|
70
|
+
custom_fields: Dictionary of custom field names and their values (string or list of strings).
|
|
71
|
+
Example: {'Layer': 'UI', 'Components': ['Auth', 'DB']}
|
|
72
|
+
test_layer_id: Optional test layer ID to assign (must exist in the project).
|
|
73
|
+
test_layer_name: Optional test layer name to assign (exact case-sensitive match).
|
|
54
74
|
project_id: Optional override for the default Project ID.
|
|
55
75
|
|
|
56
76
|
Returns:
|
|
@@ -62,15 +82,14 @@ async def create_test_case(
|
|
|
62
82
|
|
|
63
83
|
async with AllureClient.from_env(project=project_id) as client:
|
|
64
84
|
service = TestCaseService(client=client)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return f"Error creating test case: {e}"
|
|
85
|
+
result = await service.create_test_case(
|
|
86
|
+
name=name,
|
|
87
|
+
description=description,
|
|
88
|
+
steps=steps,
|
|
89
|
+
tags=tags,
|
|
90
|
+
attachments=attachments,
|
|
91
|
+
custom_fields=custom_fields,
|
|
92
|
+
test_layer_id=test_layer_id,
|
|
93
|
+
test_layer_name=test_layer_name,
|
|
94
|
+
)
|
|
95
|
+
return f"Created Test Case ID: {result.id} Name: {result.name}"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Tool for creating a test layer."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from src.client import AllureClient
|
|
8
|
+
from src.services.test_layer_service import TestLayerService
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def create_test_layer(
|
|
12
|
+
name: Annotated[str, Field(description="Name of the test layer (e.g., 'Unit', 'Integration', 'E2E').")],
|
|
13
|
+
project_id: Annotated[
|
|
14
|
+
int | None, Field(description="Allure TestOps project ID to create the test layer in.")
|
|
15
|
+
] = None,
|
|
16
|
+
) -> str:
|
|
17
|
+
"""Create a new test layer in Allure TestOps.
|
|
18
|
+
|
|
19
|
+
Test layers define taxonomy for categorizing test cases. Common examples include
|
|
20
|
+
'Unit', 'Integration', 'E2E', 'UI', 'API', etc.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
name: Name of the test layer
|
|
24
|
+
project_id: Optional project ID override
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Confirmation message with the created layer's ID and name
|
|
28
|
+
"""
|
|
29
|
+
async with AllureClient.from_env(project=project_id) as client:
|
|
30
|
+
service = TestLayerService(client)
|
|
31
|
+
layer = await service.create_test_layer(name=name)
|
|
32
|
+
|
|
33
|
+
return f"✅ Test layer created successfully! ID: {layer.id}, Name: {layer.name}"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Tool for creating a test layer schema."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from src.client import AllureClient
|
|
8
|
+
from src.services.test_layer_service import TestLayerService
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def create_test_layer_schema(
|
|
12
|
+
key: Annotated[str, Field(description="The schema key (e.g., custom field name like 'layer' or 'test_layer').")],
|
|
13
|
+
test_layer_id: Annotated[int, Field(description="ID of the test layer to link to this schema.")],
|
|
14
|
+
project_id: Annotated[int, Field(description="Allure TestOps project ID to create the schema in.")],
|
|
15
|
+
) -> str:
|
|
16
|
+
"""Create a new test layer schema to map a custom field key to a test layer.
|
|
17
|
+
|
|
18
|
+
Test layer schemas define the mapping between custom field keys and test layers.
|
|
19
|
+
This allows test cases with specific custom field values to be automatically
|
|
20
|
+
assigned to the correct test layer.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
key: Schema key (typically a custom field name)
|
|
24
|
+
test_layer_id: ID of the test layer to link
|
|
25
|
+
project_id: Project ID
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Confirmation message with the created schema's details
|
|
29
|
+
"""
|
|
30
|
+
async with AllureClient.from_env(project=project_id) as client:
|
|
31
|
+
service = TestLayerService(client)
|
|
32
|
+
schema = await service.create_test_layer_schema(
|
|
33
|
+
project_id=project_id,
|
|
34
|
+
test_layer_id=test_layer_id,
|
|
35
|
+
key=key,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
test_layer_name = schema.test_layer.name if schema.test_layer else "N/A"
|
|
39
|
+
return f"✅ Test layer schema created successfully! ID: {schema.id}, Key: {schema.key}, Layer: {test_layer_name}"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Tool for deleting a test layer."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from src.client import AllureClient
|
|
8
|
+
from src.services.test_layer_service import TestLayerService
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def delete_test_layer(
|
|
12
|
+
layer_id: Annotated[int, Field(description="ID of the test layer to delete.")],
|
|
13
|
+
project_id: Annotated[int | None, Field(description="Allure TestOps project ID.")] = None,
|
|
14
|
+
) -> str:
|
|
15
|
+
"""Delete a test layer from Allure TestOps.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
layer_id: ID of the test layer to delete
|
|
19
|
+
project_id: Optional project ID override
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Confirmation message
|
|
23
|
+
"""
|
|
24
|
+
async with AllureClient.from_env(project=project_id) as client:
|
|
25
|
+
service = TestLayerService(client)
|
|
26
|
+
deleted = await service.delete_test_layer(layer_id=layer_id)
|
|
27
|
+
|
|
28
|
+
if deleted:
|
|
29
|
+
return f"✅ Test layer {layer_id} deleted successfully!"
|
|
30
|
+
else:
|
|
31
|
+
return f"[INFO] Test layer {layer_id} was already deleted or doesn't exist - no action taken."
|