lucius-mcp 0.2.2__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. lucius_mcp-0.4.0.dist-info/METADATA +124 -0
  2. {lucius_mcp-0.2.2.dist-info → lucius_mcp-0.4.0.dist-info}/RECORD +53 -25
  3. src/client/__init__.py +16 -0
  4. src/client/client.py +559 -14
  5. src/client/exceptions.py +23 -0
  6. src/client/generated/README.md +29 -0
  7. src/client/generated/__init__.py +6 -0
  8. src/client/generated/api/__init__.py +3 -0
  9. src/client/generated/api/integration_controller_api.py +5285 -0
  10. src/client/generated/api/test_layer_controller_api.py +1746 -0
  11. src/client/generated/api/test_layer_schema_controller_api.py +1415 -0
  12. src/client/generated/docs/IntegrationControllerApi.md +1224 -0
  13. src/client/generated/docs/TestLayerControllerApi.md +407 -0
  14. src/client/generated/docs/TestLayerSchemaControllerApi.md +350 -0
  15. src/client/overridden/test_case_custom_fields_v2.py +254 -0
  16. src/services/__init__.py +16 -0
  17. src/services/custom_field_value_service.py +301 -0
  18. src/services/integration_service.py +205 -0
  19. src/services/launch_service.py +306 -0
  20. src/services/search_service.py +1 -1
  21. src/services/shared_step_service.py +34 -17
  22. src/services/test_case_service.py +769 -117
  23. src/services/test_layer_service.py +427 -0
  24. src/tools/__init__.py +53 -0
  25. src/tools/create_custom_field_value.py +38 -0
  26. src/tools/create_test_case.py +68 -19
  27. src/tools/create_test_layer.py +33 -0
  28. src/tools/create_test_layer_schema.py +39 -0
  29. src/tools/delete_custom_field_value.py +57 -0
  30. src/tools/delete_test_case.py +5 -4
  31. src/tools/delete_test_layer.py +44 -0
  32. src/tools/delete_test_layer_schema.py +44 -0
  33. src/tools/get_custom_fields.py +2 -1
  34. src/tools/get_test_case_custom_fields.py +34 -0
  35. src/tools/launches.py +198 -0
  36. src/tools/link_shared_step.py +18 -12
  37. src/tools/list_custom_field_values.py +72 -0
  38. src/tools/list_integrations.py +77 -0
  39. src/tools/list_test_layer_schemas.py +43 -0
  40. src/tools/list_test_layers.py +38 -0
  41. src/tools/search.py +9 -6
  42. src/tools/shared_steps.py +23 -8
  43. src/tools/test_layers.py +21 -0
  44. src/tools/unlink_shared_step.py +19 -5
  45. src/tools/update_custom_field_value.py +55 -0
  46. src/tools/update_test_case.py +113 -23
  47. src/tools/update_test_layer.py +44 -0
  48. src/tools/update_test_layer_schema.py +51 -0
  49. src/utils/__init__.py +4 -0
  50. src/utils/links.py +13 -0
  51. lucius_mcp-0.2.2.dist-info/METADATA +0 -290
  52. {lucius_mcp-0.2.2.dist-info → lucius_mcp-0.4.0.dist-info}/WHEEL +0 -0
  53. {lucius_mcp-0.2.2.dist-info → lucius_mcp-0.4.0.dist-info}/entry_points.txt +0 -0
  54. {lucius_mcp-0.2.2.dist-info → lucius_mcp-0.4.0.dist-info}/licenses/LICENSE +0 -0
src/client/client.py CHANGED
@@ -10,7 +10,7 @@ from collections.abc import Awaitable
10
10
  from typing import Literal, TypeVar, cast, overload
11
11
 
12
12
  import httpx
13
- from pydantic import SecretStr
13
+ from pydantic import Field, SecretStr, ValidationError
14
14
 
15
15
  from src.client.exceptions import TestCaseNotFoundError
16
16
  from src.utils.config import settings
