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.

@@ -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* - to resume the pipeline - type anything"
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
- # Get field definitions to ensure all fields are included (uses cached version)
552
- field_definitions = self.__get_field_definitions_cached()
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
- # Dynamically add all custom fields from project configuration
570
- # This ensures consistency and includes fields even if they have null/empty values
571
- for field_name in field_definitions.keys():
572
- field_def = field_definitions[field_name]
573
- is_multiple = field_def.get('multiple', False)
574
-
575
- # Find the property value in the response (if exists)
576
- field_value = None
577
- for prop in item['properties']:
578
- if prop['field_name'] == field_name:
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 | None:
667
- """ Search for requirement id using the linked jira_issue_id. """
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
- return None
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
- """ Link the list of the test cases represented as string like this '["TC-123", "TC-234"]' to the Jira requirement represented as external id e.g. PLAN-128 which is the Jira Issue Id"""
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
- list = [self.__find_qtest_id_by_test_id(test_case_id) for test_case_id in json.loads(json_list_of_test_case_ids)]
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(self.qtest_project_id, object_id=requirement_id,
684
- type=linked_type,
685
- object_type=source_type, body=list)
686
- return f"The test cases with the following id's - {[link.pid for link in response[0].objects]} have been linked in following project {self.qtest_project_id} under following requirement {requirement_external_id}"
687
- except Exception as e:
688
- from traceback import format_exc
689
- logger.error(f"Error: {format_exc()}")
690
- raise e
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": "link_tests_to_requirement",
813
- "mode": "link_tests_to_requirement",
814
- "description": """Link tests to Jira requirements. The input is jira issue id and th list of test ids in format '["TC-123", "TC-234", "TC-345"]'""",
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.427
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=SGEjHdPiV7dQno6hcnKhbQMfU_gRU0kKD_0uXzt2bV4,51833
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=kxH4H1mFm4-_xqJ3js92YzT9sJ-6XC30moC1WCExano,40926
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.427.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
357
- alita_sdk-0.3.427.dist-info/METADATA,sha256=J0tNPenigoEpUHXwQ5dQPc5lBEnwsgNFrXbplG0QAZs,19071
358
- alita_sdk-0.3.427.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
359
- alita_sdk-0.3.427.dist-info/top_level.txt,sha256=0vJYy5p_jK6AwVb1aqXr7Kgqgk3WDtQ6t5C-XI9zkmg,10
360
- alita_sdk-0.3.427.dist-info/RECORD,,
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,,