alita-sdk 0.3.427__py3-none-any.whl → 0.3.428b2__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/runtime/langchain/langraph_agent.py +1 -1
- alita_sdk/tools/qtest/api_wrapper.py +350 -89
- {alita_sdk-0.3.427.dist-info → alita_sdk-0.3.428b2.dist-info}/METADATA +1 -1
- {alita_sdk-0.3.427.dist-info → alita_sdk-0.3.428b2.dist-info}/RECORD +7 -7
- {alita_sdk-0.3.427.dist-info → alita_sdk-0.3.428b2.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.427.dist-info → alita_sdk-0.3.428b2.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.427.dist-info → alita_sdk-0.3.428b2.dist-info}/top_level.txt +0 -0
|
@@ -249,7 +249,7 @@ class PrinterNode(Runnable):
|
|
|
249
249
|
formatted_output = mapping[PRINTER]
|
|
250
250
|
# add info label to the printer's output
|
|
251
251
|
if formatted_output:
|
|
252
|
-
formatted_output += f"\n\n-----\n*How to proceed?*\n*
|
|
252
|
+
formatted_output += f"\n\n-----\n*How to proceed?*\n* *to resume the pipeline - type anything...*"
|
|
253
253
|
logger.debug(f"Formatted output: {formatted_output}")
|
|
254
254
|
result[PRINTER_NODE_RS] = formatted_output
|
|
255
255
|
return result
|
|
@@ -5,6 +5,7 @@ import re
|
|
|
5
5
|
from traceback import format_exc
|
|
6
6
|
from typing import Any, Optional
|
|
7
7
|
|
|
8
|
+
import requests
|
|
8
9
|
import swagger_client
|
|
9
10
|
from langchain_core.tools import ToolException
|
|
10
11
|
from pydantic import Field, PrivateAttr, model_validator, create_model, SecretStr
|
|
@@ -91,8 +92,16 @@ QtestCreateTestCase = create_model(
|
|
|
91
92
|
|
|
92
93
|
QtestLinkTestCaseToJiraRequirement = create_model(
|
|
93
94
|
"QtestLinkTestCaseToJiraRequirement",
|
|
94
|
-
requirement_external_id=(str, Field("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("""List of the test case ids to be linked to particular requirement.
|
|
95
|
+
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")),
|
|
96
|
+
json_list_of_test_case_ids=(str, Field(description="""List of the test case ids to be linked to particular requirement.
|
|
97
|
+
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.
|
|
98
|
+
It should be capable to be extracted directly by python json.loads method."""))
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
QtestLinkTestCaseToQtestRequirement = create_model(
|
|
102
|
+
"QtestLinkTestCaseToQtestRequirement",
|
|
103
|
+
requirement_id=(str, Field(description="QTest internal requirement ID in format RQ-123")),
|
|
104
|
+
json_list_of_test_case_ids=(str, Field(description="""List of the test case ids to be linked to particular requirement.
|
|
96
105
|
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
106
|
It should be capable to be extracted directly by python json.loads method."""))
|
|
98
107
|
)
|
|
@@ -186,11 +195,11 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
186
195
|
|
|
187
196
|
def __get_field_definitions_cached(self) -> dict:
|
|
188
197
|
"""Get field definitions with session-level caching.
|
|
189
|
-
|
|
198
|
+
|
|
190
199
|
Field definitions are cached for the lifetime of this wrapper instance.
|
|
191
200
|
If project field configuration changes, call refresh_field_definitions_cache()
|
|
192
201
|
to reload the definitions.
|
|
193
|
-
|
|
202
|
+
|
|
194
203
|
Returns:
|
|
195
204
|
dict: Field definitions mapping
|
|
196
205
|
"""
|
|
@@ -200,11 +209,11 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
200
209
|
|
|
201
210
|
def refresh_field_definitions_cache(self) -> dict:
|
|
202
211
|
"""Manually refresh the field definitions cache.
|
|
203
|
-
|
|
212
|
+
|
|
204
213
|
Call this method if project field configuration has been updated
|
|
205
214
|
(new fields added, dropdown values changed, etc.) and you need to
|
|
206
215
|
reload the definitions within the same agent session.
|
|
207
|
-
|
|
216
|
+
|
|
208
217
|
Returns:
|
|
209
218
|
dict: Freshly loaded field definitions
|
|
210
219
|
"""
|
|
@@ -215,15 +224,15 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
215
224
|
base_properties: list = None) -> list:
|
|
216
225
|
"""
|
|
217
226
|
Convert user-friendly property names/values to QTest API PropertyResource format.
|
|
218
|
-
|
|
227
|
+
|
|
219
228
|
Args:
|
|
220
229
|
test_case_data: Dict with property names as keys (e.g., {"Status": "New", "Priority": "High"})
|
|
221
230
|
field_definitions: Output from __get_project_field_definitions()
|
|
222
231
|
base_properties: Existing properties from a test case (for updates, optional)
|
|
223
|
-
|
|
232
|
+
|
|
224
233
|
Returns:
|
|
225
234
|
list[PropertyResource]: Properties ready for API submission
|
|
226
|
-
|
|
235
|
+
|
|
227
236
|
Raises:
|
|
228
237
|
ValueError: If any field names are unknown or values are invalid (shows ALL errors)
|
|
229
238
|
"""
|
|
@@ -239,32 +248,32 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
239
248
|
'field_value': prop['field_value'],
|
|
240
249
|
'field_value_name': prop.get('field_value_name')
|
|
241
250
|
}
|
|
242
|
-
|
|
251
|
+
|
|
243
252
|
# Collect ALL validation errors before raising
|
|
244
253
|
validation_errors = []
|
|
245
|
-
|
|
254
|
+
|
|
246
255
|
# Map incoming properties from test_case_data
|
|
247
256
|
for field_name, field_value in test_case_data.items():
|
|
248
257
|
# Skip non-property fields (these are handled separately)
|
|
249
258
|
if field_name in ['Name', 'Description', 'Precondition', 'Steps', 'Id', QTEST_ID]:
|
|
250
259
|
continue
|
|
251
|
-
|
|
260
|
+
|
|
252
261
|
# Skip None or empty string values (don't update these fields)
|
|
253
262
|
if field_value is None or field_value == '':
|
|
254
263
|
continue
|
|
255
|
-
|
|
264
|
+
|
|
256
265
|
# Validate field exists in project - STRICT validation
|
|
257
266
|
if field_name not in field_definitions:
|
|
258
267
|
validation_errors.append(
|
|
259
268
|
f"❌ Unknown field '{field_name}' - not defined in project configuration"
|
|
260
269
|
)
|
|
261
270
|
continue # Skip to next field, keep collecting errors
|
|
262
|
-
|
|
271
|
+
|
|
263
272
|
field_def = field_definitions[field_name]
|
|
264
273
|
field_id = field_def['field_id']
|
|
265
274
|
data_type = field_def.get('data_type')
|
|
266
275
|
is_multiple = field_def.get('multiple', False)
|
|
267
|
-
|
|
276
|
+
|
|
268
277
|
# Normalize field_value to list for consistent processing
|
|
269
278
|
# Multi-select fields can receive: "value", ["value1", "value2"], or ["value1"]
|
|
270
279
|
# Single-select fields: "value" only
|
|
@@ -274,13 +283,13 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
274
283
|
else:
|
|
275
284
|
# Single-select: keep as single value
|
|
276
285
|
values_to_process = [field_value]
|
|
277
|
-
|
|
286
|
+
|
|
278
287
|
# Validate value(s) for dropdown fields (only if field has allowed values)
|
|
279
288
|
if field_def['values']:
|
|
280
289
|
# Field has allowed values (dropdown/combobox/user fields) - validate strictly
|
|
281
290
|
value_ids = []
|
|
282
291
|
value_names = []
|
|
283
|
-
|
|
292
|
+
|
|
284
293
|
for single_value in values_to_process:
|
|
285
294
|
if single_value not in field_def['values']:
|
|
286
295
|
available = ", ".join(sorted(field_def['values'].keys()))
|
|
@@ -289,15 +298,15 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
289
298
|
f"Allowed values: {available}"
|
|
290
299
|
)
|
|
291
300
|
continue # Skip this value, but continue validating others
|
|
292
|
-
|
|
301
|
+
|
|
293
302
|
# Valid value - add to lists
|
|
294
303
|
value_ids.append(field_def['values'][single_value])
|
|
295
304
|
value_names.append(single_value)
|
|
296
|
-
|
|
305
|
+
|
|
297
306
|
# If all values were invalid, skip this field
|
|
298
307
|
if not value_ids:
|
|
299
308
|
continue
|
|
300
|
-
|
|
309
|
+
|
|
301
310
|
# Format based on field type and value count
|
|
302
311
|
if is_multiple and len(value_ids) == 1:
|
|
303
312
|
# Single value in multi-select field: bracketed string "[419950]"
|
|
@@ -318,7 +327,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
318
327
|
# No validation needed - users can write anything (by design)
|
|
319
328
|
field_value_id = field_value
|
|
320
329
|
field_value_name = field_value if isinstance(field_value, str) else None
|
|
321
|
-
|
|
330
|
+
|
|
322
331
|
# Update or add property (only if no errors for this field)
|
|
323
332
|
props_dict[field_name] = {
|
|
324
333
|
'field_id': field_id,
|
|
@@ -326,7 +335,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
326
335
|
'field_value': field_value_id,
|
|
327
336
|
'field_value_name': field_value_name
|
|
328
337
|
}
|
|
329
|
-
|
|
338
|
+
|
|
330
339
|
# If ANY validation errors found, raise comprehensive error with all issues
|
|
331
340
|
if validation_errors:
|
|
332
341
|
available_fields = ", ".join(sorted(field_definitions.keys()))
|
|
@@ -337,7 +346,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
337
346
|
f"💡 Tip: Use 'get_all_test_cases_fields_for_project' tool to see all fields with their allowed values."
|
|
338
347
|
)
|
|
339
348
|
raise ValueError(error_msg)
|
|
340
|
-
|
|
349
|
+
|
|
341
350
|
# Convert to PropertyResource list, filtering out special fields
|
|
342
351
|
result = []
|
|
343
352
|
for field_name, prop_data in props_dict.items():
|
|
@@ -349,25 +358,25 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
349
358
|
field_value=prop_data['field_value'],
|
|
350
359
|
field_value_name=prop_data.get('field_value_name')
|
|
351
360
|
))
|
|
352
|
-
|
|
361
|
+
|
|
353
362
|
return result
|
|
354
363
|
|
|
355
364
|
def __build_body_for_create_test_case(self, test_cases_data: list[dict],
|
|
356
365
|
folder_to_place_test_cases_to: str = '') -> list:
|
|
357
366
|
# Get field definitions for property mapping (cached for session)
|
|
358
367
|
field_definitions = self.__get_field_definitions_cached()
|
|
359
|
-
|
|
368
|
+
|
|
360
369
|
modules = self._parse_modules()
|
|
361
370
|
parent_id = ''.join(str(module['module_id']) for module in modules if
|
|
362
371
|
folder_to_place_test_cases_to and module['full_module_name'] == folder_to_place_test_cases_to)
|
|
363
|
-
|
|
372
|
+
|
|
364
373
|
bodies = []
|
|
365
374
|
for test_case in test_cases_data:
|
|
366
375
|
# Map properties from user format to API format
|
|
367
376
|
props = self.__map_properties_to_api_format(test_case, field_definitions)
|
|
368
|
-
|
|
377
|
+
|
|
369
378
|
body = swagger_client.TestCaseWithCustomFieldResource(properties=props)
|
|
370
|
-
|
|
379
|
+
|
|
371
380
|
# Only set fields if they are explicitly provided in the input
|
|
372
381
|
# This prevents overwriting existing values with None during partial updates
|
|
373
382
|
if 'Name' in test_case:
|
|
@@ -376,10 +385,10 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
376
385
|
body.precondition = test_case['Precondition']
|
|
377
386
|
if 'Description' in test_case:
|
|
378
387
|
body.description = test_case['Description']
|
|
379
|
-
|
|
388
|
+
|
|
380
389
|
if parent_id:
|
|
381
390
|
body.parent_id = parent_id
|
|
382
|
-
|
|
391
|
+
|
|
383
392
|
# Only set test_steps if Steps are provided in the input
|
|
384
393
|
# This prevents overwriting existing steps during partial updates
|
|
385
394
|
if 'Steps' in test_case and test_case['Steps'] is not None:
|
|
@@ -389,7 +398,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
389
398
|
swagger_client.TestStepResource(description=step.get('Test Step Description'),
|
|
390
399
|
expected=step.get('Test Step Expected Result')))
|
|
391
400
|
body.test_steps = test_steps_resources
|
|
392
|
-
|
|
401
|
+
|
|
393
402
|
bodies.append(body)
|
|
394
403
|
return bodies
|
|
395
404
|
|
|
@@ -406,10 +415,147 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
406
415
|
Exception: \n {stacktrace}""")
|
|
407
416
|
return modules
|
|
408
417
|
|
|
418
|
+
def __get_field_definitions_from_properties_api(self) -> dict:
|
|
419
|
+
"""
|
|
420
|
+
Fallback method: Get field definitions using /properties and /properties-info APIs.
|
|
421
|
+
|
|
422
|
+
These APIs don't require Field Management permission and are available to all users.
|
|
423
|
+
Requires 2 API calls + 1 search to get a test case ID.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
dict: Same structure as __get_project_field_definitions()
|
|
427
|
+
"""
|
|
428
|
+
logger.info(
|
|
429
|
+
"Using properties API fallback (no Field Management permission). "
|
|
430
|
+
"This requires getting a template test case first."
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Step 1: Get any test case ID to query properties
|
|
434
|
+
search_instance = swagger_client.SearchApi(self._client)
|
|
435
|
+
body = swagger_client.ArtifactSearchParams(
|
|
436
|
+
object_type='test-cases',
|
|
437
|
+
fields=['*'],
|
|
438
|
+
query='' # Empty query returns all test cases
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
try:
|
|
442
|
+
# Search for any test case - just need one
|
|
443
|
+
response = search_instance.search_artifact(
|
|
444
|
+
self.qtest_project_id,
|
|
445
|
+
body,
|
|
446
|
+
page_size=1,
|
|
447
|
+
page=1
|
|
448
|
+
)
|
|
449
|
+
except ApiException as e:
|
|
450
|
+
stacktrace = format_exc()
|
|
451
|
+
logger.error(f"Failed to find test case for properties API: {stacktrace}")
|
|
452
|
+
raise ValueError(
|
|
453
|
+
f"Cannot find any test case to query field definitions. "
|
|
454
|
+
f"Please create at least one test case in project {self.qtest_project_id}"
|
|
455
|
+
) from e
|
|
456
|
+
|
|
457
|
+
if not response or not response.get('items') or len(response['items']) == 0:
|
|
458
|
+
raise ValueError(
|
|
459
|
+
f"No test cases found in project {self.qtest_project_id}. "
|
|
460
|
+
f"Please create at least one test case to retrieve field definitions."
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
test_case_id = response['items'][0]['id']
|
|
464
|
+
logger.info(f"Using test case ID {test_case_id} to retrieve field definitions")
|
|
465
|
+
|
|
466
|
+
# Step 2: Call /properties API
|
|
467
|
+
headers = {
|
|
468
|
+
"Authorization": f"Bearer {self.qtest_api_token.get_secret_value()}"
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
properties_url = f"{self.base_url}/api/v3/projects/{self.qtest_project_id}/test-cases/{test_case_id}/properties"
|
|
472
|
+
properties_info_url = f"{self.base_url}/api/v3/projects/{self.qtest_project_id}/test-cases/{test_case_id}/properties-info"
|
|
473
|
+
|
|
474
|
+
try:
|
|
475
|
+
# Get properties with current values and field metadata
|
|
476
|
+
props_response = requests.get(
|
|
477
|
+
properties_url,
|
|
478
|
+
headers=headers,
|
|
479
|
+
params={'calledBy': 'testcase_properties'}
|
|
480
|
+
)
|
|
481
|
+
props_response.raise_for_status()
|
|
482
|
+
properties_data = props_response.json()
|
|
483
|
+
|
|
484
|
+
# Get properties-info with data types and allowed values
|
|
485
|
+
info_response = requests.get(properties_info_url, headers=headers)
|
|
486
|
+
info_response.raise_for_status()
|
|
487
|
+
info_data = info_response.json()
|
|
488
|
+
|
|
489
|
+
except requests.exceptions.RequestException as e:
|
|
490
|
+
stacktrace = format_exc()
|
|
491
|
+
logger.error(f"Failed to call properties API: {stacktrace}")
|
|
492
|
+
raise ValueError(
|
|
493
|
+
f"Unable to retrieve field definitions using properties API. "
|
|
494
|
+
f"Error: {stacktrace}"
|
|
495
|
+
) from e
|
|
496
|
+
|
|
497
|
+
# Step 3: Build field mapping by merging both responses
|
|
498
|
+
field_mapping = {}
|
|
499
|
+
|
|
500
|
+
# Create lookup by field ID from properties-info
|
|
501
|
+
metadata_by_id = {item['id']: item for item in info_data['metadata']}
|
|
502
|
+
|
|
503
|
+
# Data type mapping to determine 'multiple' flag
|
|
504
|
+
MULTI_SELECT_TYPES = {
|
|
505
|
+
'UserListDataType',
|
|
506
|
+
'MultiSelectionDataType',
|
|
507
|
+
'CheckListDataType'
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
USER_FIELD_TYPES = {'UserListDataType'}
|
|
511
|
+
|
|
512
|
+
# System fields to exclude (same as in property mapping)
|
|
513
|
+
excluded_fields = {'Shared', 'Projects Shared to'}
|
|
514
|
+
|
|
515
|
+
for prop in properties_data:
|
|
516
|
+
field_name = prop.get('name')
|
|
517
|
+
field_id = prop.get('id')
|
|
518
|
+
|
|
519
|
+
if not field_name or field_name in excluded_fields:
|
|
520
|
+
continue
|
|
521
|
+
|
|
522
|
+
# Get metadata for this field
|
|
523
|
+
metadata = metadata_by_id.get(field_id, {})
|
|
524
|
+
data_type_str = metadata.get('data_type')
|
|
525
|
+
|
|
526
|
+
# Determine data_type number (5 for user fields, None for others)
|
|
527
|
+
data_type = 5 if data_type_str in USER_FIELD_TYPES else None
|
|
528
|
+
|
|
529
|
+
# Determine if multi-select
|
|
530
|
+
is_multiple = data_type_str in MULTI_SELECT_TYPES
|
|
531
|
+
|
|
532
|
+
field_mapping[field_name] = {
|
|
533
|
+
'field_id': field_id,
|
|
534
|
+
'required': prop.get('required', False),
|
|
535
|
+
'data_type': data_type,
|
|
536
|
+
'multiple': is_multiple,
|
|
537
|
+
'values': {}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
# Map allowed values from metadata
|
|
541
|
+
allowed_values = metadata.get('allowed_values', [])
|
|
542
|
+
for allowed_val in allowed_values:
|
|
543
|
+
value_text = allowed_val.get('value_text')
|
|
544
|
+
value_id = allowed_val.get('id')
|
|
545
|
+
if value_text and value_id:
|
|
546
|
+
field_mapping[field_name]['values'][value_text] = value_id
|
|
547
|
+
|
|
548
|
+
logger.info(
|
|
549
|
+
f"Retrieved {len(field_mapping)} field definitions using properties API. "
|
|
550
|
+
f"This method works for all users without Field Management permission."
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
return field_mapping
|
|
554
|
+
|
|
409
555
|
def __get_project_field_definitions(self) -> dict:
|
|
410
556
|
"""
|
|
411
557
|
Get structured field definitions for test cases in the project.
|
|
412
|
-
|
|
558
|
+
|
|
413
559
|
Returns:
|
|
414
560
|
dict: Mapping of field names to their IDs and allowed values.
|
|
415
561
|
Example: {
|
|
@@ -427,15 +573,24 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
427
573
|
"""
|
|
428
574
|
fields_api = self.__instantiate_fields_api_instance()
|
|
429
575
|
qtest_object = 'test-cases'
|
|
430
|
-
|
|
576
|
+
|
|
431
577
|
try:
|
|
432
578
|
fields = fields_api.get_fields(self.qtest_project_id, qtest_object)
|
|
433
579
|
except ApiException as e:
|
|
580
|
+
# Check if permission denied (403) - use fallback
|
|
581
|
+
if e.status == 403:
|
|
582
|
+
logger.warning(
|
|
583
|
+
"get_fields permission denied (Field Management permission required). "
|
|
584
|
+
"Using properties API fallback..."
|
|
585
|
+
)
|
|
586
|
+
return self.__get_field_definitions_from_properties_api()
|
|
587
|
+
|
|
588
|
+
# Other API errors
|
|
434
589
|
stacktrace = format_exc()
|
|
435
590
|
logger.error(f"Exception when calling FieldAPI->get_fields:\n {stacktrace}")
|
|
436
591
|
raise ValueError(
|
|
437
592
|
f"Unable to get test case fields for project {self.qtest_project_id}. Exception: \n {stacktrace}")
|
|
438
|
-
|
|
593
|
+
|
|
439
594
|
# Build structured mapping
|
|
440
595
|
field_mapping = {}
|
|
441
596
|
for field in fields:
|
|
@@ -447,7 +602,7 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
447
602
|
'multiple': getattr(field, 'multiple', False), # True = multi-select, needs array format
|
|
448
603
|
'values': {}
|
|
449
604
|
}
|
|
450
|
-
|
|
605
|
+
|
|
451
606
|
# Map allowed values if field has them (dropdown/combobox/user fields)
|
|
452
607
|
# Only include active values (is_active=True)
|
|
453
608
|
if hasattr(field, 'allowed_values') and field.allowed_values:
|
|
@@ -455,38 +610,38 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
455
610
|
# Skip inactive values (deleted/deprecated options)
|
|
456
611
|
if hasattr(allowed_value, 'is_active') and not allowed_value.is_active:
|
|
457
612
|
continue
|
|
458
|
-
|
|
613
|
+
|
|
459
614
|
# AllowedValueResource has 'label' for the display name and 'value' for the ID
|
|
460
615
|
# Note: 'value' is the field_value, not 'id'
|
|
461
616
|
# For user fields (data_type=5), label is user name and value is user ID
|
|
462
617
|
value_label = allowed_value.label
|
|
463
618
|
value_id = allowed_value.value
|
|
464
619
|
field_mapping[field_name]['values'][value_label] = value_id
|
|
465
|
-
|
|
620
|
+
|
|
466
621
|
return field_mapping
|
|
467
622
|
|
|
468
623
|
def __format_field_info_for_display(self, field_definitions: dict) -> str:
|
|
469
624
|
"""
|
|
470
625
|
Format field definitions in human-readable format for LLM.
|
|
471
|
-
|
|
626
|
+
|
|
472
627
|
Args:
|
|
473
628
|
field_definitions: Output from __get_project_field_definitions()
|
|
474
|
-
|
|
629
|
+
|
|
475
630
|
Returns:
|
|
476
631
|
Formatted string with field information
|
|
477
632
|
"""
|
|
478
633
|
output = [f"Available Test Case Fields for Project {self.qtest_project_id}:\n"]
|
|
479
|
-
|
|
634
|
+
|
|
480
635
|
for field_name, field_info in sorted(field_definitions.items()):
|
|
481
636
|
required_marker = " (Required)" if field_info.get('required') else ""
|
|
482
637
|
output.append(f"\n{field_name}{required_marker}:")
|
|
483
|
-
|
|
638
|
+
|
|
484
639
|
if field_info.get('values'):
|
|
485
640
|
for value_name, value_id in sorted(field_info['values'].items()):
|
|
486
641
|
output.append(f" - {value_name} (id: {value_id})")
|
|
487
642
|
else:
|
|
488
643
|
output.append(" Type: text")
|
|
489
|
-
|
|
644
|
+
|
|
490
645
|
output.append("\n\nUse these exact value names when creating or updating test cases.")
|
|
491
646
|
return ''.join(output)
|
|
492
647
|
|
|
@@ -494,12 +649,12 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
494
649
|
"""
|
|
495
650
|
Get formatted information about available test case fields and their values.
|
|
496
651
|
This method is exposed as a tool for LLM to query field information.
|
|
497
|
-
|
|
652
|
+
|
|
498
653
|
Args:
|
|
499
654
|
force_refresh: If True, reload field definitions from API instead of using cache.
|
|
500
655
|
Use this if project configuration has changed (new fields added,
|
|
501
656
|
dropdown values modified, etc.).
|
|
502
|
-
|
|
657
|
+
|
|
503
658
|
Returns:
|
|
504
659
|
Formatted string with field names and allowed values
|
|
505
660
|
"""
|
|
@@ -547,10 +702,10 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
547
702
|
|
|
548
703
|
def __parse_data(self, response_to_parse: dict, parsed_data: list, extract_images: bool=False, prompt: str=None):
|
|
549
704
|
import html
|
|
550
|
-
|
|
551
|
-
#
|
|
552
|
-
|
|
553
|
-
|
|
705
|
+
|
|
706
|
+
# PERMISSION-FREE: Parse properties directly from API response
|
|
707
|
+
# No get_fields call needed - works for all users
|
|
708
|
+
|
|
554
709
|
for item in response_to_parse['items']:
|
|
555
710
|
# Start with core fields (always present)
|
|
556
711
|
parsed_data_row = {
|
|
@@ -565,31 +720,17 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
565
720
|
'Test Step Expected Result': self._process_image(step[1]['expected'], extract_images, prompt)
|
|
566
721
|
}, enumerate(item['test_steps']))),
|
|
567
722
|
}
|
|
568
|
-
|
|
569
|
-
#
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
#
|
|
576
|
-
field_value =
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
# Use field_value_name if available (for dropdowns), otherwise field_value
|
|
580
|
-
field_value = prop.get('field_value_name') or prop.get('field_value') or ''
|
|
581
|
-
break
|
|
582
|
-
|
|
583
|
-
# Format based on field type
|
|
584
|
-
if is_multiple and (field_value is None or field_value == ''):
|
|
585
|
-
# Multi-select field with no value: show empty array with hint
|
|
586
|
-
parsed_data_row[field_name] = '[] (multi-select)'
|
|
587
|
-
elif field_value is not None:
|
|
588
|
-
parsed_data_row[field_name] = field_value
|
|
589
|
-
else:
|
|
590
|
-
# Regular field with no value
|
|
591
|
-
parsed_data_row[field_name] = ''
|
|
592
|
-
|
|
723
|
+
|
|
724
|
+
# Add custom fields directly from API response properties
|
|
725
|
+
for prop in item['properties']:
|
|
726
|
+
field_name = prop.get('field_name')
|
|
727
|
+
if not field_name:
|
|
728
|
+
continue
|
|
729
|
+
|
|
730
|
+
# Use field_value_name if available (for dropdowns/users), otherwise field_value
|
|
731
|
+
field_value = prop.get('field_value_name') or prop.get('field_value') or ''
|
|
732
|
+
parsed_data_row[field_name] = field_value
|
|
733
|
+
|
|
593
734
|
parsed_data.append(parsed_data_row)
|
|
594
735
|
|
|
595
736
|
def _process_image(self, content: str, extract: bool=False, prompt: str=None):
|
|
@@ -647,6 +788,38 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
647
788
|
parsed_data = self.__perform_search_by_dql(dql)
|
|
648
789
|
return parsed_data[0]['QTest Id']
|
|
649
790
|
|
|
791
|
+
def __find_qtest_requirement_id_by_id(self, requirement_id: str) -> int:
|
|
792
|
+
"""Search for requirement's internal QTest ID using requirement ID (RQ-xxx format).
|
|
793
|
+
|
|
794
|
+
Args:
|
|
795
|
+
requirement_id: Requirement ID in format RQ-123
|
|
796
|
+
|
|
797
|
+
Returns:
|
|
798
|
+
int: Internal QTest ID for the requirement
|
|
799
|
+
|
|
800
|
+
Raises:
|
|
801
|
+
ValueError: If requirement is not found
|
|
802
|
+
"""
|
|
803
|
+
dql = f"Id = '{requirement_id}'"
|
|
804
|
+
search_instance: SearchApi = swagger_client.SearchApi(self._client)
|
|
805
|
+
body = swagger_client.ArtifactSearchParams(object_type='requirements', fields=['*'], query=dql)
|
|
806
|
+
|
|
807
|
+
try:
|
|
808
|
+
response = search_instance.search_artifact(self.qtest_project_id, body)
|
|
809
|
+
if response['total'] == 0:
|
|
810
|
+
raise ValueError(
|
|
811
|
+
f"Requirement '{requirement_id}' not found in project {self.qtest_project_id}. "
|
|
812
|
+
f"Please verify the requirement ID exists."
|
|
813
|
+
)
|
|
814
|
+
return response['items'][0]['id']
|
|
815
|
+
except ApiException as e:
|
|
816
|
+
stacktrace = format_exc()
|
|
817
|
+
logger.error(f"Exception when searching for requirement: \n {stacktrace}")
|
|
818
|
+
raise ToolException(
|
|
819
|
+
f"Unable to search for requirement '{requirement_id}' in project {self.qtest_project_id}. "
|
|
820
|
+
f"Exception: \n{stacktrace}"
|
|
821
|
+
) from e
|
|
822
|
+
|
|
650
823
|
def __is_jira_requirement_present(self, jira_issue_id: str) -> tuple[bool, dict]:
|
|
651
824
|
""" Define if particular Jira requirement is present in qtest or not """
|
|
652
825
|
dql = f"'External Id' = '{jira_issue_id}'"
|
|
@@ -663,31 +836,112 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
663
836
|
logger.error(f"Error: {format_exc()}")
|
|
664
837
|
raise e
|
|
665
838
|
|
|
666
|
-
def _get_jira_requirement_id(self, jira_issue_id: str) -> int
|
|
667
|
-
"""
|
|
839
|
+
def _get_jira_requirement_id(self, jira_issue_id: str) -> int:
|
|
840
|
+
"""Search for requirement id using the linked jira_issue_id.
|
|
841
|
+
|
|
842
|
+
Args:
|
|
843
|
+
jira_issue_id: External Jira issue ID (e.g., PLAN-128)
|
|
844
|
+
|
|
845
|
+
Returns:
|
|
846
|
+
int: Internal QTest ID for the Jira requirement
|
|
847
|
+
|
|
848
|
+
Raises:
|
|
849
|
+
ValueError: If Jira requirement is not found in QTest
|
|
850
|
+
"""
|
|
668
851
|
is_present, response = self.__is_jira_requirement_present(jira_issue_id)
|
|
669
852
|
if not is_present:
|
|
670
|
-
|
|
853
|
+
raise ValueError(
|
|
854
|
+
f"Jira requirement '{jira_issue_id}' not found in QTest project {self.qtest_project_id}. "
|
|
855
|
+
f"Please ensure the Jira issue is linked to QTest as a requirement."
|
|
856
|
+
)
|
|
671
857
|
return response['items'][0]['id']
|
|
672
858
|
|
|
673
859
|
|
|
674
860
|
def link_tests_to_jira_requirement(self, requirement_external_id: str, json_list_of_test_case_ids: str) -> str:
|
|
675
|
-
"""
|
|
861
|
+
"""Link test cases to external Jira requirement.
|
|
862
|
+
|
|
863
|
+
Args:
|
|
864
|
+
requirement_external_id: Jira issue ID (e.g., PLAN-128)
|
|
865
|
+
json_list_of_test_case_ids: JSON array string of test case IDs (e.g., '["TC-123", "TC-234"]')
|
|
866
|
+
|
|
867
|
+
Returns:
|
|
868
|
+
Success message with linked test case IDs
|
|
869
|
+
"""
|
|
676
870
|
link_object_api_instance = swagger_client.ObjectLinkApi(self._client)
|
|
677
871
|
source_type = "requirements"
|
|
678
872
|
linked_type = "test-cases"
|
|
679
|
-
|
|
873
|
+
test_case_ids = json.loads(json_list_of_test_case_ids)
|
|
874
|
+
qtest_test_case_ids = [self.__find_qtest_id_by_test_id(tc_id) for tc_id in test_case_ids]
|
|
680
875
|
requirement_id = self._get_jira_requirement_id(requirement_external_id)
|
|
681
876
|
|
|
682
877
|
try:
|
|
683
|
-
response = link_object_api_instance.link_artifacts(
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
878
|
+
response = link_object_api_instance.link_artifacts(
|
|
879
|
+
self.qtest_project_id,
|
|
880
|
+
object_id=requirement_id,
|
|
881
|
+
type=linked_type,
|
|
882
|
+
object_type=source_type,
|
|
883
|
+
body=qtest_test_case_ids
|
|
884
|
+
)
|
|
885
|
+
linked_test_cases = [link.pid for link in response[0].objects]
|
|
886
|
+
return (
|
|
887
|
+
f"Successfully linked {len(linked_test_cases)} test case(s) to Jira requirement '{requirement_external_id}' "
|
|
888
|
+
f"in project {self.qtest_project_id}.\n"
|
|
889
|
+
f"Linked test cases: {', '.join(linked_test_cases)}"
|
|
890
|
+
)
|
|
891
|
+
except ApiException as e:
|
|
892
|
+
stacktrace = format_exc()
|
|
893
|
+
logger.error(f"Error linking to Jira requirement: {stacktrace}")
|
|
894
|
+
raise ToolException(
|
|
895
|
+
f"Unable to link test cases to Jira requirement '{requirement_external_id}' "
|
|
896
|
+
f"in project {self.qtest_project_id}. Exception: \n{stacktrace}"
|
|
897
|
+
) from e
|
|
898
|
+
|
|
899
|
+
def link_tests_to_qtest_requirement(self, requirement_id: str, json_list_of_test_case_ids: str) -> str:
|
|
900
|
+
"""Link test cases to internal QTest requirement.
|
|
901
|
+
|
|
902
|
+
Args:
|
|
903
|
+
requirement_id: QTest requirement ID in format RQ-123
|
|
904
|
+
json_list_of_test_case_ids: JSON array string of test case IDs (e.g., '["TC-123", "TC-234"]')
|
|
905
|
+
|
|
906
|
+
Returns:
|
|
907
|
+
Success message with linked test case IDs
|
|
908
|
+
|
|
909
|
+
Raises:
|
|
910
|
+
ValueError: If requirement or test cases are not found
|
|
911
|
+
ToolException: If linking fails
|
|
912
|
+
"""
|
|
913
|
+
link_object_api_instance = swagger_client.ObjectLinkApi(self._client)
|
|
914
|
+
source_type = "requirements"
|
|
915
|
+
linked_type = "test-cases"
|
|
916
|
+
|
|
917
|
+
# Parse and convert test case IDs
|
|
918
|
+
test_case_ids = json.loads(json_list_of_test_case_ids)
|
|
919
|
+
qtest_test_case_ids = [self.__find_qtest_id_by_test_id(tc_id) for tc_id in test_case_ids]
|
|
920
|
+
|
|
921
|
+
# Get internal QTest ID for the requirement
|
|
922
|
+
qtest_requirement_id = self.__find_qtest_requirement_id_by_id(requirement_id)
|
|
923
|
+
|
|
924
|
+
try:
|
|
925
|
+
response = link_object_api_instance.link_artifacts(
|
|
926
|
+
self.qtest_project_id,
|
|
927
|
+
object_id=qtest_requirement_id,
|
|
928
|
+
type=linked_type,
|
|
929
|
+
object_type=source_type,
|
|
930
|
+
body=qtest_test_case_ids
|
|
931
|
+
)
|
|
932
|
+
linked_test_cases = [link.pid for link in response[0].objects]
|
|
933
|
+
return (
|
|
934
|
+
f"Successfully linked {len(linked_test_cases)} test case(s) to QTest requirement '{requirement_id}' "
|
|
935
|
+
f"in project {self.qtest_project_id}.\n"
|
|
936
|
+
f"Linked test cases: {', '.join(linked_test_cases)}"
|
|
937
|
+
)
|
|
938
|
+
except ApiException as e:
|
|
939
|
+
stacktrace = format_exc()
|
|
940
|
+
logger.error(f"Error linking to QTest requirement: {stacktrace}")
|
|
941
|
+
raise ToolException(
|
|
942
|
+
f"Unable to link test cases to QTest requirement '{requirement_id}' "
|
|
943
|
+
f"in project {self.qtest_project_id}. Exception: \n{stacktrace}"
|
|
944
|
+
) from e
|
|
691
945
|
|
|
692
946
|
def search_by_dql(self, dql: str, extract_images:bool=False, prompt: str=None):
|
|
693
947
|
"""Search for the test cases in qTest using Data Query Language """
|
|
@@ -809,12 +1063,19 @@ class QtestApiWrapper(BaseToolApiWrapper):
|
|
|
809
1063
|
"ref": self.delete_test_case,
|
|
810
1064
|
},
|
|
811
1065
|
{
|
|
812
|
-
"name": "
|
|
813
|
-
"mode": "
|
|
814
|
-
"description": "
|
|
1066
|
+
"name": "link_tests_to_jira_requirement",
|
|
1067
|
+
"mode": "link_tests_to_jira_requirement",
|
|
1068
|
+
"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\"]'",
|
|
815
1069
|
"args_schema": QtestLinkTestCaseToJiraRequirement,
|
|
816
1070
|
"ref": self.link_tests_to_jira_requirement,
|
|
817
1071
|
},
|
|
1072
|
+
{
|
|
1073
|
+
"name": "link_tests_to_qtest_requirement",
|
|
1074
|
+
"mode": "link_tests_to_qtest_requirement",
|
|
1075
|
+
"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\"]'",
|
|
1076
|
+
"args_schema": QtestLinkTestCaseToQtestRequirement,
|
|
1077
|
+
"ref": self.link_tests_to_qtest_requirement,
|
|
1078
|
+
},
|
|
818
1079
|
{
|
|
819
1080
|
"name": "get_modules",
|
|
820
1081
|
"mode": "get_modules",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alita_sdk
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.428b2
|
|
4
4
|
Summary: SDK for building langchain agents using resources from Alita
|
|
5
5
|
Author-email: Artem Rozumenko <artyom.rozumenko@gmail.com>, Mikalai Biazruchka <mikalai_biazruchka@epam.com>, Roman Mitusov <roman_mitusov@epam.com>, Ivan Krakhmaliuk <lifedj27@gmail.com>, Artem Dubrovskiy <ad13box@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -45,7 +45,7 @@ alita_sdk/runtime/langchain/assistant.py,sha256=qKoEjbGuUnX-OZDHmSaK3plb1jON9unz
|
|
|
45
45
|
alita_sdk/runtime/langchain/chat_message_template.py,sha256=kPz8W2BG6IMyITFDA5oeb5BxVRkHEVZhuiGl4MBZKdc,2176
|
|
46
46
|
alita_sdk/runtime/langchain/constants.py,sha256=I3dwexVp_Qq3MueRA2ClLgFDEhk4BkJhgR6m7V0gVPc,3404
|
|
47
47
|
alita_sdk/runtime/langchain/indexer.py,sha256=0ENHy5EOhThnAiYFc7QAsaTNp9rr8hDV_hTK8ahbatk,37592
|
|
48
|
-
alita_sdk/runtime/langchain/langraph_agent.py,sha256=
|
|
48
|
+
alita_sdk/runtime/langchain/langraph_agent.py,sha256=CxDFfUTCG-i8koMR9PwOktvlcdUe5cyG4D8CQHrTH1E,51836
|
|
49
49
|
alita_sdk/runtime/langchain/mixedAgentParser.py,sha256=M256lvtsL3YtYflBCEp-rWKrKtcY1dJIyRGVv7KW9ME,2611
|
|
50
50
|
alita_sdk/runtime/langchain/mixedAgentRenderes.py,sha256=asBtKqm88QhZRILditjYICwFVKF5KfO38hu2O-WrSWE,5964
|
|
51
51
|
alita_sdk/runtime/langchain/store_manager.py,sha256=i8Fl11IXJhrBXq1F1ukEVln57B1IBe-tqSUvfUmBV4A,2218
|
|
@@ -303,7 +303,7 @@ alita_sdk/tools/postman/postman_analysis.py,sha256=ckc2BfKEop0xnmLPksVRE_Y94ixuq
|
|
|
303
303
|
alita_sdk/tools/pptx/__init__.py,sha256=vVUrWnj7KWJgEk9oxGSsCAQ2SMSXrp_SFOdUHYQKcAo,3444
|
|
304
304
|
alita_sdk/tools/pptx/pptx_wrapper.py,sha256=yyCYcTlIY976kJ4VfPo4dyxj4yeii9j9TWP6W8ZIpN8,29195
|
|
305
305
|
alita_sdk/tools/qtest/__init__.py,sha256=Jf0xo5S_4clXR2TfCbJbB1sFgCbcFQRM-YYX2ltWBzo,4461
|
|
306
|
-
alita_sdk/tools/qtest/api_wrapper.py,sha256=
|
|
306
|
+
alita_sdk/tools/qtest/api_wrapper.py,sha256=Cds_TnyWhR-2NaoHio9ePYMLT7RGS0nx20KAqifIOWk,50977
|
|
307
307
|
alita_sdk/tools/qtest/tool.py,sha256=kKzNPS4fUC76WQQttQ6kdVANViHEvKE8Kf174MQiNYU,562
|
|
308
308
|
alita_sdk/tools/rally/__init__.py,sha256=2BPPXJxAOKgfmaxVFVvxndfK0JxOXDLkoRmzu2dUwOE,3512
|
|
309
309
|
alita_sdk/tools/rally/api_wrapper.py,sha256=mouzU6g0KML4UNapdk0k6Q0pU3MpJuWnNo71n9PSEHM,11752
|
|
@@ -353,8 +353,8 @@ alita_sdk/tools/zephyr_scale/api_wrapper.py,sha256=kT0TbmMvuKhDUZc0i7KO18O38JM9S
|
|
|
353
353
|
alita_sdk/tools/zephyr_squad/__init__.py,sha256=0ne8XLJEQSLOWfzd2HdnqOYmQlUliKHbBED5kW_Vias,2895
|
|
354
354
|
alita_sdk/tools/zephyr_squad/api_wrapper.py,sha256=kmw_xol8YIYFplBLWTqP_VKPRhL_1ItDD0_vXTe_UuI,14906
|
|
355
355
|
alita_sdk/tools/zephyr_squad/zephyr_squad_cloud_client.py,sha256=R371waHsms4sllHCbijKYs90C-9Yu0sSR3N4SUfQOgU,5066
|
|
356
|
-
alita_sdk-0.3.
|
|
357
|
-
alita_sdk-0.3.
|
|
358
|
-
alita_sdk-0.3.
|
|
359
|
-
alita_sdk-0.3.
|
|
360
|
-
alita_sdk-0.3.
|
|
356
|
+
alita_sdk-0.3.428b2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
357
|
+
alita_sdk-0.3.428b2.dist-info/METADATA,sha256=WmL2Jo4Efefiq86rZ0hYJlMZx-epmDrwx-2qNTaWnEc,19073
|
|
358
|
+
alita_sdk-0.3.428b2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
359
|
+
alita_sdk-0.3.428b2.dist-info/top_level.txt,sha256=0vJYy5p_jK6AwVb1aqXr7Kgqgk3WDtQ6t5C-XI9zkmg,10
|
|
360
|
+
alita_sdk-0.3.428b2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|