lucius-mcp 0.3.0__py3-none-any.whl → 0.4.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.4.0.dist-info/METADATA +124 -0
- {lucius_mcp-0.3.0.dist-info → lucius_mcp-0.4.0.dist-info}/RECORD +38 -29
- src/client/__init__.py +6 -0
- src/client/client.py +271 -7
- src/client/exceptions.py +23 -0
- src/client/generated/README.md +18 -0
- src/client/generated/__init__.py +2 -0
- src/client/generated/api/__init__.py +1 -0
- src/client/generated/api/integration_controller_api.py +5285 -0
- src/client/generated/docs/IntegrationControllerApi.md +1224 -0
- src/services/__init__.py +9 -1
- src/services/custom_field_value_service.py +301 -0
- src/services/integration_service.py +205 -0
- src/services/launch_service.py +29 -1
- src/services/shared_step_service.py +34 -17
- src/services/test_case_service.py +269 -37
- src/services/test_layer_service.py +11 -0
- src/tools/__init__.py +19 -1
- src/tools/create_custom_field_value.py +38 -0
- src/tools/create_test_case.py +32 -2
- src/tools/delete_custom_field_value.py +57 -0
- src/tools/delete_test_case.py +5 -4
- src/tools/delete_test_layer.py +17 -4
- src/tools/delete_test_layer_schema.py +17 -4
- src/tools/launches.py +86 -0
- src/tools/link_shared_step.py +18 -12
- src/tools/list_custom_field_values.py +72 -0
- src/tools/list_integrations.py +77 -0
- src/tools/search.py +3 -3
- src/tools/shared_steps.py +23 -8
- src/tools/unlink_shared_step.py +19 -5
- src/tools/update_custom_field_value.py +55 -0
- src/tools/update_test_case.py +67 -2
- src/tools/update_test_layer.py +15 -4
- src/tools/update_test_layer_schema.py +16 -5
- lucius_mcp-0.3.0.dist-info/METADATA +0 -297
- {lucius_mcp-0.3.0.dist-info → lucius_mcp-0.4.0.dist-info}/WHEEL +0 -0
- {lucius_mcp-0.3.0.dist-info → lucius_mcp-0.4.0.dist-info}/entry_points.txt +0 -0
- {lucius_mcp-0.3.0.dist-info → lucius_mcp-0.4.0.dist-info}/licenses/LICENSE +0 -0
src/tools/__init__.py
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
from collections.abc import Awaitable, Callable
|
|
2
2
|
|
|
3
|
+
from src.tools.create_custom_field_value import create_custom_field_value
|
|
3
4
|
from src.tools.create_test_case import create_test_case
|
|
5
|
+
from src.tools.delete_custom_field_value import delete_custom_field_value
|
|
4
6
|
from src.tools.delete_test_case import delete_test_case
|
|
5
7
|
from src.tools.get_custom_fields import get_custom_fields
|
|
6
8
|
from src.tools.get_test_case_custom_fields import get_test_case_custom_fields
|
|
7
|
-
from src.tools.launches import create_launch, list_launches
|
|
9
|
+
from src.tools.launches import create_launch, get_launch, list_launches
|
|
8
10
|
from src.tools.link_shared_step import link_shared_step
|
|
11
|
+
from src.tools.list_custom_field_values import list_custom_field_values
|
|
12
|
+
from src.tools.list_integrations import list_integrations
|
|
9
13
|
from src.tools.search import get_test_case_details, list_test_cases, search_test_cases
|
|
10
14
|
from src.tools.shared_steps import create_shared_step, delete_shared_step, list_shared_steps, update_shared_step
|
|
11
15
|
from src.tools.test_layers import (
|
|
@@ -19,22 +23,28 @@ from src.tools.test_layers import (
|
|
|
19
23
|
update_test_layer_schema,
|
|
20
24
|
)
|
|
21
25
|
from src.tools.unlink_shared_step import unlink_shared_step
|
|
26
|
+
from src.tools.update_custom_field_value import update_custom_field_value
|
|
22
27
|
from src.tools.update_test_case import update_test_case
|
|
23
28
|
|
|
24
29
|
__all__ = [
|
|
30
|
+
"create_custom_field_value",
|
|
25
31
|
"create_launch",
|
|
26
32
|
"create_shared_step",
|
|
27
33
|
"create_test_case",
|
|
28
34
|
"create_test_layer",
|
|
29
35
|
"create_test_layer_schema",
|
|
36
|
+
"delete_custom_field_value",
|
|
30
37
|
"delete_shared_step",
|
|
31
38
|
"delete_test_case",
|
|
32
39
|
"delete_test_layer",
|
|
33
40
|
"delete_test_layer_schema",
|
|
34
41
|
"get_custom_fields",
|
|
42
|
+
"get_launch",
|
|
35
43
|
"get_test_case_custom_fields",
|
|
36
44
|
"get_test_case_details",
|
|
37
45
|
"link_shared_step",
|
|
46
|
+
"list_custom_field_values",
|
|
47
|
+
"list_integrations",
|
|
38
48
|
"list_launches",
|
|
39
49
|
"list_shared_steps",
|
|
40
50
|
"list_test_cases",
|
|
@@ -42,6 +52,7 @@ __all__ = [
|
|
|
42
52
|
"list_test_layers",
|
|
43
53
|
"search_test_cases",
|
|
44
54
|
"unlink_shared_step",
|
|
55
|
+
"update_custom_field_value",
|
|
45
56
|
"update_shared_step",
|
|
46
57
|
"update_test_case",
|
|
47
58
|
"update_test_layer",
|
|
@@ -57,10 +68,17 @@ all_tools: list[ToolFn] = [
|
|
|
57
68
|
delete_test_case,
|
|
58
69
|
list_test_cases,
|
|
59
70
|
get_custom_fields,
|
|
71
|
+
list_custom_field_values,
|
|
72
|
+
create_custom_field_value,
|
|
73
|
+
update_custom_field_value,
|
|
74
|
+
delete_custom_field_value,
|
|
60
75
|
get_test_case_custom_fields,
|
|
61
76
|
search_test_cases,
|
|
62
77
|
create_launch,
|
|
78
|
+
get_launch,
|
|
63
79
|
list_launches,
|
|
80
|
+
# Integration Tools
|
|
81
|
+
list_integrations,
|
|
64
82
|
create_shared_step,
|
|
65
83
|
list_shared_steps,
|
|
66
84
|
update_shared_step,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from src.client import AllureClient
|
|
6
|
+
from src.services.custom_field_value_service import CustomFieldValueService
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def create_custom_field_value(
|
|
10
|
+
name: Annotated[str, Field(description="Name for the new custom field value.")],
|
|
11
|
+
custom_field_id: Annotated[
|
|
12
|
+
int | None,
|
|
13
|
+
Field(description="Project-scoped custom field ID to create a value for."),
|
|
14
|
+
] = None,
|
|
15
|
+
custom_field_name: Annotated[
|
|
16
|
+
str | None,
|
|
17
|
+
Field(description="Custom field name to resolve when custom_field_id is not provided."),
|
|
18
|
+
] = None,
|
|
19
|
+
project_id: Annotated[int | None, Field(description="Optional override for the default Project ID.")] = None,
|
|
20
|
+
) -> str:
|
|
21
|
+
"""Create a new custom field value option.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
name: Name for the new custom field value.
|
|
25
|
+
custom_field_id: Project-scoped custom field ID to create a value for.
|
|
26
|
+
custom_field_name: Custom field name to resolve when custom_field_id is not provided.
|
|
27
|
+
project_id: Optional override for the default Project ID.
|
|
28
|
+
"""
|
|
29
|
+
async with AllureClient.from_env(project=project_id) as client:
|
|
30
|
+
service = CustomFieldValueService(client=client)
|
|
31
|
+
created = await service.create_custom_field_value(
|
|
32
|
+
project_id=project_id,
|
|
33
|
+
custom_field_id=custom_field_id,
|
|
34
|
+
custom_field_name=custom_field_name,
|
|
35
|
+
name=name,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return f"✅ Custom field value created successfully!\nID: {created.id}\nName: {created.name}"
|
src/tools/create_test_case.py
CHANGED
|
@@ -53,6 +53,28 @@ async def create_test_case(
|
|
|
53
53
|
)
|
|
54
54
|
),
|
|
55
55
|
] = None,
|
|
56
|
+
issues: Annotated[
|
|
57
|
+
list[str] | None,
|
|
58
|
+
Field(description="Optional list of issue keys to link (e.g., ['PROJ-123'])."),
|
|
59
|
+
] = None,
|
|
60
|
+
integration_id: Annotated[
|
|
61
|
+
int | None,
|
|
62
|
+
Field(
|
|
63
|
+
description=(
|
|
64
|
+
"Optional integration ID for issue linking (use list_integrations to find IDs). "
|
|
65
|
+
"Required when multiple integrations exist. Mutually exclusive with integration_name."
|
|
66
|
+
)
|
|
67
|
+
),
|
|
68
|
+
] = None,
|
|
69
|
+
integration_name: Annotated[
|
|
70
|
+
str | None,
|
|
71
|
+
Field(
|
|
72
|
+
description=(
|
|
73
|
+
"Optional integration name for issue linking (exact case-sensitive match). "
|
|
74
|
+
"Required when multiple integrations exist. Mutually exclusive with integration_id."
|
|
75
|
+
)
|
|
76
|
+
),
|
|
77
|
+
] = None,
|
|
56
78
|
project_id: Annotated[int | None, Field(description="Optional override for the default Project ID.")] = None,
|
|
57
79
|
) -> str:
|
|
58
80
|
"""Create a new test case in Allure TestOps.
|
|
@@ -69,8 +91,10 @@ async def create_test_case(
|
|
|
69
91
|
'content_type': 'application/pdf'}]
|
|
70
92
|
custom_fields: Dictionary of custom field names and their values (string or list of strings).
|
|
71
93
|
Example: {'Layer': 'UI', 'Components': ['Auth', 'DB']}
|
|
72
|
-
test_layer_id: Optional test layer ID to assign (must exist in the project).
|
|
73
94
|
test_layer_name: Optional test layer name to assign (exact case-sensitive match).
|
|
95
|
+
issues: Optional list of issue keys to link (e.g., ['PROJ-123']).
|
|
96
|
+
integration_id: Optional integration ID for issue linking (required when multiple integrations exist).
|
|
97
|
+
integration_name: Optional integration name for issue linking (mutually exclusive with integration_id).
|
|
74
98
|
project_id: Optional override for the default Project ID.
|
|
75
99
|
|
|
76
100
|
Returns:
|
|
@@ -91,5 +115,11 @@ async def create_test_case(
|
|
|
91
115
|
custom_fields=custom_fields,
|
|
92
116
|
test_layer_id=test_layer_id,
|
|
93
117
|
test_layer_name=test_layer_name,
|
|
118
|
+
issues=issues,
|
|
119
|
+
integration_id=integration_id,
|
|
120
|
+
integration_name=integration_name,
|
|
94
121
|
)
|
|
95
|
-
|
|
122
|
+
msg = f"Created Test Case ID: {result.id} Name: {result.name}"
|
|
123
|
+
if issues:
|
|
124
|
+
msg += f" with {len(issues)} linked issues: {', '.join(issues)}"
|
|
125
|
+
return msg
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from src.client import AllureClient
|
|
6
|
+
from src.services.custom_field_value_service import CustomFieldValueService
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def delete_custom_field_value(
|
|
10
|
+
cfv_id: Annotated[int, Field(description="Custom field value ID to delete.")],
|
|
11
|
+
custom_field_id: Annotated[
|
|
12
|
+
int | None,
|
|
13
|
+
Field(description="Project-scoped custom field ID (optional, resolves by name if missing)."),
|
|
14
|
+
] = None,
|
|
15
|
+
custom_field_name: Annotated[
|
|
16
|
+
str | None,
|
|
17
|
+
Field(description="Custom field name to resolve when custom_field_id is not provided."),
|
|
18
|
+
] = None,
|
|
19
|
+
project_id: Annotated[int | None, Field(description="Optional override for the default Project ID.")] = None,
|
|
20
|
+
confirm: Annotated[
|
|
21
|
+
bool, Field(description="Must be set to True to proceed with deletion. Safety measure.")
|
|
22
|
+
] = False,
|
|
23
|
+
) -> str:
|
|
24
|
+
"""Delete a custom field value.
|
|
25
|
+
⚠️ CAUTION: Destructive.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
cfv_id: Custom field value ID to delete.
|
|
29
|
+
custom_field_id: Project-scoped custom field ID (optional, resolves by name if missing).
|
|
30
|
+
custom_field_name: Custom field name to resolve when custom_field_id is not provided.
|
|
31
|
+
project_id: Optional Allure TestOps project ID override.
|
|
32
|
+
confirm: Must be set to True to proceed with deletion.
|
|
33
|
+
This is a safety measure to prevent accidental deletions.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Confirmation message.
|
|
37
|
+
"""
|
|
38
|
+
if not confirm:
|
|
39
|
+
return (
|
|
40
|
+
"⚠️ Deletion requires confirmation.\n\n"
|
|
41
|
+
"Deleting a custom field value may fail if it is currently in use. "
|
|
42
|
+
f"Please call again with confirm=True to proceed with deleting custom field value {cfv_id}."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
async with AllureClient.from_env(project=project_id) as client:
|
|
46
|
+
service = CustomFieldValueService(client=client)
|
|
47
|
+
deleted = await service.delete_custom_field_value(
|
|
48
|
+
project_id=project_id,
|
|
49
|
+
cfv_id=cfv_id,
|
|
50
|
+
custom_field_id=custom_field_id,
|
|
51
|
+
custom_field_name=custom_field_name,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if deleted:
|
|
55
|
+
return f"✅ Custom field value {cfv_id} deleted successfully!"
|
|
56
|
+
|
|
57
|
+
return f"[INFO] Custom field value {cfv_id} was already removed."
|
src/tools/delete_test_case.py
CHANGED
|
@@ -16,6 +16,7 @@ async def delete_test_case(
|
|
|
16
16
|
project_id: Annotated[int | None, Field(description="Optional Allure TestOps project ID override.")] = None,
|
|
17
17
|
) -> str:
|
|
18
18
|
"""Archive an obsolete test case.
|
|
19
|
+
⚠️ CAUTION: Destructive.
|
|
19
20
|
|
|
20
21
|
This performs a SOFT DELETE (archive). The test case can typically
|
|
21
22
|
be recovered from the Allure UI if needed.
|
|
@@ -24,11 +25,11 @@ async def delete_test_case(
|
|
|
24
25
|
Historical data and launch associations may be affected.
|
|
25
26
|
|
|
26
27
|
Args:
|
|
27
|
-
test_case_id: The Allure test case ID to archive
|
|
28
|
+
test_case_id: The Allure test case ID to archive.
|
|
28
29
|
Found in the URL: /testcase/12345 -> test_case_id=12345
|
|
29
30
|
confirm: Must be set to True to proceed with deletion.
|
|
30
31
|
This is a safety measure to prevent accidental deletions.
|
|
31
|
-
project_id: Optional
|
|
32
|
+
project_id: Optional Allure TestOps project ID override.
|
|
32
33
|
|
|
33
34
|
Returns:
|
|
34
35
|
Confirmation message with the archived test case details.
|
|
@@ -36,8 +37,8 @@ async def delete_test_case(
|
|
|
36
37
|
if not confirm:
|
|
37
38
|
return (
|
|
38
39
|
"⚠️ Deletion requires confirmation.\n\n"
|
|
39
|
-
"
|
|
40
|
-
f"test case {test_case_id}."
|
|
40
|
+
"Archiving test cases removes them from active views. "
|
|
41
|
+
f"Please call again with confirm=True to proceed with archiving test case {test_case_id}."
|
|
41
42
|
)
|
|
42
43
|
|
|
43
44
|
async with AllureClient.from_env(project=project_id) as client:
|
src/tools/delete_test_layer.py
CHANGED
|
@@ -11,16 +11,29 @@ from src.services.test_layer_service import TestLayerService
|
|
|
11
11
|
async def delete_test_layer(
|
|
12
12
|
layer_id: Annotated[int, Field(description="ID of the test layer to delete.")],
|
|
13
13
|
project_id: Annotated[int | None, Field(description="Allure TestOps project ID.")] = None,
|
|
14
|
+
confirm: Annotated[
|
|
15
|
+
bool, Field(description="Must be set to True to proceed with deletion. Safety measure.")
|
|
16
|
+
] = False,
|
|
14
17
|
) -> str:
|
|
15
18
|
"""Delete a test layer from Allure TestOps.
|
|
19
|
+
⚠️ CAUTION: Destructive.
|
|
16
20
|
|
|
17
21
|
Args:
|
|
18
|
-
layer_id: ID of the test layer to delete
|
|
19
|
-
project_id: Optional project ID override
|
|
22
|
+
layer_id: ID of the test layer to delete.
|
|
23
|
+
project_id: Optional Allure TestOps project ID override.
|
|
24
|
+
confirm: Must be set to True to proceed with deletion.
|
|
25
|
+
This is a safety measure to prevent accidental deletions.
|
|
20
26
|
|
|
21
27
|
Returns:
|
|
22
|
-
Confirmation message
|
|
28
|
+
Confirmation message.
|
|
23
29
|
"""
|
|
30
|
+
if not confirm:
|
|
31
|
+
return (
|
|
32
|
+
"⚠️ Deletion requires confirmation.\n\n"
|
|
33
|
+
"Deleting a test layer may affect test case categorization. "
|
|
34
|
+
f"Please call again with confirm=True to proceed with deleting test layer {layer_id}."
|
|
35
|
+
)
|
|
36
|
+
|
|
24
37
|
async with AllureClient.from_env(project=project_id) as client:
|
|
25
38
|
service = TestLayerService(client)
|
|
26
39
|
deleted = await service.delete_test_layer(layer_id=layer_id)
|
|
@@ -28,4 +41,4 @@ async def delete_test_layer(
|
|
|
28
41
|
if deleted:
|
|
29
42
|
return f"✅ Test layer {layer_id} deleted successfully!"
|
|
30
43
|
else:
|
|
31
|
-
return f"
|
|
44
|
+
return f"ℹ️ Test layer {layer_id} was already deleted or doesn't exist." # noqa: RUF001
|
|
@@ -11,16 +11,29 @@ from src.services.test_layer_service import TestLayerService
|
|
|
11
11
|
async def delete_test_layer_schema(
|
|
12
12
|
schema_id: Annotated[int, Field(description="ID of the test layer schema to delete.")],
|
|
13
13
|
project_id: Annotated[int | None, Field(description="Allure TestOps project ID.")] = None,
|
|
14
|
+
confirm: Annotated[
|
|
15
|
+
bool, Field(description="Must be set to True to proceed with deletion. Safety measure.")
|
|
16
|
+
] = False,
|
|
14
17
|
) -> str:
|
|
15
18
|
"""Delete a test layer schema from the project.
|
|
19
|
+
⚠️ CAUTION: Destructive.
|
|
16
20
|
|
|
17
21
|
Args:
|
|
18
|
-
schema_id: ID of the schema to delete
|
|
19
|
-
project_id: Optional project ID override
|
|
22
|
+
schema_id: ID of the schema to delete.
|
|
23
|
+
project_id: Optional Allure TestOps project ID override.
|
|
24
|
+
confirm: Must be set to True to proceed with deletion.
|
|
25
|
+
This is a safety measure to prevent accidental deletions.
|
|
20
26
|
|
|
21
27
|
Returns:
|
|
22
|
-
Confirmation message
|
|
28
|
+
Confirmation message.
|
|
23
29
|
"""
|
|
30
|
+
if not confirm:
|
|
31
|
+
return (
|
|
32
|
+
"⚠️ Deletion requires confirmation.\n\n"
|
|
33
|
+
"Deleting a schema removes the mapping between custom fields and layers. "
|
|
34
|
+
f"Please call again with confirm=True to proceed with deleting test layer schema {schema_id}."
|
|
35
|
+
)
|
|
36
|
+
|
|
24
37
|
async with AllureClient.from_env(project=project_id) as client:
|
|
25
38
|
service = TestLayerService(client)
|
|
26
39
|
deleted = await service.delete_test_layer_schema(schema_id=schema_id)
|
|
@@ -28,4 +41,4 @@ async def delete_test_layer_schema(
|
|
|
28
41
|
if deleted:
|
|
29
42
|
return f"✅ Test layer schema {schema_id} deleted successfully!"
|
|
30
43
|
else:
|
|
31
|
-
return f"
|
|
44
|
+
return f"ℹ️ Test layer schema {schema_id} was already deleted or doesn't exist." # noqa RUF001
|
src/tools/launches.py
CHANGED
|
@@ -6,6 +6,8 @@ from pydantic import Field
|
|
|
6
6
|
|
|
7
7
|
from src.client import AllureClient
|
|
8
8
|
from src.services.launch_service import LaunchListResult, LaunchService
|
|
9
|
+
from src.utils.auth import get_auth_context
|
|
10
|
+
from src.utils.config import settings
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
async def create_launch(
|
|
@@ -88,6 +90,35 @@ async def list_launches(
|
|
|
88
90
|
return _format_launch_list(result)
|
|
89
91
|
|
|
90
92
|
|
|
93
|
+
async def get_launch(
|
|
94
|
+
launch_id: Annotated[int, Field(description="Launch ID (required).")],
|
|
95
|
+
project_id: Annotated[int | None, Field(description="Optional override for the default Project ID.")] = None,
|
|
96
|
+
) -> str:
|
|
97
|
+
"""Retrieve a specific launch and summarize its details.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
launch_id: The unique ID of the launch.
|
|
101
|
+
project_id: Optional override for the default Project ID.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
LLM-friendly summary of the launch details.
|
|
105
|
+
"""
|
|
106
|
+
auth_context = get_auth_context(project_id=project_id)
|
|
107
|
+
project = auth_context.project_id or settings.ALLURE_PROJECT_ID
|
|
108
|
+
if project is None:
|
|
109
|
+
raise ValueError("Project ID is required to retrieve launch details")
|
|
110
|
+
|
|
111
|
+
async with AllureClient(
|
|
112
|
+
base_url=settings.ALLURE_ENDPOINT,
|
|
113
|
+
token=auth_context.api_token,
|
|
114
|
+
project=project,
|
|
115
|
+
) as client:
|
|
116
|
+
service = LaunchService(client=client)
|
|
117
|
+
launch = await service.get_launch(launch_id)
|
|
118
|
+
|
|
119
|
+
return _format_launch_detail(launch)
|
|
120
|
+
|
|
121
|
+
|
|
91
122
|
def _format_launch_list(result: LaunchListResult) -> str:
|
|
92
123
|
if not result.items:
|
|
93
124
|
return "No launches found in this project."
|
|
@@ -110,3 +141,58 @@ def _format_launch_list(result: LaunchListResult) -> str:
|
|
|
110
141
|
lines.append(f"\nUse page={result.page + 1} to see more results.")
|
|
111
142
|
|
|
112
143
|
return "\n".join(lines)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _format_launch_detail(launch: object) -> str:
|
|
147
|
+
launch_id = getattr(launch, "id", None)
|
|
148
|
+
name = getattr(launch, "name", None) or "(unnamed)"
|
|
149
|
+
closed = getattr(launch, "closed", None)
|
|
150
|
+
status = "closed" if closed else "open"
|
|
151
|
+
|
|
152
|
+
lines = ["Launch details:"]
|
|
153
|
+
lines.append(f"- ID: {launch_id if launch_id is not None else 'unknown'}")
|
|
154
|
+
lines.append(f"- Name: {name}")
|
|
155
|
+
lines.append(f"- Status: {status}")
|
|
156
|
+
|
|
157
|
+
started_at = getattr(launch, "created_date", None) or getattr(launch, "createdDate", None)
|
|
158
|
+
ended_at = getattr(launch, "last_modified_date", None) or getattr(launch, "lastModifiedDate", None)
|
|
159
|
+
|
|
160
|
+
if started_at is not None:
|
|
161
|
+
lines.append(f"- Started: {started_at}")
|
|
162
|
+
if ended_at is not None:
|
|
163
|
+
lines.append(f"- Ended: {ended_at}")
|
|
164
|
+
|
|
165
|
+
field_labels = [
|
|
166
|
+
("project_id", "Project ID"),
|
|
167
|
+
("projectId", "Project ID"),
|
|
168
|
+
("autoclose", "Autoclose"),
|
|
169
|
+
("external", "External"),
|
|
170
|
+
("known_defects_count", "Known defects"),
|
|
171
|
+
("knownDefectsCount", "Known defects"),
|
|
172
|
+
("new_defects_count", "New defects"),
|
|
173
|
+
("newDefectsCount", "New defects"),
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
seen_labels: set[str] = set()
|
|
177
|
+
for field_name, label in field_labels:
|
|
178
|
+
value = getattr(launch, field_name, None)
|
|
179
|
+
if value is None:
|
|
180
|
+
continue
|
|
181
|
+
if label in seen_labels:
|
|
182
|
+
continue
|
|
183
|
+
lines.append(f"- {label}: {value}")
|
|
184
|
+
seen_labels.add(label)
|
|
185
|
+
|
|
186
|
+
statistic = getattr(launch, "statistic", None)
|
|
187
|
+
if statistic:
|
|
188
|
+
summary_parts: list[str] = []
|
|
189
|
+
for item in statistic:
|
|
190
|
+
status_label = getattr(item, "status", None)
|
|
191
|
+
count = getattr(item, "count", None)
|
|
192
|
+
if status_label is None or count is None:
|
|
193
|
+
continue
|
|
194
|
+
summary_parts.append(f"{status_label!s}={count}")
|
|
195
|
+
if summary_parts:
|
|
196
|
+
lines.append(f"- Summary: {', '.join(summary_parts)}")
|
|
197
|
+
|
|
198
|
+
return "\n".join(lines)
|
src/tools/link_shared_step.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Tool for linking Shared Steps to Test Cases."""
|
|
2
2
|
|
|
3
|
-
from typing import Annotated
|
|
3
|
+
from typing import Annotated
|
|
4
4
|
|
|
5
5
|
from pydantic import Field
|
|
6
6
|
|
|
@@ -9,25 +9,19 @@ from src.client.generated.models.shared_step_step_dto import SharedStepStepDto
|
|
|
9
9
|
from src.services.test_case_service import TestCaseService
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def _format_steps(scenario:
|
|
12
|
+
def _format_steps(scenario: object) -> str:
|
|
13
13
|
"""Format steps for display."""
|
|
14
|
-
|
|
14
|
+
steps = getattr(scenario, "steps", None)
|
|
15
|
+
if not isinstance(steps, list) or not steps:
|
|
15
16
|
return "No steps."
|
|
16
17
|
|
|
17
18
|
output = []
|
|
18
|
-
for i, step in enumerate(
|
|
19
|
+
for i, step in enumerate(steps):
|
|
19
20
|
# Handle Shared Step
|
|
20
21
|
if step.actual_instance and isinstance(step.actual_instance, SharedStepStepDto):
|
|
21
|
-
# Ideally we would have the name, but DTO might only have ID if not enriched.
|
|
22
|
-
# SharedStepStepDto has 'sharedStepId'.
|
|
23
|
-
# Does it have 'name'? The scenarios steps in 'get_test_case_scenario'
|
|
24
|
-
# might not have name enriched unless we fetched it or denormalized it.
|
|
25
|
-
# The _denormalize_to_v2_from_dict doesn't currently inject name.
|
|
26
|
-
# For now, just show ID.
|
|
27
22
|
output.append(f"{i + 1}. [Shared Step] ID: {step.actual_instance.shared_step_id}")
|
|
28
23
|
else:
|
|
29
24
|
# Inline step
|
|
30
|
-
# We assume it has some description or body
|
|
31
25
|
body = "Step"
|
|
32
26
|
if hasattr(step.actual_instance, "body"):
|
|
33
27
|
body = step.actual_instance.body or "Step"
|
|
@@ -48,8 +42,10 @@ async def link_shared_step(
|
|
|
48
42
|
),
|
|
49
43
|
] = None,
|
|
50
44
|
project_id: Annotated[int | None, Field(description="Optional override for the default Project ID.")] = None,
|
|
45
|
+
confirm: Annotated[bool, Field(description="Must be set to True to proceed with linking. Safety measure.")] = False,
|
|
51
46
|
) -> str:
|
|
52
47
|
"""Link a shared step to a test case.
|
|
48
|
+
⚠️ CAUTION: Destructive.
|
|
53
49
|
|
|
54
50
|
Adds a reference to the shared step in the test case's step list.
|
|
55
51
|
The shared step's actions will expand at execution time.
|
|
@@ -64,6 +60,8 @@ async def link_shared_step(
|
|
|
64
60
|
- None = Append to end (default)
|
|
65
61
|
- N = Insert after step N (so it becomes the (N+1)th step)
|
|
66
62
|
project_id: Optional override for the default Project ID.
|
|
63
|
+
confirm: Must be set to True to proceed with linking.
|
|
64
|
+
This is a safety measure to prevent accidental linking.
|
|
67
65
|
|
|
68
66
|
Returns:
|
|
69
67
|
Confirmation with updated step list preview.
|
|
@@ -72,9 +70,17 @@ async def link_shared_step(
|
|
|
72
70
|
link_shared_step(
|
|
73
71
|
test_case_id=12345,
|
|
74
72
|
shared_step_id=789, # "Login as Admin"
|
|
75
|
-
position=0
|
|
73
|
+
position=0,
|
|
74
|
+
confirm=True
|
|
76
75
|
)
|
|
77
76
|
"""
|
|
77
|
+
if not confirm:
|
|
78
|
+
return (
|
|
79
|
+
"⚠️ Linking requires confirmation.\n\n"
|
|
80
|
+
"This will add a shared step to the test case scenario. "
|
|
81
|
+
f"Please call again with confirm=True to proceed with linking "
|
|
82
|
+
f"shared step {shared_step_id} to test case {test_case_id}."
|
|
83
|
+
)
|
|
78
84
|
|
|
79
85
|
async with AllureClient.from_env(project=project_id) as client:
|
|
80
86
|
service = TestCaseService(client=client)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from src.client import AllureClient
|
|
6
|
+
from src.services.custom_field_value_service import CustomFieldValueService
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def list_custom_field_values(
|
|
10
|
+
custom_field_id: Annotated[
|
|
11
|
+
int | None, Field(description="Project-scoped custom field ID to list values for.")
|
|
12
|
+
] = None,
|
|
13
|
+
custom_field_name: Annotated[
|
|
14
|
+
str | None,
|
|
15
|
+
Field(description="Custom field name to resolve when custom_field_id is not provided."),
|
|
16
|
+
] = None,
|
|
17
|
+
query: Annotated[str | None, Field(description="Optional search query to filter values.")] = None,
|
|
18
|
+
var_global: Annotated[bool | None, Field(description="Optional filter for global values.")] = None,
|
|
19
|
+
test_case_search: Annotated[
|
|
20
|
+
str | None,
|
|
21
|
+
Field(description="Optional test case search filter to match values in test cases."),
|
|
22
|
+
] = None,
|
|
23
|
+
page: Annotated[int | None, Field(description="Zero-based page index.")] = None,
|
|
24
|
+
size: Annotated[int | None, Field(description="Number of items per page.")] = None,
|
|
25
|
+
sort: Annotated[
|
|
26
|
+
list[str] | None,
|
|
27
|
+
Field(description="Optional sort criteria, e.g. ['name,asc', 'id,desc']"),
|
|
28
|
+
] = None,
|
|
29
|
+
project_id: Annotated[int | None, Field(description="Optional override for the default Project ID.")] = None,
|
|
30
|
+
) -> str:
|
|
31
|
+
"""List available values for a custom field.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
custom_field_id: Project-scoped custom field ID to list values for.
|
|
35
|
+
custom_field_name: Custom field name to resolve when custom_field_id is not provided.
|
|
36
|
+
query: Optional search query to filter values.
|
|
37
|
+
var_global: Optional filter for global values.
|
|
38
|
+
test_case_search: Optional test case search filter to match values in test cases.
|
|
39
|
+
page: Zero-based page index.
|
|
40
|
+
size: Number of items per page.
|
|
41
|
+
sort: Optional sort criteria, e.g. ["name,asc", "id,desc"].
|
|
42
|
+
project_id: Optional override for the default Project ID.
|
|
43
|
+
"""
|
|
44
|
+
async with AllureClient.from_env(project=project_id) as client:
|
|
45
|
+
service = CustomFieldValueService(client=client)
|
|
46
|
+
result = await service.list_custom_field_values(
|
|
47
|
+
project_id=project_id,
|
|
48
|
+
custom_field_id=custom_field_id,
|
|
49
|
+
custom_field_name=custom_field_name,
|
|
50
|
+
query=query,
|
|
51
|
+
var_global=var_global,
|
|
52
|
+
test_case_search=test_case_search,
|
|
53
|
+
page=page,
|
|
54
|
+
size=size,
|
|
55
|
+
sort=sort,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
values = result.content or []
|
|
59
|
+
if not values:
|
|
60
|
+
return "No custom field values found."
|
|
61
|
+
|
|
62
|
+
total = result.total_elements if result.total_elements is not None else len(values)
|
|
63
|
+
header = f"Found {total} custom field values:"
|
|
64
|
+
if result.number is not None and result.total_pages is not None:
|
|
65
|
+
header = f"Found {total} custom field values (page {result.number + 1}/{result.total_pages}):"
|
|
66
|
+
|
|
67
|
+
lines = [header]
|
|
68
|
+
for value in values:
|
|
69
|
+
count = value.test_cases_count if value.test_cases_count is not None else 0
|
|
70
|
+
lines.append(f"- ID: {value.id}, Name: {value.name}, Test cases: {count}")
|
|
71
|
+
|
|
72
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Tool for listing available integrations in Allure TestOps."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from src.client import AllureClient
|
|
8
|
+
from src.services.integration_service import IntegrationService
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def list_integrations(
|
|
12
|
+
project_id: Annotated[
|
|
13
|
+
int | None,
|
|
14
|
+
Field(
|
|
15
|
+
description=(
|
|
16
|
+
"Optional override for the default Project ID. "
|
|
17
|
+
"Note: Integrations are configured at the instance level, not per-project, "
|
|
18
|
+
"but this parameter is accepted for API consistency."
|
|
19
|
+
)
|
|
20
|
+
),
|
|
21
|
+
] = None,
|
|
22
|
+
) -> str:
|
|
23
|
+
"""List available integrations (issue trackers) in Allure TestOps.
|
|
24
|
+
|
|
25
|
+
Use this tool to discover which integrations (Jira, GitHub, etc.) are
|
|
26
|
+
configured and available for linking issues to test cases.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
project_id: Optional override for the default Project ID.
|
|
30
|
+
Note: Integrations are configured at the instance level, not per-project,
|
|
31
|
+
but this parameter is accepted for API consistency.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
A formatted list of available integrations with their IDs and names.
|
|
35
|
+
If no integrations are configured, returns an informative message.
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
>>> list_integrations()
|
|
39
|
+
"📋 Available Integrations (3 found):
|
|
40
|
+
• Github Integration (ID: 1) [github]
|
|
41
|
+
• Noxtua Jira Integration (ID: 2) [jira]
|
|
42
|
+
• TestOps TMS (ID: 3) [tms]
|
|
43
|
+
|
|
44
|
+
Hint: Use 'integration_id' or 'integration_name' when creating/updating
|
|
45
|
+
test cases with issues to specify which integration to use."
|
|
46
|
+
"""
|
|
47
|
+
async with AllureClient.from_env(project=project_id) as client:
|
|
48
|
+
service = IntegrationService(client)
|
|
49
|
+
integrations = await service.list_integrations()
|
|
50
|
+
|
|
51
|
+
if not integrations:
|
|
52
|
+
return (
|
|
53
|
+
"📋 No integrations configured.\n\n"
|
|
54
|
+
"Integrations connect Allure TestOps to external issue trackers "
|
|
55
|
+
"(Jira, GitHub, etc.). Contact your Allure admin to configure integrations."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Format output
|
|
59
|
+
lines: list[str] = [f"📋 **Available Integrations** ({len(integrations)} found):\n"]
|
|
60
|
+
|
|
61
|
+
for integration in integrations:
|
|
62
|
+
name = integration.name or "(unnamed)"
|
|
63
|
+
int_id = integration.id or "N/A"
|
|
64
|
+
info_type = ""
|
|
65
|
+
if integration.info and hasattr(integration.info, "type") and integration.info.type:
|
|
66
|
+
# Handle enum or string; since IntegrationTypeDto is (str, Enum), .value works or its just the str
|
|
67
|
+
info_type = getattr(integration.info.type, "value", str(integration.info.type))
|
|
68
|
+
info_type = f" [{info_type.lower()}]"
|
|
69
|
+
lines.append(f"• **{name}** (ID: {int_id}){info_type}")
|
|
70
|
+
|
|
71
|
+
lines.append("")
|
|
72
|
+
lines.append(
|
|
73
|
+
"**Hint:** Use `integration_id` or `integration_name` when creating/updating "
|
|
74
|
+
"test cases with issues to specify which integration to use."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return "\n".join(lines)
|