alita-sdk 0.3.376__py3-none-any.whl → 0.3.435__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.
Potentially problematic release.
This version of alita-sdk might be problematic. Click here for more details.
- alita_sdk/configurations/bitbucket.py +95 -0
- alita_sdk/configurations/confluence.py +96 -1
- alita_sdk/configurations/gitlab.py +79 -0
- alita_sdk/configurations/jira.py +103 -0
- alita_sdk/configurations/testrail.py +88 -0
- alita_sdk/configurations/xray.py +93 -0
- alita_sdk/configurations/zephyr_enterprise.py +93 -0
- alita_sdk/configurations/zephyr_essential.py +75 -0
- alita_sdk/runtime/clients/client.py +9 -4
- alita_sdk/runtime/clients/mcp_discovery.py +342 -0
- alita_sdk/runtime/clients/mcp_manager.py +262 -0
- alita_sdk/runtime/clients/sandbox_client.py +8 -0
- alita_sdk/runtime/langchain/assistant.py +41 -38
- alita_sdk/runtime/langchain/constants.py +5 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +4 -1
- alita_sdk/runtime/langchain/document_loaders/constants.py +28 -12
- alita_sdk/runtime/langchain/langraph_agent.py +91 -27
- alita_sdk/runtime/langchain/utils.py +24 -4
- alita_sdk/runtime/models/mcp_models.py +57 -0
- alita_sdk/runtime/toolkits/__init__.py +24 -0
- alita_sdk/runtime/toolkits/application.py +8 -1
- alita_sdk/runtime/toolkits/mcp.py +787 -0
- alita_sdk/runtime/toolkits/tools.py +98 -50
- alita_sdk/runtime/tools/__init__.py +7 -2
- alita_sdk/runtime/tools/application.py +7 -0
- alita_sdk/runtime/tools/function.py +20 -28
- alita_sdk/runtime/tools/graph.py +10 -4
- alita_sdk/runtime/tools/image_generation.py +104 -8
- alita_sdk/runtime/tools/llm.py +146 -114
- alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
- alita_sdk/runtime/tools/mcp_server_tool.py +79 -10
- alita_sdk/runtime/tools/sandbox.py +166 -63
- alita_sdk/runtime/tools/vectorstore.py +3 -2
- alita_sdk/runtime/tools/vectorstore_base.py +4 -3
- alita_sdk/runtime/utils/streamlit.py +34 -3
- alita_sdk/runtime/utils/toolkit_utils.py +5 -2
- alita_sdk/runtime/utils/utils.py +1 -0
- alita_sdk/tools/__init__.py +48 -31
- alita_sdk/tools/ado/work_item/ado_wrapper.py +17 -8
- alita_sdk/tools/base_indexer_toolkit.py +75 -66
- alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
- alita_sdk/tools/code_indexer_toolkit.py +13 -3
- alita_sdk/tools/confluence/api_wrapper.py +29 -7
- alita_sdk/tools/confluence/loader.py +10 -0
- alita_sdk/tools/elitea_base.py +7 -7
- alita_sdk/tools/gitlab/api_wrapper.py +11 -7
- alita_sdk/tools/jira/api_wrapper.py +1 -1
- alita_sdk/tools/openapi/__init__.py +10 -1
- alita_sdk/tools/qtest/api_wrapper.py +522 -74
- alita_sdk/tools/sharepoint/api_wrapper.py +104 -33
- alita_sdk/tools/sharepoint/authorization_helper.py +175 -1
- alita_sdk/tools/sharepoint/utils.py +8 -2
- alita_sdk/tools/utils/content_parser.py +27 -16
- alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +19 -6
- {alita_sdk-0.3.376.dist-info → alita_sdk-0.3.435.dist-info}/METADATA +1 -1
- {alita_sdk-0.3.376.dist-info → alita_sdk-0.3.435.dist-info}/RECORD +60 -55
- {alita_sdk-0.3.376.dist-info → alita_sdk-0.3.435.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.376.dist-info → alita_sdk-0.3.435.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.376.dist-info → alita_sdk-0.3.435.dist-info}/top_level.txt +0 -0
|
@@ -9,7 +9,7 @@ import swagger_client
|
|
|
9
9
|
from langchain_core.tools import ToolException
|
|
10
10
|
from pydantic import Field, PrivateAttr, model_validator, create_model, SecretStr
|
|
11
11
|
from sklearn.feature_extraction.text import strip_tags
|
|
12
|
-
from swagger_client import TestCaseApi, SearchApi, PropertyResource, ModuleApi
|
|
12
|
+
from swagger_client import TestCaseApi, SearchApi, PropertyResource, ModuleApi, ProjectApi, FieldApi
|
|
13
13
|
from swagger_client.rest import ApiException
|
|
14
14
|
|
|
15
15
|
from ..elitea_base import BaseToolApiWrapper
|
|
@@ -26,21 +26,29 @@ If generated data was used, put appropriate note to the test case description fi
|
|
|
26
26
|
### CRITERIA
|
|
27
27
|
1. The structure should be as in EXAMPLE.
|
|
28
28
|
2. Case and spaces for field names should be exactly the same as in NOTES.
|
|
29
|
-
3. Extra fields are allowed.
|
|
29
|
+
3. Extra fields are allowed and will be mapped to project's custom fields if they exist.
|
|
30
30
|
4. "{QTEST_ID}" is required to update, change or replace values in test case.
|
|
31
31
|
5. Do not provide "Id" and "{QTEST_ID}" to create test case.
|
|
32
|
-
6
|
|
32
|
+
6. "Steps" is a list of test step objects with fields "Test Step Number", "Test Step Description", "Test Step Expected Result".
|
|
33
|
+
7. For updates, provide ONLY the fields you want to change. Omitted fields will remain unchanged.
|
|
33
34
|
|
|
34
35
|
### NOTES
|
|
35
|
-
Id: Unique identifier (e.g., TC-123).
|
|
36
|
-
QTest id: Unique identifier (e.g., 4626964).
|
|
37
|
-
Name: Brief title.
|
|
38
|
-
Description: Short purpose.
|
|
39
|
-
Type: 'Manual'
|
|
40
|
-
Status:
|
|
41
|
-
Priority:
|
|
42
|
-
Test Type:
|
|
43
|
-
Precondition:
|
|
36
|
+
Id: Unique identifier (e.g., TC-123). Read-only.
|
|
37
|
+
QTest id: Unique identifier (e.g., 4626964). Required for updates.
|
|
38
|
+
Name: Brief title of the test case.
|
|
39
|
+
Description: Short description of the test purpose.
|
|
40
|
+
Type: Type of test (e.g., 'Manual', 'Automation - UTAF').
|
|
41
|
+
Status: Current status (e.g., 'New', 'In Progress', 'Completed').
|
|
42
|
+
Priority: Priority level (e.g., 'High', 'Medium', 'Low').
|
|
43
|
+
Test Type: Category of test (e.g., 'Functional', 'Regression', 'Smoke').
|
|
44
|
+
Precondition: Prerequisites for the test, formatted as: <Step1> <Step2> Leave blank if none.
|
|
45
|
+
Steps: Array of test steps with Description and Expected Result.
|
|
46
|
+
|
|
47
|
+
**Multi-select fields**: For fields that allow multiple values (e.g., Team, Assigned To etc.), you can provide:
|
|
48
|
+
- Single value: "Team": "Epam"
|
|
49
|
+
- Multiple values: "Team": ["Epam", "EJ"]
|
|
50
|
+
|
|
51
|
+
**For Updates**: Include only the fields you want to modify. The system will validate property values against project configuration.
|
|
44
52
|
|
|
45
53
|
### EXAMPLE
|
|
46
54
|
{{
|
|
@@ -53,6 +61,7 @@ Precondition: List prerequisites in one cell, formatted as: <Step1> <Step2> Leav
|
|
|
53
61
|
"Priority": "",
|
|
54
62
|
"Test Type": "Functional",
|
|
55
63
|
"Precondition": "<ONLY provided by user precondition>",
|
|
64
|
+
"Team": ["Epam", "EJ"],
|
|
56
65
|
"Steps": [
|
|
57
66
|
{{ "Test Step Number": 1, "Test Step Description": "Navigate to url", "Test Step Expected Result": "Page content is loaded"}},
|
|
58
67
|
{{ "Test Step Number": 2, "Test Step Description": "Click 'Login'", "Test Step Expected Result": "Form is expanded"}},
|
|
@@ -82,8 +91,16 @@ QtestCreateTestCase = create_model(
|
|
|
82
91
|
|
|
83
92
|
QtestLinkTestCaseToJiraRequirement = create_model(
|
|
84
93
|
"QtestLinkTestCaseToJiraRequirement",
|
|
85
|
-
requirement_external_id=(str, Field("Qtest requirement external id which represent jira issue id linked to Qtest as a requirement e.g. SITEPOD-4038")),
|
|
86
|
-
json_list_of_test_case_ids=(str, Field("""List of the test case ids to be linked to particular requirement.
|
|
94
|
+
requirement_external_id=(str, Field(description="Qtest requirement external id which represent jira issue id linked to Qtest as a requirement e.g. SITEPOD-4038")),
|
|
95
|
+
json_list_of_test_case_ids=(str, Field(description="""List of the test case ids to be linked to particular requirement.
|
|
96
|
+
Create a list of the test case ids in the following format '["TC-123", "TC-234", "TC-456"]' which represents json array as a string.
|
|
97
|
+
It should be capable to be extracted directly by python json.loads method."""))
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
QtestLinkTestCaseToQtestRequirement = create_model(
|
|
101
|
+
"QtestLinkTestCaseToQtestRequirement",
|
|
102
|
+
requirement_id=(str, Field(description="QTest internal requirement ID in format RQ-123")),
|
|
103
|
+
json_list_of_test_case_ids=(str, Field(description="""List of the test case ids to be linked to particular requirement.
|
|
87
104
|
Create a list of the test case ids in the following format '["TC-123", "TC-234", "TC-456"]' which represents json array as a string.
|
|
88
105
|
It should be capable to be extracted directly by python json.loads method."""))
|
|
89
106
|
)
|
|
@@ -118,6 +135,17 @@ GetModules = create_model(
|
|
|
118
135
|
|
|
119
136
|
)
|
|
120
137
|
|
|
138
|
+
GetAllTestCasesFieldsForProject = create_model(
|
|
139
|
+
"GetAllTestCasesFieldsForProject",
|
|
140
|
+
force_refresh=(Optional[bool],
|
|
141
|
+
Field(description="Set to true to reload field definitions from API if project configuration has changed (new fields added, dropdown values modified). Default: false (uses cached data).",
|
|
142
|
+
default=False)),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
NoInput = create_model(
|
|
146
|
+
"NoInput"
|
|
147
|
+
)
|
|
148
|
+
|
|
121
149
|
class QtestApiWrapper(BaseToolApiWrapper):
|
|
122
150
|
base_url: str
|
|
123
151
|
qtest_project_id: int
|
|
@@ -126,6 +154,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
126
154
|
page: int = 1
|
|
127
155
|
no_of_tests_shown_in_dql_search: int = 10
|
|
128
156
|
_client: Any = PrivateAttr()
|
|
157
|
+
_field_definitions_cache: Optional[dict] = PrivateAttr(default=None)
|
|
129
158
|
llm: Any
|
|
130
159
|
|
|
131
160
|
@model_validator(mode='before')
|
|
@@ -135,9 +164,8 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
135
164
|
values['qtest_project_id'] = values.pop('project_id')
|
|
136
165
|
return values
|
|
137
166
|
|
|
138
|
-
@model_validator(mode='
|
|
139
|
-
|
|
140
|
-
def validate_toolkit(cls, values):
|
|
167
|
+
@model_validator(mode='after')
|
|
168
|
+
def validate_toolkit(self):
|
|
141
169
|
try:
|
|
142
170
|
import swagger_client # noqa: F401
|
|
143
171
|
except ImportError:
|
|
@@ -146,15 +174,13 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
146
174
|
"`pip install git+https://github.com/Roman-Mitusov/qtest-api.git`"
|
|
147
175
|
)
|
|
148
176
|
|
|
149
|
-
|
|
150
|
-
api_token = values.get('qtest_api_token')
|
|
151
|
-
if api_token:
|
|
177
|
+
if self.qtest_api_token:
|
|
152
178
|
configuration = swagger_client.Configuration()
|
|
153
|
-
configuration.host =
|
|
154
|
-
configuration.api_key['Authorization'] =
|
|
179
|
+
configuration.host = self.base_url
|
|
180
|
+
configuration.api_key['Authorization'] = self.qtest_api_token.get_secret_value()
|
|
155
181
|
configuration.api_key_prefix['Authorization'] = 'Bearer'
|
|
156
|
-
|
|
157
|
-
return
|
|
182
|
+
self._client = swagger_client.ApiClient(configuration)
|
|
183
|
+
return self
|
|
158
184
|
|
|
159
185
|
def __instantiate_test_api_instance(self) -> TestCaseApi:
|
|
160
186
|
# Instantiate the TestCaseApi instance according to the qtest api documentation and swagger client
|
|
@@ -163,33 +189,215 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
163
189
|
def __instantiate_module_api_instance(self) -> ModuleApi:
|
|
164
190
|
return swagger_client.ModuleApi(self._client)
|
|
165
191
|
|
|
192
|
+
def __instantiate_fields_api_instance(self) -> FieldApi:
|
|
193
|
+
return swagger_client.FieldApi(self._client)
|
|
194
|
+
|
|
195
|
+
def __get_field_definitions_cached(self) -> dict:
|
|
196
|
+
"""Get field definitions with session-level caching.
|
|
197
|
+
|
|
198
|
+
Field definitions are cached for the lifetime of this wrapper instance.
|
|
199
|
+
If project field configuration changes, call refresh_field_definitions_cache()
|
|
200
|
+
to reload the definitions.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
dict: Field definitions mapping
|
|
204
|
+
"""
|
|
205
|
+
if self._field_definitions_cache is None:
|
|
206
|
+
self._field_definitions_cache = self.__get_project_field_definitions()
|
|
207
|
+
return self._field_definitions_cache
|
|
208
|
+
|
|
209
|
+
def refresh_field_definitions_cache(self) -> dict:
|
|
210
|
+
"""Manually refresh the field definitions cache.
|
|
211
|
+
|
|
212
|
+
Call this method if project field configuration has been updated
|
|
213
|
+
(new fields added, dropdown values changed, etc.) and you need to
|
|
214
|
+
reload the definitions within the same agent session.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
dict: Freshly loaded field definitions
|
|
218
|
+
"""
|
|
219
|
+
self._field_definitions_cache = None
|
|
220
|
+
return self.__get_field_definitions_cached()
|
|
221
|
+
|
|
222
|
+
def __map_properties_to_api_format(self, test_case_data: dict, field_definitions: dict,
|
|
223
|
+
base_properties: list = None) -> list:
|
|
224
|
+
"""
|
|
225
|
+
Convert user-friendly property names/values to QTest API PropertyResource format.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
test_case_data: Dict with property names as keys (e.g., {"Status": "New", "Priority": "High"})
|
|
229
|
+
field_definitions: Output from __get_project_field_definitions()
|
|
230
|
+
base_properties: Existing properties from a test case (for updates, optional)
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
list[PropertyResource]: Properties ready for API submission
|
|
234
|
+
|
|
235
|
+
Raises:
|
|
236
|
+
ValueError: If any field names are unknown or values are invalid (shows ALL errors)
|
|
237
|
+
"""
|
|
238
|
+
# Start with base properties or empty dict
|
|
239
|
+
props_dict = {}
|
|
240
|
+
if base_properties:
|
|
241
|
+
for prop in base_properties:
|
|
242
|
+
field_name = prop.get('field_name')
|
|
243
|
+
if field_name:
|
|
244
|
+
props_dict[field_name] = {
|
|
245
|
+
'field_id': prop['field_id'],
|
|
246
|
+
'field_name': field_name,
|
|
247
|
+
'field_value': prop['field_value'],
|
|
248
|
+
'field_value_name': prop.get('field_value_name')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# Collect ALL validation errors before raising
|
|
252
|
+
validation_errors = []
|
|
253
|
+
|
|
254
|
+
# Map incoming properties from test_case_data
|
|
255
|
+
for field_name, field_value in test_case_data.items():
|
|
256
|
+
# Skip non-property fields (these are handled separately)
|
|
257
|
+
if field_name in ['Name', 'Description', 'Precondition', 'Steps', 'Id', QTEST_ID]:
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
# Skip None or empty string values (don't update these fields)
|
|
261
|
+
if field_value is None or field_value == '':
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
# Validate field exists in project - STRICT validation
|
|
265
|
+
if field_name not in field_definitions:
|
|
266
|
+
validation_errors.append(
|
|
267
|
+
f"❌ Unknown field '{field_name}' - not defined in project configuration"
|
|
268
|
+
)
|
|
269
|
+
continue # Skip to next field, keep collecting errors
|
|
270
|
+
|
|
271
|
+
field_def = field_definitions[field_name]
|
|
272
|
+
field_id = field_def['field_id']
|
|
273
|
+
data_type = field_def.get('data_type')
|
|
274
|
+
is_multiple = field_def.get('multiple', False)
|
|
275
|
+
|
|
276
|
+
# Normalize field_value to list for consistent processing
|
|
277
|
+
# Multi-select fields can receive: "value", ["value1", "value2"], or ["value1"]
|
|
278
|
+
# Single-select fields: "value" only
|
|
279
|
+
if is_multiple:
|
|
280
|
+
# Convert to list if not already
|
|
281
|
+
values_to_process = field_value if isinstance(field_value, list) else [field_value]
|
|
282
|
+
else:
|
|
283
|
+
# Single-select: keep as single value
|
|
284
|
+
values_to_process = [field_value]
|
|
285
|
+
|
|
286
|
+
# Validate value(s) for dropdown fields (only if field has allowed values)
|
|
287
|
+
if field_def['values']:
|
|
288
|
+
# Field has allowed values (dropdown/combobox/user fields) - validate strictly
|
|
289
|
+
value_ids = []
|
|
290
|
+
value_names = []
|
|
291
|
+
|
|
292
|
+
for single_value in values_to_process:
|
|
293
|
+
if single_value not in field_def['values']:
|
|
294
|
+
available = ", ".join(sorted(field_def['values'].keys()))
|
|
295
|
+
validation_errors.append(
|
|
296
|
+
f"❌ Invalid value '{single_value}' for field '{field_name}'. "
|
|
297
|
+
f"Allowed values: {available}"
|
|
298
|
+
)
|
|
299
|
+
continue # Skip this value, but continue validating others
|
|
300
|
+
|
|
301
|
+
# Valid value - add to lists
|
|
302
|
+
value_ids.append(field_def['values'][single_value])
|
|
303
|
+
value_names.append(single_value)
|
|
304
|
+
|
|
305
|
+
# If all values were invalid, skip this field
|
|
306
|
+
if not value_ids:
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
# Format based on field type and value count
|
|
310
|
+
if is_multiple and len(value_ids) == 1:
|
|
311
|
+
# Single value in multi-select field: bracketed string "[419950]"
|
|
312
|
+
# This includes single user assignment: "[626983]"
|
|
313
|
+
field_value_id = f"[{value_ids[0]}]"
|
|
314
|
+
field_value_name = f"[{value_names[0]}]" if data_type == 5 else value_names[0]
|
|
315
|
+
elif is_multiple:
|
|
316
|
+
# Multiple values in multi-select: bracketed string with comma-separated IDs
|
|
317
|
+
ids_str = ",".join(str(vid) for vid in value_ids)
|
|
318
|
+
field_value_id = f"[{ids_str}]"
|
|
319
|
+
field_value_name = ", ".join(value_names)
|
|
320
|
+
else:
|
|
321
|
+
# Regular single-select dropdown: plain ID
|
|
322
|
+
field_value_id = value_ids[0]
|
|
323
|
+
field_value_name = value_names[0]
|
|
324
|
+
else:
|
|
325
|
+
# Text field or field without restricted values - use value directly
|
|
326
|
+
# No validation needed - users can write anything (by design)
|
|
327
|
+
field_value_id = field_value
|
|
328
|
+
field_value_name = field_value if isinstance(field_value, str) else None
|
|
329
|
+
|
|
330
|
+
# Update or add property (only if no errors for this field)
|
|
331
|
+
props_dict[field_name] = {
|
|
332
|
+
'field_id': field_id,
|
|
333
|
+
'field_name': field_name,
|
|
334
|
+
'field_value': field_value_id,
|
|
335
|
+
'field_value_name': field_value_name
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
# If ANY validation errors found, raise comprehensive error with all issues
|
|
339
|
+
if validation_errors:
|
|
340
|
+
available_fields = ", ".join(sorted(field_definitions.keys()))
|
|
341
|
+
error_msg = (
|
|
342
|
+
f"Found {len(validation_errors)} validation error(s) in test case properties:\n\n" +
|
|
343
|
+
"\n".join(validation_errors) +
|
|
344
|
+
f"\n\n📋 Available fields for this project: {available_fields}\n\n"
|
|
345
|
+
f"💡 Tip: Use 'get_all_test_cases_fields_for_project' tool to see all fields with their allowed values."
|
|
346
|
+
)
|
|
347
|
+
raise ValueError(error_msg)
|
|
348
|
+
|
|
349
|
+
# Convert to PropertyResource list, filtering out special fields
|
|
350
|
+
result = []
|
|
351
|
+
for field_name, prop_data in props_dict.items():
|
|
352
|
+
if field_name in ['Shared', 'Projects Shared to']:
|
|
353
|
+
continue
|
|
354
|
+
result.append(PropertyResource(
|
|
355
|
+
field_id=prop_data['field_id'],
|
|
356
|
+
field_name=prop_data['field_name'],
|
|
357
|
+
field_value=prop_data['field_value'],
|
|
358
|
+
field_value_name=prop_data.get('field_value_name')
|
|
359
|
+
))
|
|
360
|
+
|
|
361
|
+
return result
|
|
362
|
+
|
|
166
363
|
def __build_body_for_create_test_case(self, test_cases_data: list[dict],
|
|
167
364
|
folder_to_place_test_cases_to: str = '') -> list:
|
|
168
|
-
|
|
365
|
+
# Get field definitions for property mapping (cached for session)
|
|
366
|
+
field_definitions = self.__get_field_definitions_cached()
|
|
367
|
+
|
|
169
368
|
modules = self._parse_modules()
|
|
170
369
|
parent_id = ''.join(str(module['module_id']) for module in modules if
|
|
171
370
|
folder_to_place_test_cases_to and module['full_module_name'] == folder_to_place_test_cases_to)
|
|
172
|
-
|
|
173
|
-
for prop in initial_project_properties:
|
|
174
|
-
if prop.get('field_name', '') == 'Shared' or prop.get('field_name', '') == 'Projects Shared to':
|
|
175
|
-
continue
|
|
176
|
-
props.append(PropertyResource(field_id=prop['field_id'], field_name=prop['field_name'],
|
|
177
|
-
field_value_name=prop.get('field_value_name', None),
|
|
178
|
-
field_value=prop['field_value']))
|
|
371
|
+
|
|
179
372
|
bodies = []
|
|
180
373
|
for test_case in test_cases_data:
|
|
374
|
+
# Map properties from user format to API format
|
|
375
|
+
props = self.__map_properties_to_api_format(test_case, field_definitions)
|
|
376
|
+
|
|
181
377
|
body = swagger_client.TestCaseWithCustomFieldResource(properties=props)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
378
|
+
|
|
379
|
+
# Only set fields if they are explicitly provided in the input
|
|
380
|
+
# This prevents overwriting existing values with None during partial updates
|
|
381
|
+
if 'Name' in test_case:
|
|
382
|
+
body.name = test_case['Name']
|
|
383
|
+
if 'Precondition' in test_case:
|
|
384
|
+
body.precondition = test_case['Precondition']
|
|
385
|
+
if 'Description' in test_case:
|
|
386
|
+
body.description = test_case['Description']
|
|
387
|
+
|
|
185
388
|
if parent_id:
|
|
186
389
|
body.parent_id = parent_id
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
390
|
+
|
|
391
|
+
# Only set test_steps if Steps are provided in the input
|
|
392
|
+
# This prevents overwriting existing steps during partial updates
|
|
393
|
+
if 'Steps' in test_case and test_case['Steps'] is not None:
|
|
394
|
+
test_steps_resources = []
|
|
395
|
+
for step in test_case['Steps']:
|
|
396
|
+
test_steps_resources.append(
|
|
397
|
+
swagger_client.TestStepResource(description=step.get('Test Step Description'),
|
|
398
|
+
expected=step.get('Test Step Expected Result')))
|
|
399
|
+
body.test_steps = test_steps_resources
|
|
400
|
+
|
|
193
401
|
bodies.append(body)
|
|
194
402
|
return bodies
|
|
195
403
|
|
|
@@ -206,6 +414,108 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
206
414
|
Exception: \n {stacktrace}""")
|
|
207
415
|
return modules
|
|
208
416
|
|
|
417
|
+
def __get_project_field_definitions(self) -> dict:
|
|
418
|
+
"""
|
|
419
|
+
Get structured field definitions for test cases in the project.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
dict: Mapping of field names to their IDs and allowed values.
|
|
423
|
+
Example: {
|
|
424
|
+
'Status': {
|
|
425
|
+
'field_id': 12345,
|
|
426
|
+
'required': True,
|
|
427
|
+
'values': {'New': 1, 'In Progress': 2, 'Completed': 3}
|
|
428
|
+
},
|
|
429
|
+
'Priority': {
|
|
430
|
+
'field_id': 12346,
|
|
431
|
+
'required': False,
|
|
432
|
+
'values': {'High': 1, 'Medium': 2, 'Low': 3}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
"""
|
|
436
|
+
fields_api = self.__instantiate_fields_api_instance()
|
|
437
|
+
qtest_object = 'test-cases'
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
fields = fields_api.get_fields(self.qtest_project_id, qtest_object)
|
|
441
|
+
except ApiException as e:
|
|
442
|
+
stacktrace = format_exc()
|
|
443
|
+
logger.error(f"Exception when calling FieldAPI->get_fields:\n {stacktrace}")
|
|
444
|
+
raise ValueError(
|
|
445
|
+
f"Unable to get test case fields for project {self.qtest_project_id}. Exception: \n {stacktrace}")
|
|
446
|
+
|
|
447
|
+
# Build structured mapping
|
|
448
|
+
field_mapping = {}
|
|
449
|
+
for field in fields:
|
|
450
|
+
field_name = field.label
|
|
451
|
+
field_mapping[field_name] = {
|
|
452
|
+
'field_id': field.id,
|
|
453
|
+
'required': getattr(field, 'required', False),
|
|
454
|
+
'data_type': getattr(field, 'data_type', None), # 5 = user field
|
|
455
|
+
'multiple': getattr(field, 'multiple', False), # True = multi-select, needs array format
|
|
456
|
+
'values': {}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
# Map allowed values if field has them (dropdown/combobox/user fields)
|
|
460
|
+
# Only include active values (is_active=True)
|
|
461
|
+
if hasattr(field, 'allowed_values') and field.allowed_values:
|
|
462
|
+
for allowed_value in field.allowed_values:
|
|
463
|
+
# Skip inactive values (deleted/deprecated options)
|
|
464
|
+
if hasattr(allowed_value, 'is_active') and not allowed_value.is_active:
|
|
465
|
+
continue
|
|
466
|
+
|
|
467
|
+
# AllowedValueResource has 'label' for the display name and 'value' for the ID
|
|
468
|
+
# Note: 'value' is the field_value, not 'id'
|
|
469
|
+
# For user fields (data_type=5), label is user name and value is user ID
|
|
470
|
+
value_label = allowed_value.label
|
|
471
|
+
value_id = allowed_value.value
|
|
472
|
+
field_mapping[field_name]['values'][value_label] = value_id
|
|
473
|
+
|
|
474
|
+
return field_mapping
|
|
475
|
+
|
|
476
|
+
def __format_field_info_for_display(self, field_definitions: dict) -> str:
|
|
477
|
+
"""
|
|
478
|
+
Format field definitions in human-readable format for LLM.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
field_definitions: Output from __get_project_field_definitions()
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
Formatted string with field information
|
|
485
|
+
"""
|
|
486
|
+
output = [f"Available Test Case Fields for Project {self.qtest_project_id}:\n"]
|
|
487
|
+
|
|
488
|
+
for field_name, field_info in sorted(field_definitions.items()):
|
|
489
|
+
required_marker = " (Required)" if field_info.get('required') else ""
|
|
490
|
+
output.append(f"\n{field_name}{required_marker}:")
|
|
491
|
+
|
|
492
|
+
if field_info.get('values'):
|
|
493
|
+
for value_name, value_id in sorted(field_info['values'].items()):
|
|
494
|
+
output.append(f" - {value_name} (id: {value_id})")
|
|
495
|
+
else:
|
|
496
|
+
output.append(" Type: text")
|
|
497
|
+
|
|
498
|
+
output.append("\n\nUse these exact value names when creating or updating test cases.")
|
|
499
|
+
return ''.join(output)
|
|
500
|
+
|
|
501
|
+
def get_all_test_cases_fields_for_project(self, force_refresh: bool = False) -> str:
|
|
502
|
+
"""
|
|
503
|
+
Get formatted information about available test case fields and their values.
|
|
504
|
+
This method is exposed as a tool for LLM to query field information.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
force_refresh: If True, reload field definitions from API instead of using cache.
|
|
508
|
+
Use this if project configuration has changed (new fields added,
|
|
509
|
+
dropdown values modified, etc.).
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
Formatted string with field names and allowed values
|
|
513
|
+
"""
|
|
514
|
+
if force_refresh:
|
|
515
|
+
self.refresh_field_definitions_cache()
|
|
516
|
+
field_defs = self.__get_field_definitions_cached()
|
|
517
|
+
return self.__format_field_info_for_display(field_defs)
|
|
518
|
+
|
|
209
519
|
def _parse_modules(self) -> list[dict]:
|
|
210
520
|
modules = self.__get_all_modules_for_project()
|
|
211
521
|
result = []
|
|
@@ -245,27 +555,49 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
245
555
|
|
|
246
556
|
def __parse_data(self, response_to_parse: dict, parsed_data: list, extract_images: bool=False, prompt: str=None):
|
|
247
557
|
import html
|
|
558
|
+
|
|
559
|
+
# Get field definitions to ensure all fields are included (uses cached version)
|
|
560
|
+
field_definitions = self.__get_field_definitions_cached()
|
|
561
|
+
|
|
248
562
|
for item in response_to_parse['items']:
|
|
563
|
+
# Start with core fields (always present)
|
|
249
564
|
parsed_data_row = {
|
|
250
565
|
'Id': item['pid'],
|
|
566
|
+
'Name': item['name'],
|
|
251
567
|
'Description': html.unescape(strip_tags(item['description'])),
|
|
252
568
|
'Precondition': html.unescape(strip_tags(item['precondition'])),
|
|
253
|
-
'Name': item['name'],
|
|
254
569
|
QTEST_ID: item['id'],
|
|
255
570
|
'Steps': list(map(lambda step: {
|
|
256
571
|
'Test Step Number': step[0] + 1,
|
|
257
572
|
'Test Step Description': self._process_image(step[1]['description'], extract_images, prompt),
|
|
258
573
|
'Test Step Expected Result': self._process_image(step[1]['expected'], extract_images, prompt)
|
|
259
574
|
}, enumerate(item['test_steps']))),
|
|
260
|
-
'Status': ''.join([properties['field_value_name'] for properties in item['properties']
|
|
261
|
-
if properties['field_name'] == 'Status']),
|
|
262
|
-
'Automation': ''.join([properties['field_value_name'] for properties in item['properties']
|
|
263
|
-
if properties['field_name'] == 'Automation']),
|
|
264
|
-
'Type': ''.join([properties['field_value_name'] for properties in item['properties']
|
|
265
|
-
if properties['field_name'] == 'Type']),
|
|
266
|
-
'Priority': ''.join([properties['field_value_name'] for properties in item['properties']
|
|
267
|
-
if properties['field_name'] == 'Priority']),
|
|
268
575
|
}
|
|
576
|
+
|
|
577
|
+
# Dynamically add all custom fields from project configuration
|
|
578
|
+
# This ensures consistency and includes fields even if they have null/empty values
|
|
579
|
+
for field_name in field_definitions.keys():
|
|
580
|
+
field_def = field_definitions[field_name]
|
|
581
|
+
is_multiple = field_def.get('multiple', False)
|
|
582
|
+
|
|
583
|
+
# Find the property value in the response (if exists)
|
|
584
|
+
field_value = None
|
|
585
|
+
for prop in item['properties']:
|
|
586
|
+
if prop['field_name'] == field_name:
|
|
587
|
+
# Use field_value_name if available (for dropdowns), otherwise field_value
|
|
588
|
+
field_value = prop.get('field_value_name') or prop.get('field_value') or ''
|
|
589
|
+
break
|
|
590
|
+
|
|
591
|
+
# Format based on field type
|
|
592
|
+
if is_multiple and (field_value is None or field_value == ''):
|
|
593
|
+
# Multi-select field with no value: show empty array with hint
|
|
594
|
+
parsed_data_row[field_name] = '[] (multi-select)'
|
|
595
|
+
elif field_value is not None:
|
|
596
|
+
parsed_data_row[field_name] = field_value
|
|
597
|
+
else:
|
|
598
|
+
# Regular field with no value
|
|
599
|
+
parsed_data_row[field_name] = ''
|
|
600
|
+
|
|
269
601
|
parsed_data.append(parsed_data_row)
|
|
270
602
|
|
|
271
603
|
def _process_image(self, content: str, extract: bool=False, prompt: str=None):
|
|
@@ -323,18 +655,39 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
323
655
|
parsed_data = self.__perform_search_by_dql(dql)
|
|
324
656
|
return parsed_data[0]['QTest Id']
|
|
325
657
|
|
|
326
|
-
def
|
|
327
|
-
|
|
328
|
-
|
|
658
|
+
def __find_qtest_requirement_id_by_id(self, requirement_id: str) -> int:
|
|
659
|
+
"""Search for requirement's internal QTest ID using requirement ID (RQ-xxx format).
|
|
660
|
+
|
|
661
|
+
Args:
|
|
662
|
+
requirement_id: Requirement ID in format RQ-123
|
|
663
|
+
|
|
664
|
+
Returns:
|
|
665
|
+
int: Internal QTest ID for the requirement
|
|
666
|
+
|
|
667
|
+
Raises:
|
|
668
|
+
ValueError: If requirement is not found
|
|
669
|
+
"""
|
|
670
|
+
dql = f"Id = '{requirement_id}'"
|
|
671
|
+
search_instance: SearchApi = swagger_client.SearchApi(self._client)
|
|
672
|
+
body = swagger_client.ArtifactSearchParams(object_type='requirements', fields=['*'], query=dql)
|
|
673
|
+
|
|
329
674
|
try:
|
|
330
|
-
response =
|
|
331
|
-
|
|
675
|
+
response = search_instance.search_artifact(self.qtest_project_id, body)
|
|
676
|
+
if response['total'] == 0:
|
|
677
|
+
raise ValueError(
|
|
678
|
+
f"Requirement '{requirement_id}' not found in project {self.qtest_project_id}. "
|
|
679
|
+
f"Please verify the requirement ID exists."
|
|
680
|
+
)
|
|
681
|
+
return response['items'][0]['id']
|
|
332
682
|
except ApiException as e:
|
|
333
683
|
stacktrace = format_exc()
|
|
334
|
-
logger.error(f"Exception when
|
|
335
|
-
raise
|
|
684
|
+
logger.error(f"Exception when searching for requirement: \n {stacktrace}")
|
|
685
|
+
raise ToolException(
|
|
686
|
+
f"Unable to search for requirement '{requirement_id}' in project {self.qtest_project_id}. "
|
|
687
|
+
f"Exception: \n{stacktrace}"
|
|
688
|
+
) from e
|
|
336
689
|
|
|
337
|
-
def __is_jira_requirement_present(self, jira_issue_id: str) ->
|
|
690
|
+
def __is_jira_requirement_present(self, jira_issue_id: str) -> tuple[bool, dict]:
|
|
338
691
|
""" Define if particular Jira requirement is present in qtest or not """
|
|
339
692
|
dql = f"'External Id' = '{jira_issue_id}'"
|
|
340
693
|
search_instance: SearchApi = swagger_client.SearchApi(self._client)
|
|
@@ -350,31 +703,112 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
350
703
|
logger.error(f"Error: {format_exc()}")
|
|
351
704
|
raise e
|
|
352
705
|
|
|
353
|
-
def _get_jira_requirement_id(self, jira_issue_id: str) -> int
|
|
354
|
-
"""
|
|
706
|
+
def _get_jira_requirement_id(self, jira_issue_id: str) -> int:
|
|
707
|
+
"""Search for requirement id using the linked jira_issue_id.
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
jira_issue_id: External Jira issue ID (e.g., PLAN-128)
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
int: Internal QTest ID for the Jira requirement
|
|
714
|
+
|
|
715
|
+
Raises:
|
|
716
|
+
ValueError: If Jira requirement is not found in QTest
|
|
717
|
+
"""
|
|
355
718
|
is_present, response = self.__is_jira_requirement_present(jira_issue_id)
|
|
356
719
|
if not is_present:
|
|
357
|
-
|
|
720
|
+
raise ValueError(
|
|
721
|
+
f"Jira requirement '{jira_issue_id}' not found in QTest project {self.qtest_project_id}. "
|
|
722
|
+
f"Please ensure the Jira issue is linked to QTest as a requirement."
|
|
723
|
+
)
|
|
358
724
|
return response['items'][0]['id']
|
|
359
725
|
|
|
360
726
|
|
|
361
727
|
def link_tests_to_jira_requirement(self, requirement_external_id: str, json_list_of_test_case_ids: str) -> str:
|
|
362
|
-
"""
|
|
728
|
+
"""Link test cases to external Jira requirement.
|
|
729
|
+
|
|
730
|
+
Args:
|
|
731
|
+
requirement_external_id: Jira issue ID (e.g., PLAN-128)
|
|
732
|
+
json_list_of_test_case_ids: JSON array string of test case IDs (e.g., '["TC-123", "TC-234"]')
|
|
733
|
+
|
|
734
|
+
Returns:
|
|
735
|
+
Success message with linked test case IDs
|
|
736
|
+
"""
|
|
363
737
|
link_object_api_instance = swagger_client.ObjectLinkApi(self._client)
|
|
364
738
|
source_type = "requirements"
|
|
365
739
|
linked_type = "test-cases"
|
|
366
|
-
|
|
740
|
+
test_case_ids = json.loads(json_list_of_test_case_ids)
|
|
741
|
+
qtest_test_case_ids = [self.__find_qtest_id_by_test_id(tc_id) for tc_id in test_case_ids]
|
|
367
742
|
requirement_id = self._get_jira_requirement_id(requirement_external_id)
|
|
368
743
|
|
|
369
744
|
try:
|
|
370
|
-
response = link_object_api_instance.link_artifacts(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
745
|
+
response = link_object_api_instance.link_artifacts(
|
|
746
|
+
self.qtest_project_id,
|
|
747
|
+
object_id=requirement_id,
|
|
748
|
+
type=linked_type,
|
|
749
|
+
object_type=source_type,
|
|
750
|
+
body=qtest_test_case_ids
|
|
751
|
+
)
|
|
752
|
+
linked_test_cases = [link.pid for link in response[0].objects]
|
|
753
|
+
return (
|
|
754
|
+
f"Successfully linked {len(linked_test_cases)} test case(s) to Jira requirement '{requirement_external_id}' "
|
|
755
|
+
f"in project {self.qtest_project_id}.\n"
|
|
756
|
+
f"Linked test cases: {', '.join(linked_test_cases)}"
|
|
757
|
+
)
|
|
758
|
+
except ApiException as e:
|
|
759
|
+
stacktrace = format_exc()
|
|
760
|
+
logger.error(f"Error linking to Jira requirement: {stacktrace}")
|
|
761
|
+
raise ToolException(
|
|
762
|
+
f"Unable to link test cases to Jira requirement '{requirement_external_id}' "
|
|
763
|
+
f"in project {self.qtest_project_id}. Exception: \n{stacktrace}"
|
|
764
|
+
) from e
|
|
765
|
+
|
|
766
|
+
def link_tests_to_qtest_requirement(self, requirement_id: str, json_list_of_test_case_ids: str) -> str:
|
|
767
|
+
"""Link test cases to internal QTest requirement.
|
|
768
|
+
|
|
769
|
+
Args:
|
|
770
|
+
requirement_id: QTest requirement ID in format RQ-123
|
|
771
|
+
json_list_of_test_case_ids: JSON array string of test case IDs (e.g., '["TC-123", "TC-234"]')
|
|
772
|
+
|
|
773
|
+
Returns:
|
|
774
|
+
Success message with linked test case IDs
|
|
775
|
+
|
|
776
|
+
Raises:
|
|
777
|
+
ValueError: If requirement or test cases are not found
|
|
778
|
+
ToolException: If linking fails
|
|
779
|
+
"""
|
|
780
|
+
link_object_api_instance = swagger_client.ObjectLinkApi(self._client)
|
|
781
|
+
source_type = "requirements"
|
|
782
|
+
linked_type = "test-cases"
|
|
783
|
+
|
|
784
|
+
# Parse and convert test case IDs
|
|
785
|
+
test_case_ids = json.loads(json_list_of_test_case_ids)
|
|
786
|
+
qtest_test_case_ids = [self.__find_qtest_id_by_test_id(tc_id) for tc_id in test_case_ids]
|
|
787
|
+
|
|
788
|
+
# Get internal QTest ID for the requirement
|
|
789
|
+
qtest_requirement_id = self.__find_qtest_requirement_id_by_id(requirement_id)
|
|
790
|
+
|
|
791
|
+
try:
|
|
792
|
+
response = link_object_api_instance.link_artifacts(
|
|
793
|
+
self.qtest_project_id,
|
|
794
|
+
object_id=qtest_requirement_id,
|
|
795
|
+
type=linked_type,
|
|
796
|
+
object_type=source_type,
|
|
797
|
+
body=qtest_test_case_ids
|
|
798
|
+
)
|
|
799
|
+
linked_test_cases = [link.pid for link in response[0].objects]
|
|
800
|
+
return (
|
|
801
|
+
f"Successfully linked {len(linked_test_cases)} test case(s) to QTest requirement '{requirement_id}' "
|
|
802
|
+
f"in project {self.qtest_project_id}.\n"
|
|
803
|
+
f"Linked test cases: {', '.join(linked_test_cases)}"
|
|
804
|
+
)
|
|
805
|
+
except ApiException as e:
|
|
806
|
+
stacktrace = format_exc()
|
|
807
|
+
logger.error(f"Error linking to QTest requirement: {stacktrace}")
|
|
808
|
+
raise ToolException(
|
|
809
|
+
f"Unable to link test cases to QTest requirement '{requirement_id}' "
|
|
810
|
+
f"in project {self.qtest_project_id}. Exception: \n{stacktrace}"
|
|
811
|
+
) from e
|
|
378
812
|
|
|
379
813
|
def search_by_dql(self, dql: str, extract_images:bool=False, prompt: str=None):
|
|
380
814
|
"""Search for the test cases in qTest using Data Query Language """
|
|
@@ -496,17 +930,31 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
496
930
|
"ref": self.delete_test_case,
|
|
497
931
|
},
|
|
498
932
|
{
|
|
499
|
-
"name": "
|
|
500
|
-
"mode": "
|
|
501
|
-
"description": "
|
|
933
|
+
"name": "link_tests_to_jira_requirement",
|
|
934
|
+
"mode": "link_tests_to_jira_requirement",
|
|
935
|
+
"description": "Link test cases to external Jira requirement. Provide Jira issue ID (e.g., PLAN-128) and list of test case IDs in format '[\"TC-123\", \"TC-234\"]'",
|
|
502
936
|
"args_schema": QtestLinkTestCaseToJiraRequirement,
|
|
503
937
|
"ref": self.link_tests_to_jira_requirement,
|
|
504
938
|
},
|
|
939
|
+
{
|
|
940
|
+
"name": "link_tests_to_qtest_requirement",
|
|
941
|
+
"mode": "link_tests_to_qtest_requirement",
|
|
942
|
+
"description": "Link test cases to internal QTest requirement. Provide QTest requirement ID (e.g., RQ-15) and list of test case IDs in format '[\"TC-123\", \"TC-234\"]'",
|
|
943
|
+
"args_schema": QtestLinkTestCaseToQtestRequirement,
|
|
944
|
+
"ref": self.link_tests_to_qtest_requirement,
|
|
945
|
+
},
|
|
505
946
|
{
|
|
506
947
|
"name": "get_modules",
|
|
507
948
|
"mode": "get_modules",
|
|
508
949
|
"description": self.get_modules.__doc__,
|
|
509
950
|
"args_schema": GetModules,
|
|
510
951
|
"ref": self.get_modules,
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
"name": "get_all_test_cases_fields_for_project",
|
|
955
|
+
"mode": "get_all_test_cases_fields_for_project",
|
|
956
|
+
"description": "Get information about available test case fields and their valid values for the project. Shows which property values are allowed (e.g., Status: 'New', 'In Progress', 'Completed') based on the project configuration. Use force_refresh=true if project configuration has changed.",
|
|
957
|
+
"args_schema": GetAllTestCasesFieldsForProject,
|
|
958
|
+
"ref": self.get_all_test_cases_fields_for_project,
|
|
511
959
|
}
|
|
512
960
|
]
|