@@ -26,27 +26,45 @@ from .exceptions import (
26
26
  from .generated.api.custom_field_controller_api import CustomFieldControllerApi
27
27
  from .generated.api.custom_field_project_controller_api import CustomFieldProjectControllerApi
28
28
  from .generated.api.custom_field_project_controller_v2_api import CustomFieldProjectControllerV2Api
29
+ from .generated.api.custom_field_value_controller_api import CustomFieldValueControllerApi
29
30
  from .generated.api.custom_field_value_project_controller_api import CustomFieldValueProjectControllerApi
31
+ from .generated.api.integration_controller_api import IntegrationControllerApi
32
+ from .generated.api.launch_controller_api import LaunchControllerApi
33
+ from .generated.api.launch_search_controller_api import LaunchSearchControllerApi
30
34
  from .generated.api.shared_step_attachment_controller_api import SharedStepAttachmentControllerApi
31
35
  from .generated.api.shared_step_controller_api import SharedStepControllerApi
32
36
  from .generated.api.shared_step_scenario_controller_api import SharedStepScenarioControllerApi
33
37
  from .generated.api.test_case_attachment_controller_api import TestCaseAttachmentControllerApi
34
38
  from .generated.api.test_case_controller_api import TestCaseControllerApi
35
- from .generated.api.test_case_custom_field_controller_api import TestCaseCustomFieldControllerApi
36
39
  from .generated.api.test_case_overview_controller_api import TestCaseOverviewControllerApi
37
40
  from .generated.api.test_case_scenario_controller_api import TestCaseScenarioControllerApi
38
41
  from .generated.api.test_case_search_controller_api import TestCaseSearchControllerApi
42
+ from .generated.api.test_layer_controller_api import TestLayerControllerApi
43
+ from .generated.api.test_layer_schema_controller_api import TestLayerSchemaControllerApi
39
44
  from .generated.api_client import ApiClient
40
45
  from .generated.configuration import Configuration
41
46
  from .generated.exceptions import ApiException
47
+ from .generated.models.aql_validate_response_dto import AqlValidateResponseDto
42
48
  from .generated.models.attachment_step_dto import AttachmentStepDto
43
49
  from .generated.models.body_step_dto import BodyStepDto
44
50
  from .generated.models.custom_field_project_with_values_dto import CustomFieldProjectWithValuesDto
51
+ from .generated.models.custom_field_value_project_create_dto import CustomFieldValueProjectCreateDto
52
+ from .generated.models.custom_field_value_project_patch_dto import CustomFieldValueProjectPatchDto
45
53
  from .generated.models.custom_field_value_with_cf_dto import CustomFieldValueWithCfDto
54
+ from .generated.models.custom_field_with_values_dto import CustomFieldWithValuesDto
55
+ from .generated.models.find_all29200_response import FindAll29200Response
56
+ from .generated.models.integration_dto import IntegrationDto
57
+ from .generated.models.issue_dto import IssueDto
58
+ from .generated.models.launch_create_dto import LaunchCreateDto
59
+ from .generated.models.launch_dto import LaunchDto
60
+ from .generated.models.page_custom_field_value_with_tc_count_dto import PageCustomFieldValueWithTcCountDto
61
+ from .generated.models.page_launch_dto import PageLaunchDto
62
+ from .generated.models.page_launch_preview_dto import PageLaunchPreviewDto
46
63
  from .generated.models.page_shared_step_dto import PageSharedStepDto
47
64
  from .generated.models.page_test_case_dto import PageTestCaseDto
48
65
  from .generated.models.scenario_step_create_dto import ScenarioStepCreateDto
49
66
  from .generated.models.scenario_step_created_response_dto import ScenarioStepCreatedResponseDto
67
+ from .generated.models.scenario_step_patch_dto import ScenarioStepPatchDto
50
68
  from .generated.models.shared_step_attachment_row_dto import SharedStepAttachmentRowDto
51
69
  from .generated.models.shared_step_create_dto import SharedStepCreateDto
52
70
  from .generated.models.shared_step_dto import SharedStepDto
@@ -62,13 +80,18 @@ from .generated.models.test_case_row_dto import TestCaseRowDto
62
80
  from .generated.models.test_case_scenario_dto import TestCaseScenarioDto
63
81
  from .generated.models.test_case_scenario_v2_dto import TestCaseScenarioV2Dto
64
82
  from .generated.models.test_case_tree_selection_dto import TestCaseTreeSelectionDto
83
+ from .overridden.test_case_custom_fields_v2 import TestCaseCustomFieldV2ControllerApi
65
84
 
66
85
 
67
86
  # Subclasses to add missing fields to generated models
68
87
  class TestCaseDtoWithCF(TestCaseDto):
69
- """Subclass to support custom_fields access."""
88
+ """Subclass to support custom_fields and issues access."""
70
89
 
71
90
  custom_fields: list[CustomFieldValueWithCfDto] | None = None
91
+ issues: list[IssueDto] | None = None
92
+
93
+
94
+ TestCaseDtoWithCF.model_rebuild()
72
95
 
73
96
 
74
97
  class BodyStepDtoWithSteps(BodyStepDto):
@@ -78,6 +101,14 @@ class BodyStepDtoWithSteps(BodyStepDto):
78
101
  id: int | None = None
79
102
 
80
103
 
104
+ class StepWithExpected(BodyStepDto):
105
+ """Subclass to support expected results and nested steps."""
106
+
107
+ expected_result: str | None = Field(default=None, alias="expectedResult")
108
+ steps: list[SharedStepScenarioDtoStepsInner] | None = None
109
+ id: int | None = None
110
+
111
+
81
112
  class AttachmentStepDtoWithName(AttachmentStepDto):
82
113
  """Subclass to support name attribute and id."""
83
114
 
@@ -104,11 +135,17 @@ type ApiType = (
104
135
  | SharedStepScenarioControllerApi
105
136
  | TestCaseOverviewControllerApi
106
137
  | TestCaseSearchControllerApi
107
- | TestCaseCustomFieldControllerApi
138
+ | TestCaseCustomFieldV2ControllerApi
108
139
  | CustomFieldControllerApi
109
140
  | CustomFieldProjectControllerApi
110
141
  | CustomFieldProjectControllerV2Api
142
+ | CustomFieldValueControllerApi
111
143
  | CustomFieldValueProjectControllerApi
144
+ | TestLayerControllerApi
145
+ | TestLayerSchemaControllerApi
146
+ | LaunchControllerApi
147
+ | LaunchSearchControllerApi
148
+ | IntegrationControllerApi
112
149
  )
113
150
 
114
151
  type NormalizedScenarioDict = dict[str, object]
@@ -123,16 +160,24 @@ __all__ = [
123
160
  "AttachmentStepDtoWithName",
124
161
  "BodyStepDtoWithSteps",
125
162
  "CustomFieldProjectWithValuesDto",
163
+ "CustomFieldWithValuesDto",
164
+ "FindAll29200Response",
165
+ "LaunchCreateDto",
166
+ "LaunchDto",
167
+ "PageLaunchDto",
168
+ "PageLaunchPreviewDto",
126
169
  "PageSharedStepDto",
127
170
  "PageTestCaseDto",
128
171
  "ScenarioStepCreateDto",
129
172
  "ScenarioStepCreatedResponseDto",
173
+ "ScenarioStepPatchDto",
130
174
  "SharedStepAttachmentRowDto",
131
175
  "SharedStepCreateDto",
132
176
  "SharedStepDto",
133
177
  "SharedStepPatchDto",
134
178
  "SharedStepScenarioDtoStepsInner",
135
179
  "SharedStepStepDtoWithId",
180
+ "StepWithExpected",
136
181
  "TestCaseAttachmentRowDto",
137
182
  "TestCaseCreateV2Dto",
138
183
  "TestCaseDto",
@@ -204,11 +249,17 @@ class AllureClient:
204
249
  self._shared_step_scenario_api: SharedStepScenarioControllerApi | None = None
205
250
  self._overview_api: TestCaseOverviewControllerApi
206
251
  self._search_api: TestCaseSearchControllerApi | None = None
207
- self._test_case_custom_field_api: TestCaseCustomFieldControllerApi | None = None
252
+ self._test_case_custom_field_api: TestCaseCustomFieldV2ControllerApi | None = None
208
253
  self._custom_field_api: CustomFieldControllerApi | None = None
209
254
  self._custom_field_project_api: CustomFieldProjectControllerApi | None = None
210
255
  self._custom_field_project_v2_api: CustomFieldProjectControllerV2Api | None = None
256
+ self._custom_field_value_api: CustomFieldValueControllerApi | None = None
211
257
  self._custom_field_value_project_api: CustomFieldValueProjectControllerApi | None = None
258
+ self._test_layer_api: TestLayerControllerApi | None = None
259
+ self._test_layer_schema_api: TestLayerSchemaControllerApi | None = None
260
+ self._launch_api: LaunchControllerApi | None = None
261
+ self._launch_search_api: LaunchSearchControllerApi | None = None
262
+ self._integration_api: IntegrationControllerApi | None = None
212
263
  self._is_entered = False
213
264
 
214
265
  @classmethod
@@ -239,7 +290,7 @@ class AllureClient:
239
290
  if not isinstance(settings.ALLURE_PROJECT_ID, int) or settings.ALLURE_PROJECT_ID <= 0:
240
291
  raise ValueError("ALLURE_PROJECT_ID must be a positive integer")
241
292
 
242
- if project:
293
+ if project is not None:
243
294
  p = project
244
295
  else:
245
296
  p = settings.ALLURE_PROJECT_ID
@@ -345,11 +396,17 @@ class AllureClient:
345
396
  self._shared_step_scenario_api = SharedStepScenarioControllerApi(self._api_client)
346
397
  self._overview_api = TestCaseOverviewControllerApi(self._api_client)
347
398
  self._search_api = TestCaseSearchControllerApi(self._api_client)
348
- self._test_case_custom_field_api = TestCaseCustomFieldControllerApi(self._api_client)
399
+ self._test_case_custom_field_api = TestCaseCustomFieldV2ControllerApi(self._api_client)
349
400
  self._custom_field_api = CustomFieldControllerApi(self._api_client)
350
401
  self._custom_field_project_api = CustomFieldProjectControllerApi(self._api_client)
351
402
  self._custom_field_project_v2_api = CustomFieldProjectControllerV2Api(self._api_client)
403
+ self._custom_field_value_api = CustomFieldValueControllerApi(self._api_client)
352
404
  self._custom_field_value_project_api = CustomFieldValueProjectControllerApi(self._api_client)
405
+ self._test_layer_api = TestLayerControllerApi(self._api_client)
406
+ self._test_layer_schema_api = TestLayerSchemaControllerApi(self._api_client)
407
+ self._launch_api = LaunchControllerApi(self._api_client)
408
+ self._launch_search_api = LaunchSearchControllerApi(self._api_client)
409
+ self._integration_api = IntegrationControllerApi(self._api_client)
353
410
 
354
411
  @property
355
412
  def api_client(self) -> ApiClient:
@@ -362,6 +419,21 @@ class AllureClient:
362
419
  raise RuntimeError("AllureClient must be used as an async context manager")
363
420
  return self._api_client
364
421
 
422
+ async def get_integrations(self) -> list[IntegrationDto]:
423
+ """Fetch all integrations."""
424
+ # Ensure we have a valid client
425
+ if self._integration_api is None:
426
+ raise RuntimeError("AllureClient must be used as an async context manager")
427
+
428
+ try:
429
+ # Fetch first page with reasonable size
430
+ page = await self._integration_api.get_integrations(page=0, size=100)
431
+ return page.content or []
432
+ except Exception:
433
+ # Log warning or re-raise depending on strictness.
434
+ # For now return empty list to act as fallback.
435
+ return []
436
+
365
437
  async def __aenter__(self) -> AllureClient:
366
438
  """Initialize the client session within an async context.
367
439
 
@@ -466,7 +538,7 @@ class AllureClient:
466
538
  @overload
467
539
  async def _get_api(
468
540
  self, attr_name: Literal["_test_case_custom_field_api"], *, error_name: str | None = None
469
- ) -> TestCaseCustomFieldControllerApi: ...
541
+ ) -> TestCaseCustomFieldV2ControllerApi: ...
470
542
 
471
543
  @overload
472
544
  async def _get_api(
@@ -483,11 +555,26 @@ class AllureClient:
483
555
  self, attr_name: Literal["_custom_field_project_v2_api"], *, error_name: str | None = None
484
556
  ) -> CustomFieldProjectControllerV2Api: ...
485
557
 
558
+ @overload
559
+ async def _get_api(
560
+ self, attr_name: Literal["_custom_field_value_api"], *, error_name: str | None = None
561
+ ) -> CustomFieldValueControllerApi: ...
562
+
486
563
  @overload
487
564
  async def _get_api(
488
565
  self, attr_name: Literal["_custom_field_value_project_api"], *, error_name: str | None = None
489
566
  ) -> CustomFieldValueProjectControllerApi: ...
490
567
 
568
+ @overload
569
+ async def _get_api(
570
+ self, attr_name: Literal["_launch_api"], *, error_name: str | None = None
571
+ ) -> LaunchControllerApi: ...
572
+
573
+ @overload
574
+ async def _get_api(
575
+ self, attr_name: Literal["_launch_search_api"], *, error_name: str | None = None
576
+ ) -> LaunchSearchControllerApi: ...
577
+
491
578
  async def _get_api(self, attr_name: str, *, error_name: str | None = None) -> ApiType:
492
579
  self._require_entered()
493
580
  await self._ensure_valid_token()
@@ -693,6 +780,210 @@ class AllureClient:
693
780
  )
694
781
  )
695
782
 
783
+ # ==========================================
784
+ # Launch operations
785
+ # ==========================================
786
+
787
+ async def create_launch(self, data: LaunchCreateDto) -> LaunchDto:
788
+ """Create a new launch in the specified project.
789
+
790
+ Args:
791
+ data: Launch definition (name, project_id, etc.).
792
+
793
+ Returns:
794
+ The created launch.
795
+
796
+ Raises:
797
+ AllureNotFoundError: If project doesn't exist.
798
+ AllureValidationError: If input data fails validation.
799
+ AllureAuthError: If unauthorized.
800
+ AllureAPIError: If the server returns an error.
801
+ """
802
+ api = await self._get_api("_launch_api")
803
+
804
+ if hasattr(data, "project_id") and not data.project_id:
805
+ data.project_id = self._project
806
+
807
+ return await self._call_api(api.create31(launch_create_dto=data, _request_timeout=self._timeout))
808
+
809
+ async def list_launches(
810
+ self,
811
+ project_id: int,
812
+ page: int = 0,
813
+ size: int = 20,
814
+ search: str | None = None,
815
+ filter_id: int | None = None,
816
+ sort: list[str] | None = None,
817
+ ) -> FindAll29200Response:
818
+ """List launches for a project.
819
+
820
+ Args:
821
+ project_id: Target project ID.
822
+ page: Zero-based page index.
823
+ size: Page size.
824
+ search: Optional name search.
825
+ filter_id: Optional filter ID.
826
+ sort: Optional sort criteria.
827
+
828
+ Returns:
829
+ Paginated launches or launch previews.
830
+
831
+ Raises:
832
+ AllureNotFoundError: If project doesn't exist.
833
+ AllureValidationError: If input data fails validation.
834
+ AllureAuthError: If unauthorized.
835
+ AllureAPIError: If the server returns an error.
836
+ """
837
+ api = await self._get_api("_launch_api", error_name="launch APIs")
838
+
839
+ if not isinstance(project_id, int) or project_id <= 0:
840
+ raise AllureValidationError("Project ID must be a positive integer")
841
+ if not isinstance(page, int) or page < 0:
842
+ raise AllureValidationError("Page must be a non-negative integer")
843
+ if not isinstance(size, int) or size <= 0 or size > 100:
844
+ raise AllureValidationError("Size must be between 1 and 100")
845
+
846
+ try:
847
+ return await self._call_api(
848
+ api.find_all29(
849
+ project_id=project_id,
850
+ search=search,
851
+ filter_id=filter_id,
852
+ page=page,
853
+ size=size,
854
+ sort=sort,
855
+ _request_timeout=self._timeout,
856
+ )
857
+ )
858
+ except ValueError:
859
+ response = await self._call_api_raw(
860
+ api.find_all29_without_preload_content(
861
+ project_id=project_id,
862
+ search=search,
863
+ filter_id=filter_id,
864
+ page=page,
865
+ size=size,
866
+ sort=sort,
867
+ _request_timeout=self._timeout,
868
+ )
869
+ )
870
+ data = self._extract_response_data(response)
871
+ try:
872
+ page_data = PageLaunchDto.from_dict(data)
873
+ if page_data is None:
874
+ raise AllureValidationError("Unexpected launch list response from API")
875
+ return FindAll29200Response(page_data)
876
+ except ValidationError as e:
877
+ preview_data = PageLaunchPreviewDto.from_dict(data)
878
+ if preview_data is None:
879
+ raise AllureValidationError("Unexpected launch list response from API") from e
880
+ return FindAll29200Response(preview_data)
881
+
882
+ async def get_launch(self, launch_id: int) -> LaunchDto:
883
+ """Retrieve a specific launch by its ID.
884
+
885
+ Args:
886
+ launch_id: The unique ID of the launch.
887
+
888
+ Returns:
889
+ The launch data.
890
+
891
+ Raises:
892
+ AllureNotFoundError: If launch doesn't exist.
893
+ AllureValidationError: If input is invalid.
894
+ AllureAuthError: If unauthorized.
895
+ AllureAPIError: If the server returns an error.
896
+ """
897
+ api = await self._get_api("_launch_api", error_name="launch APIs")
898
+
899
+ if not isinstance(launch_id, int) or launch_id <= 0:
900
+ raise AllureValidationError("Launch ID must be a positive integer")
901
+
902
+ return await self._call_api(api.find_one23(id=launch_id, _request_timeout=self._timeout))
903
+
904
+ async def search_launches_aql(
905
+ self,
906
+ project_id: int,
907
+ rql: str,
908
+ page: int = 0,
909
+ size: int = 20,
910
+ sort: list[str] | None = None,
911
+ ) -> PageLaunchDto:
912
+ """Search launches using raw AQL (Allure Query Language).
913
+
914
+ Args:
915
+ project_id: Target project ID.
916
+ rql: Raw AQL query string.
917
+ page: Zero-based page index.
918
+ size: Page size (max 100).
919
+ sort: Optional sort criteria (e.g., ["createdDate,DESC"]).
920
+
921
+ Returns:
922
+ Paginated launch results matching the AQL query.
923
+
924
+ Raises:
925
+ AllureValidationError: If AQL syntax is invalid or input fails validation.
926
+ AllureNotFoundError: If project doesn't exist.
927
+ AllureAuthError: If unauthorized.
928
+ AllureAPIError: If the server returns an error.
929
+ """
930
+ api = await self._get_api("_launch_search_api", error_name="launch search APIs")
931
+
932
+ if not isinstance(project_id, int) or project_id <= 0:
933
+ raise AllureValidationError("Project ID must be a positive integer")
934
+ if not isinstance(rql, str) or not rql.strip():
935
+ raise AllureValidationError("AQL query must be a non-empty string")
936
+ if not isinstance(page, int) or page < 0:
937
+ raise AllureValidationError("Page must be a non-negative integer")
938
+ if not isinstance(size, int) or size <= 0 or size > 100:
939
+ raise AllureValidationError("Size must be between 1 and 100")
940
+
941
+ return await self._call_api(
942
+ api.search2(
943
+ project_id=project_id,
944
+ rql=rql,
945
+ page=page,
946
+ size=size,
947
+ sort=sort,
948
+ _request_timeout=self._timeout,
949
+ )
950
+ )
951
+
952
+ async def validate_launch_query(
953
+ self,
954
+ project_id: int,
955
+ rql: str,
956
+ ) -> AqlValidateResponseDto:
957
+ """Validate an AQL query for launches without executing it.
958
+
959
+ Args:
960
+ project_id: Target project ID.
961
+ rql: Raw AQL query string to validate.
962
+
963
+ Returns:
964
+ Validation response with validity and count.
965
+
966
+ Raises:
967
+ AllureValidationError: If input fails basic validation.
968
+ AllureNotFoundError: If project doesn't exist.
969
+ AllureAuthError: If unauthorized.
970
+ AllureAPIError: If the server returns an error.
971
+ """
972
+ api = await self._get_api("_launch_search_api", error_name="launch search APIs")
973
+
974
+ if not isinstance(project_id, int) or project_id <= 0:
975
+ raise AllureValidationError("Project ID must be a positive integer")
976
+ if not isinstance(rql, str) or not rql.strip():
977
+ raise AllureValidationError("AQL query must be a non-empty string")
978
+
979
+ return await self._call_api(
980
+ api.validate_query2(
981
+ project_id=project_id,
982
+ rql=rql,
983
+ _request_timeout=self._timeout,
984
+ )
985
+ )
986
+
696
987
  async def search_test_cases_aql(
697
988
  self,
698
989
  project_id: int,
@@ -867,6 +1158,165 @@ class AllureClient:
867
1158
  with_expected_result=with_expected_result,
868
1159
  )
869
1160
 
1161
+ async def list_custom_field_values(
1162
+ self,
1163
+ project_id: int,
1164
+ custom_field_id: int,
1165
+ *,
1166
+ query: str | None = None,
1167
+ var_global: bool | None = None,
1168
+ test_case_search: str | None = None,
1169
+ page: int | None = None,
1170
+ size: int | None = None,
1171
+ sort: list[str] | None = None,
1172
+ ) -> PageCustomFieldValueWithTcCountDto:
1173
+ """List custom field values for a project field.
1174
+
1175
+ Args:
1176
+ project_id: Target project ID.
1177
+ custom_field_id: Target custom field ID (project-scoped).
1178
+ query: Optional search query.
1179
+ var_global: Optional global flag filter.
1180
+ test_case_search: Optional test case search filter.
1181
+ page: Zero-based page index.
1182
+ size: Page size.
1183
+ sort: Optional sort criteria.
1184
+
1185
+ Returns:
1186
+ Paginated custom field values with test case counts.
1187
+
1188
+ Raises:
1189
+ AllureNotFoundError: If project or custom field doesn't exist.
1190
+ AllureValidationError: If input data fails validation.
1191
+ AllureAuthError: If unauthorized.
1192
+ AllureAPIError: If the server returns an error.
1193
+ """
1194
+ api = await self._get_api("_custom_field_value_project_api")
1195
+
1196
+ if not isinstance(project_id, int) or project_id <= 0:
1197
+ raise AllureValidationError("Project ID must be a positive integer")
1198
+ if not isinstance(custom_field_id, int) or custom_field_id == 0:
1199
+ raise AllureValidationError("Custom Field ID must be a non-zero integer")
1200
+ if page is not None and (not isinstance(page, int) or page < 0):
1201
+ raise AllureValidationError("Page must be a non-negative integer")
1202
+ if size is not None and (not isinstance(size, int) or size <= 0 or size > 1000):
1203
+ raise AllureValidationError("Size must be between 1 and 1000")
1204
+
1205
+ return await self._call_api(
1206
+ api.find_all22(
1207
+ project_id=project_id,
1208
+ custom_field_id=custom_field_id,
1209
+ query=query,
1210
+ var_global=var_global,
1211
+ test_case_search=test_case_search,
1212
+ page=page,
1213
+ size=size,
1214
+ sort=sort,
1215
+ _request_timeout=self._timeout,
1216
+ )
1217
+ )
1218
+
1219
+ async def create_custom_field_value(
1220
+ self, project_id: int, data: CustomFieldValueProjectCreateDto
1221
+ ) -> CustomFieldValueWithCfDto:
1222
+ """Create a custom field value in a project.
1223
+
1224
+ Args:
1225
+ project_id: Target project ID.
1226
+ data: Custom field value payload.
1227
+
1228
+ Returns:
1229
+ The created custom field value DTO.
1230
+
1231
+ Raises:
1232
+ AllureNotFoundError: If project doesn't exist.
1233
+ AllureValidationError: If input data fails validation.
1234
+ AllureAuthError: If unauthorized.
1235
+ AllureAPIError: If the server returns an error.
1236
+ """
1237
+ api = await self._get_api("_custom_field_value_project_api")
1238
+
1239
+ if not isinstance(project_id, int) or project_id <= 0:
1240
+ raise AllureValidationError("Project ID must be a positive integer")
1241
+
1242
+ try:
1243
+ return await self._call_api(
1244
+ api.create26(
1245
+ project_id=project_id,
1246
+ custom_field_value_project_create_dto=data,
1247
+ _request_timeout=self._timeout,
1248
+ )
1249
+ )
1250
+ except AllureAPIError as exc:
1251
+ if exc.status_code == 409:
1252
+ raise AllureValidationError(
1253
+ "Duplicate custom field value name.",
1254
+ status_code=exc.status_code,
1255
+ response_body=exc.response_body,
1256
+ suggestions=["Use a unique custom field value name"],
1257
+ ) from exc
1258
+ raise
1259
+
1260
+ async def update_custom_field_value(
1261
+ self, project_id: int, cfv_id: int, data: CustomFieldValueProjectPatchDto
1262
+ ) -> None:
1263
+ """Update a custom field value in a project.
1264
+
1265
+ Args:
1266
+ project_id: Target project ID.
1267
+ cfv_id: Target custom field value ID.
1268
+ data: Patch payload for the custom field value.
1269
+
1270
+ Raises:
1271
+ AllureNotFoundError: If project or value doesn't exist.
1272
+ AllureValidationError: If input data fails validation.
1273
+ AllureAuthError: If unauthorized.
1274
+ AllureAPIError: If the server returns an error.
1275
+ """
1276
+ api = await self._get_api("_custom_field_value_project_api")
1277
+
1278
+ if not isinstance(project_id, int) or project_id <= 0:
1279
+ raise AllureValidationError("Project ID must be a positive integer")
1280
+ if not isinstance(cfv_id, int) or cfv_id <= 0:
1281
+ raise AllureValidationError("Custom Field Value ID must be a positive integer")
1282
+
1283
+ await self._call_api(
1284
+ api.patch23(
1285
+ project_id=project_id,
1286
+ cfv_id=cfv_id,
1287
+ custom_field_value_project_patch_dto=data,
1288
+ _request_timeout=self._timeout,
1289
+ )
1290
+ )
1291
+
1292
+ async def delete_custom_field_value(self, project_id: int, cfv_id: int) -> None:
1293
+ """Delete a custom field value in a project.
1294
+
1295
+ Args:
1296
+ project_id: Target project ID.
1297
+ cfv_id: Target custom field value ID.
1298
+
1299
+ Raises:
1300
+ AllureNotFoundError: If project or value doesn't exist.
1301
+ AllureValidationError: If input data fails validation.
1302
+ AllureAuthError: If unauthorized.
1303
+ AllureAPIError: If the server returns an error.
1304
+ """
1305
+ api = await self._get_api("_custom_field_value_project_api")
1306
+
1307
+ if not isinstance(project_id, int) or project_id <= 0:
1308
+ raise AllureValidationError("Project ID must be a positive integer")
1309
+ if not isinstance(cfv_id, int) or cfv_id <= 0:
1310
+ raise AllureValidationError("Custom Field Value ID must be a positive integer")
1311
+
1312
+ await self._call_api(
1313
+ api.delete47(
1314
+ project_id=project_id,
1315
+ id=cfv_id,
1316
+ _request_timeout=self._timeout,
1317
+ )
1318
+ )
1319
+
870
1320
  async def get_custom_fields_with_values(self, project_id: int) -> list[CustomFieldProjectWithValuesDto]:
871
1321
  """Fetch all custom fields and their allowed values for a project.
872
1322
 
@@ -923,6 +1373,49 @@ class AllureClient:
923
1373
 
924
1374
  return results
925
1375
 
1376
+ async def get_test_case_custom_fields(
1377
+ self,
1378
+ test_case_id: int,
1379
+ project_id: int,
1380
+ ) -> list[CustomFieldProjectWithValuesDto]:
1381
+ """Fetch custom fields with values for a specific test case.
1382
+
1383
+ Args:
1384
+ test_case_id: Target test case ID.
1385
+ project_id: The project ID context.
1386
+
1387
+ Returns:
1388
+ List of custom field DTOs with their assigned values.
1389
+ """
1390
+ api = await self._get_api("_test_case_custom_field_api")
1391
+ return await self._call_api(
1392
+ api.get_custom_fields_with_values3(
1393
+ test_case_id=test_case_id,
1394
+ project_id=project_id,
1395
+ _request_timeout=self._timeout,
1396
+ )
1397
+ )
1398
+
1399
+ async def update_cfvs_of_test_case(
1400
+ self,
1401
+ test_case_id: int,
1402
+ custom_fields: list[CustomFieldValueWithCfDto],
1403
+ ) -> None:
1404
+ """Update custom field values for a test case.
1405
+
1406
+ Args:
1407
+ test_case_id: Target test case ID.
1408
+ custom_fields: List of custom field DTOs with new values.
1409
+ """
1410
+ api = await self._get_api("_test_case_custom_field_api")
1411
+ await self._call_api(
1412
+ api.update_cfvs_of_test_case(
1413
+ test_case_id=test_case_id,
1414
+ custom_field_with_values_dto=custom_fields,
1415
+ _request_timeout=self._timeout,
1416
+ )
1417
+ )
1418
+
926
1419
  async def delete_scenario_step(self, step_id: int) -> None:
927
1420
  """Delete a scenario step.
928
1421
 
@@ -933,14 +1426,14 @@ class AllureClient:
933
1426
  AllureAPIError: If the API request fails.
934
1427
  """
935
1428
  scenario_api = await self._get_api("_scenario_api")
936
- await self._call_api(
937
- scenario_api.delete_by_id1(
1429
+ await self._call_api_raw(
1430
+ scenario_api.delete_by_id1_without_preload_content(
938
1431
  id=step_id,
939
1432
  _request_timeout=self._timeout,
940
1433
  )
941
1434
  )
942
1435
 
943
- async def get_test_case(self, test_case_id: int) -> TestCaseDto:
1436
+ async def get_test_case(self, test_case_id: int) -> TestCaseDtoWithCF:
944
1437
  """Retrieve a specific test case by its ID.
945
1438
 
946
1439
  Args:
@@ -973,6 +1466,8 @@ class AllureClient:
973
1466
  )
974
1467
  if overview.custom_fields:
975
1468
  case.custom_fields = overview.custom_fields
1469
+ if overview.issues:
1470
+ case.issues = overview.issues
976
1471
  except Exception as e:
977
1472
  logger.warning(f"Failed to fetch overview for test case {test_case_id}: {e}")
978
1473
 
@@ -1041,7 +1536,6 @@ class AllureClient:
1041
1536
  )
1042
1537
  )
1043
1538
  raw_data = self._extract_response_data(response)
1044
- # print(f"DEBUG Initial Raw Normalized Scenario: {raw_data}")
1045
1539
  return self._denormalize_to_v2_from_dict(raw_data)
1046
1540
 
1047
1541
  @staticmethod
@@ -1114,13 +1608,14 @@ class AllureClient:
1114
1608
  child_ids = step_def.get("children") or []
1115
1609
  child_steps = build_steps(child_ids) if child_ids else None
1116
1610
 
1117
- # Build BodyStepDtoWithSteps
1611
+ # Build StepWithExpected
1118
1612
  steps_list.append(
1119
1613
  SharedStepScenarioDtoStepsInner(
1120
- actual_instance=BodyStepDtoWithSteps.model_construct(
1614
+ actual_instance=StepWithExpected.model_construct(
1121
1615
  type="BodyStepDto",
1122
1616
  body=body,
1123
1617
  body_json=None, # Skip complex rich-text
1618
+ expected_result=step_def.get("expectedResult"),
1124
1619
  steps=child_steps,
1125
1620
  id=sid,
1126
1621
  )
@@ -1219,6 +1714,38 @@ class AllureClient:
1219
1714
 
1220
1715
  return await self._create_scenario_step_via_api(shared_step_scenario_api, step)
1221
1716
 
1717
+ async def patch_test_case_scenario_step(
1718
+ self,
1719
+ step_id: int,
1720
+ patch: ScenarioStepPatchDto,
1721
+ ) -> None:
1722
+ """Patch a specific scenario step within a test case."""
1723
+ scenario_api = await self._get_api("_scenario_api")
1724
+ await self._call_api_raw(
1725
+ scenario_api.patch_by_id_without_preload_content(
1726
+ id=step_id,
1727
+ scenario_step_patch_dto=patch,
1728
+ with_expected_result=False,
1729
+ _request_timeout=self._timeout,
1730
+ )
1731
+ )
1732
+
1733
+ async def patch_shared_step_scenario_step(
1734
+ self,
1735
+ step_id: int,
1736
+ patch: ScenarioStepPatchDto,
1737
+ ) -> None:
1738
+ """Patch a specific scenario step within a shared step."""
1739
+ shared_step_scenario_api = await self._get_api("_shared_step_scenario_api")
1740
+ await self._call_api_raw(
1741
+ shared_step_scenario_api.patch_by_id1_without_preload_content(
1742
+ id=step_id,
1743
+ scenario_step_patch_dto=patch,
1744
+ with_expected_result=False,
1745
+ _request_timeout=self._timeout,
1746
+ )
1747
+ )
1748
+
1222
1749
  async def upload_shared_step_attachment(
1223
1750
  self,
1224
1751
  shared_step_id: int,
@@ -1306,3 +1833,21 @@ class AllureClient:
1306
1833
  shared_step_api = await self._get_api("_shared_step_api")
1307
1834
  # Soft delete via archive
1308
1835
  await self._call_api(shared_step_api.archive(id=shared_step_id, _request_timeout=self._timeout))
1836
+
1837
+ async def update_test_case_custom_fields(
1838
+ self, test_case_id: int, custom_fields: list[CustomFieldValueWithCfDto]
1839
+ ) -> None:
1840
+ """Update custom field values of a test case using dedicated endpoint.
1841
+
1842
+ Args:
1843
+ test_case_id: Target test case ID.
1844
+ custom_fields: List of custom fields with values to set/clear.
1845
+ """
1846
+ api = await self._get_api("_test_case_custom_field_api")
1847
+ await self._call_api(
1848
+ api.update_cfvs_of_test_case(
1849
+ test_case_id=test_case_id,
1850
+ custom_field_with_values_dto=custom_fields,
1851
+ _request_timeout=self._timeout,
1852
+ )
1853
+ )