lucius-mcp 0.1.0__py3-none-any.whl → 0.2.2__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 (31) hide show
  1. {lucius_mcp-0.1.0.dist-info → lucius_mcp-0.2.2.dist-info}/METADATA +23 -2
  2. {lucius_mcp-0.1.0.dist-info → lucius_mcp-0.2.2.dist-info}/RECORD +29 -14
  3. src/client/client.py +105 -0
  4. src/client/generated/README.md +65 -7
  5. src/client/generated/__init__.py +16 -0
  6. src/client/generated/api/__init__.py +8 -0
  7. src/client/generated/api/custom_field_controller_api.py +2617 -0
  8. src/client/generated/api/custom_field_project_controller_api.py +2337 -0
  9. src/client/generated/api/custom_field_project_controller_v2_api.py +659 -0
  10. src/client/generated/api/custom_field_schema_controller_api.py +1710 -0
  11. src/client/generated/api/custom_field_value_controller_api.py +3106 -0
  12. src/client/generated/api/custom_field_value_project_controller_api.py +1835 -0
  13. src/client/generated/api/project_controller_api.py +2986 -0
  14. src/client/generated/api/status_controller_api.py +1780 -0
  15. src/client/generated/docs/CustomFieldControllerApi.md +616 -0
  16. src/client/generated/docs/CustomFieldProjectControllerApi.md +554 -0
  17. src/client/generated/docs/CustomFieldProjectControllerV2Api.md +149 -0
  18. src/client/generated/docs/CustomFieldSchemaControllerApi.md +421 -0
  19. src/client/generated/docs/CustomFieldValueControllerApi.md +723 -0
  20. src/client/generated/docs/CustomFieldValueProjectControllerApi.md +430 -0
  21. src/client/generated/docs/LaunchControllerApi.md +2 -2
  22. src/client/generated/docs/ProjectControllerApi.md +717 -0
  23. src/client/generated/docs/StatusControllerApi.md +429 -0
  24. src/services/test_case_service.py +92 -33
  25. src/tools/__init__.py +3 -0
  26. src/tools/get_custom_fields.py +48 -0
  27. src/client/refactor-hotspots.md +0 -53
  28. src/client/refactor-plan.md +0 -78
  29. {lucius_mcp-0.1.0.dist-info → lucius_mcp-0.2.2.dist-info}/WHEEL +0 -0
  30. {lucius_mcp-0.1.0.dist-info → lucius_mcp-0.2.2.dist-info}/entry_points.txt +0 -0
  31. {lucius_mcp-0.1.0.dist-info → lucius_mcp-0.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,429 @@
1
+ # src.client.generated.StatusControllerApi
2
+
3
+ All URIs are relative to *http://localhost*
4
+
5
+ Method | HTTP request | Description
6
+ ------------- | ------------- | -------------
7
+ [**create18**](StatusControllerApi.md#create18) | **POST** /api/status | Create a new status
8
+ [**delete17**](StatusControllerApi.md#delete17) | **DELETE** /api/status/{id} | Delete status by id
9
+ [**find_all15**](StatusControllerApi.md#find_all15) | **GET** /api/status | Find all statuses
10
+ [**find_one14**](StatusControllerApi.md#find_one14) | **GET** /api/status/{id} | Find status by id
11
+ [**patch17**](StatusControllerApi.md#patch17) | **PATCH** /api/status/{id} | Patch status
12
+ [**suggest8**](StatusControllerApi.md#suggest8) | **GET** /api/status/suggest | Suggest statuses
13
+
14
+
15
+ # **create18**
16
+ > StatusDto create18(status_create_dto)
17
+
18
+ Create a new status
19
+
20
+ ### Example
21
+
22
+
23
+ ```python
24
+ import src.client.generated
25
+ from src.client.generated.models.status_create_dto import StatusCreateDto
26
+ from src.client.generated.models.status_dto import StatusDto
27
+ from src.client.generated.rest import ApiException
28
+ from pprint import pprint
29
+
30
+ # Defining the host is optional and defaults to http://localhost
31
+ # See configuration.py for a list of all supported configuration parameters.
32
+ configuration = src.client.generated.Configuration(
33
+ host = "http://localhost"
34
+ )
35
+
36
+
37
+ # Enter a context with an instance of the API client
38
+ async with src.client.generated.ApiClient(configuration) as api_client:
39
+ # Create an instance of the API class
40
+ api_instance = src.client.generated.StatusControllerApi(api_client)
41
+ status_create_dto = src.client.generated.StatusCreateDto() # StatusCreateDto |
42
+
43
+ try:
44
+ # Create a new status
45
+ api_response = await api_instance.create18(status_create_dto)
46
+ print("The response of StatusControllerApi->create18:\n")
47
+ pprint(api_response)
48
+ except Exception as e:
49
+ print("Exception when calling StatusControllerApi->create18: %s\n" % e)
50
+ ```
51
+
52
+
53
+
54
+ ### Parameters
55
+
56
+
57
+ Name | Type | Description | Notes
58
+ ------------- | ------------- | ------------- | -------------
59
+ **status_create_dto** | [**StatusCreateDto**](StatusCreateDto.md)| |
60
+
61
+ ### Return type
62
+
63
+ [**StatusDto**](StatusDto.md)
64
+
65
+ ### Authorization
66
+
67
+ No authorization required
68
+
69
+ ### HTTP request headers
70
+
71
+ - **Content-Type**: application/json
72
+ - **Accept**: */*
73
+
74
+ ### HTTP response details
75
+
76
+ | Status code | Description | Response headers |
77
+ |-------------|-------------|------------------|
78
+ **200** | OK | - |
79
+
80
+ [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
81
+
82
+ # **delete17**
83
+ > delete17(id)
84
+
85
+ Delete status by id
86
+
87
+ ### Example
88
+
89
+
90
+ ```python
91
+ import src.client.generated
92
+ from src.client.generated.rest import ApiException
93
+ from pprint import pprint
94
+
95
+ # Defining the host is optional and defaults to http://localhost
96
+ # See configuration.py for a list of all supported configuration parameters.
97
+ configuration = src.client.generated.Configuration(
98
+ host = "http://localhost"
99
+ )
100
+
101
+
102
+ # Enter a context with an instance of the API client
103
+ async with src.client.generated.ApiClient(configuration) as api_client:
104
+ # Create an instance of the API class
105
+ api_instance = src.client.generated.StatusControllerApi(api_client)
106
+ id = 56 # int |
107
+
108
+ try:
109
+ # Delete status by id
110
+ await api_instance.delete17(id)
111
+ except Exception as e:
112
+ print("Exception when calling StatusControllerApi->delete17: %s\n" % e)
113
+ ```
114
+
115
+
116
+
117
+ ### Parameters
118
+
119
+
120
+ Name | Type | Description | Notes
121
+ ------------- | ------------- | ------------- | -------------
122
+ **id** | **int**| |
123
+
124
+ ### Return type
125
+
126
+ void (empty response body)
127
+
128
+ ### Authorization
129
+
130
+ No authorization required
131
+
132
+ ### HTTP request headers
133
+
134
+ - **Content-Type**: Not defined
135
+ - **Accept**: Not defined
136
+
137
+ ### HTTP response details
138
+
139
+ | Status code | Description | Response headers |
140
+ |-------------|-------------|------------------|
141
+ **204** | No Content | - |
142
+
143
+ [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
144
+
145
+ # **find_all15**
146
+ > PageStatusDto find_all15(workflow_id=workflow_id, page=page, size=size, sort=sort)
147
+
148
+ Find all statuses
149
+
150
+ ### Example
151
+
152
+
153
+ ```python
154
+ import src.client.generated
155
+ from src.client.generated.models.page_status_dto import PageStatusDto
156
+ from src.client.generated.rest import ApiException
157
+ from pprint import pprint
158
+
159
+ # Defining the host is optional and defaults to http://localhost
160
+ # See configuration.py for a list of all supported configuration parameters.
161
+ configuration = src.client.generated.Configuration(
162
+ host = "http://localhost"
163
+ )
164
+
165
+
166
+ # Enter a context with an instance of the API client
167
+ async with src.client.generated.ApiClient(configuration) as api_client:
168
+ # Create an instance of the API class
169
+ api_instance = src.client.generated.StatusControllerApi(api_client)
170
+ workflow_id = 56 # int | (optional)
171
+ page = 0 # int | Zero-based page index (0..N) (optional) (default to 0)
172
+ size = 10 # int | The size of the page to be returned (optional) (default to 10)
173
+ sort = [name,ASC] # List[str] | Sorting criteria in the format: property(,asc|desc). Default sort order is ascending. Multiple sort criteria are supported. (optional) (default to [name,ASC])
174
+
175
+ try:
176
+ # Find all statuses
177
+ api_response = await api_instance.find_all15(workflow_id=workflow_id, page=page, size=size, sort=sort)
178
+ print("The response of StatusControllerApi->find_all15:\n")
179
+ pprint(api_response)
180
+ except Exception as e:
181
+ print("Exception when calling StatusControllerApi->find_all15: %s\n" % e)
182
+ ```
183
+
184
+
185
+
186
+ ### Parameters
187
+
188
+
189
+ Name | Type | Description | Notes
190
+ ------------- | ------------- | ------------- | -------------
191
+ **workflow_id** | **int**| | [optional]
192
+ **page** | **int**| Zero-based page index (0..N) | [optional] [default to 0]
193
+ **size** | **int**| The size of the page to be returned | [optional] [default to 10]
194
+ **sort** | [**List[str]**](str.md)| Sorting criteria in the format: property(,asc|desc). Default sort order is ascending. Multiple sort criteria are supported. | [optional] [default to [name,ASC]]
195
+
196
+ ### Return type
197
+
198
+ [**PageStatusDto**](PageStatusDto.md)
199
+
200
+ ### Authorization
201
+
202
+ No authorization required
203
+
204
+ ### HTTP request headers
205
+
206
+ - **Content-Type**: Not defined
207
+ - **Accept**: */*
208
+
209
+ ### HTTP response details
210
+
211
+ | Status code | Description | Response headers |
212
+ |-------------|-------------|------------------|
213
+ **200** | OK | - |
214
+
215
+ [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
216
+
217
+ # **find_one14**
218
+ > StatusDto find_one14(id)
219
+
220
+ Find status by id
221
+
222
+ ### Example
223
+
224
+
225
+ ```python
226
+ import src.client.generated
227
+ from src.client.generated.models.status_dto import StatusDto
228
+ from src.client.generated.rest import ApiException
229
+ from pprint import pprint
230
+
231
+ # Defining the host is optional and defaults to http://localhost
232
+ # See configuration.py for a list of all supported configuration parameters.
233
+ configuration = src.client.generated.Configuration(
234
+ host = "http://localhost"
235
+ )
236
+
237
+
238
+ # Enter a context with an instance of the API client
239
+ async with src.client.generated.ApiClient(configuration) as api_client:
240
+ # Create an instance of the API class
241
+ api_instance = src.client.generated.StatusControllerApi(api_client)
242
+ id = 56 # int |
243
+
244
+ try:
245
+ # Find status by id
246
+ api_response = await api_instance.find_one14(id)
247
+ print("The response of StatusControllerApi->find_one14:\n")
248
+ pprint(api_response)
249
+ except Exception as e:
250
+ print("Exception when calling StatusControllerApi->find_one14: %s\n" % e)
251
+ ```
252
+
253
+
254
+
255
+ ### Parameters
256
+
257
+
258
+ Name | Type | Description | Notes
259
+ ------------- | ------------- | ------------- | -------------
260
+ **id** | **int**| |
261
+
262
+ ### Return type
263
+
264
+ [**StatusDto**](StatusDto.md)
265
+
266
+ ### Authorization
267
+
268
+ No authorization required
269
+
270
+ ### HTTP request headers
271
+
272
+ - **Content-Type**: Not defined
273
+ - **Accept**: */*
274
+
275
+ ### HTTP response details
276
+
277
+ | Status code | Description | Response headers |
278
+ |-------------|-------------|------------------|
279
+ **200** | OK | - |
280
+
281
+ [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
282
+
283
+ # **patch17**
284
+ > StatusDto patch17(id, status_patch_dto)
285
+
286
+ Patch status
287
+
288
+ ### Example
289
+
290
+
291
+ ```python
292
+ import src.client.generated
293
+ from src.client.generated.models.status_dto import StatusDto
294
+ from src.client.generated.models.status_patch_dto import StatusPatchDto
295
+ from src.client.generated.rest import ApiException
296
+ from pprint import pprint
297
+
298
+ # Defining the host is optional and defaults to http://localhost
299
+ # See configuration.py for a list of all supported configuration parameters.
300
+ configuration = src.client.generated.Configuration(
301
+ host = "http://localhost"
302
+ )
303
+
304
+
305
+ # Enter a context with an instance of the API client
306
+ async with src.client.generated.ApiClient(configuration) as api_client:
307
+ # Create an instance of the API class
308
+ api_instance = src.client.generated.StatusControllerApi(api_client)
309
+ id = 56 # int |
310
+ status_patch_dto = src.client.generated.StatusPatchDto() # StatusPatchDto |
311
+
312
+ try:
313
+ # Patch status
314
+ api_response = await api_instance.patch17(id, status_patch_dto)
315
+ print("The response of StatusControllerApi->patch17:\n")
316
+ pprint(api_response)
317
+ except Exception as e:
318
+ print("Exception when calling StatusControllerApi->patch17: %s\n" % e)
319
+ ```
320
+
321
+
322
+
323
+ ### Parameters
324
+
325
+
326
+ Name | Type | Description | Notes
327
+ ------------- | ------------- | ------------- | -------------
328
+ **id** | **int**| |
329
+ **status_patch_dto** | [**StatusPatchDto**](StatusPatchDto.md)| |
330
+
331
+ ### Return type
332
+
333
+ [**StatusDto**](StatusDto.md)
334
+
335
+ ### Authorization
336
+
337
+ No authorization required
338
+
339
+ ### HTTP request headers
340
+
341
+ - **Content-Type**: application/json
342
+ - **Accept**: */*
343
+
344
+ ### HTTP response details
345
+
346
+ | Status code | Description | Response headers |
347
+ |-------------|-------------|------------------|
348
+ **200** | OK | - |
349
+
350
+ [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
351
+
352
+ # **suggest8**
353
+ > PageIdAndNameOnlyDto suggest8(query=query, workflow_id=workflow_id, id=id, ignore_id=ignore_id, page=page, size=size, sort=sort)
354
+
355
+ Suggest statuses
356
+
357
+ ### Example
358
+
359
+
360
+ ```python
361
+ import src.client.generated
362
+ from src.client.generated.models.page_id_and_name_only_dto import PageIdAndNameOnlyDto
363
+ from src.client.generated.rest import ApiException
364
+ from pprint import pprint
365
+
366
+ # Defining the host is optional and defaults to http://localhost
367
+ # See configuration.py for a list of all supported configuration parameters.
368
+ configuration = src.client.generated.Configuration(
369
+ host = "http://localhost"
370
+ )
371
+
372
+
373
+ # Enter a context with an instance of the API client
374
+ async with src.client.generated.ApiClient(configuration) as api_client:
375
+ # Create an instance of the API class
376
+ api_instance = src.client.generated.StatusControllerApi(api_client)
377
+ query = 'query_example' # str | (optional)
378
+ workflow_id = 56 # int | (optional)
379
+ id = [56] # List[int] | (optional)
380
+ ignore_id = [56] # List[int] | (optional)
381
+ page = 0 # int | Zero-based page index (0..N) (optional) (default to 0)
382
+ size = 10 # int | The size of the page to be returned (optional) (default to 10)
383
+ sort = [name,ASC] # List[str] | Sorting criteria in the format: property(,asc|desc). Default sort order is ascending. Multiple sort criteria are supported. (optional) (default to [name,ASC])
384
+
385
+ try:
386
+ # Suggest statuses
387
+ api_response = await api_instance.suggest8(query=query, workflow_id=workflow_id, id=id, ignore_id=ignore_id, page=page, size=size, sort=sort)
388
+ print("The response of StatusControllerApi->suggest8:\n")
389
+ pprint(api_response)
390
+ except Exception as e:
391
+ print("Exception when calling StatusControllerApi->suggest8: %s\n" % e)
392
+ ```
393
+
394
+
395
+
396
+ ### Parameters
397
+
398
+
399
+ Name | Type | Description | Notes
400
+ ------------- | ------------- | ------------- | -------------
401
+ **query** | **str**| | [optional]
402
+ **workflow_id** | **int**| | [optional]
403
+ **id** | [**List[int]**](int.md)| | [optional]
404
+ **ignore_id** | [**List[int]**](int.md)| | [optional]
405
+ **page** | **int**| Zero-based page index (0..N) | [optional] [default to 0]
406
+ **size** | **int**| The size of the page to be returned | [optional] [default to 10]
407
+ **sort** | [**List[str]**](str.md)| Sorting criteria in the format: property(,asc|desc). Default sort order is ascending. Multiple sort criteria are supported. | [optional] [default to [name,ASC]]
408
+
409
+ ### Return type
410
+
411
+ [**PageIdAndNameOnlyDto**](PageIdAndNameOnlyDto.md)
412
+
413
+ ### Authorization
414
+
415
+ No authorization required
416
+
417
+ ### HTTP request headers
418
+
419
+ - **Content-Type**: Not defined
420
+ - **Accept**: */*
421
+
422
+ ### HTTP response details
423
+
424
+ | Status code | Description | Response headers |
425
+ |-------------|-------------|------------------|
426
+ **200** | OK | - |
427
+
428
+ [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
429
+
@@ -19,7 +19,6 @@ from src.client.generated.models import (
19
19
  TestCaseCreateV2Dto,
20
20
  TestCaseDto,
21
21
  TestCaseOverviewDto,
22
- TestCaseTreeSelectionDto,
23
22
  TestTagDto,
24
23
  )
25
24
  from src.client.generated.models.attachment_step_dto import AttachmentStepDto
@@ -80,9 +79,10 @@ class TestCaseService:
80
79
  self._client = client
81
80
  self._project_id = client.get_project()
82
81
  self._attachment_service = attachment_service or AttachmentService(self._client)
83
- self._cf_cache: dict[int, dict[str, int]] = {} # {project_id: {name: id}}
82
+ # {project_id: {name: {"id": int, "values": list[str]}}}
83
+ self._cf_cache: dict[int, dict[str, dict[str, Any]]] = {}
84
84
 
85
- async def create_test_case(
85
+ async def create_test_case( # noqa: C901
86
86
  self,
87
87
  name: str,
88
88
  description: str | None = None,
@@ -120,15 +120,46 @@ class TestCaseService:
120
120
  resolved_custom_fields = []
121
121
  if custom_fields:
122
122
  project_cfs = await self._get_resolved_custom_fields(self._project_id)
123
+ missing_fields = []
124
+ invalid_values = []
125
+
123
126
  for key, value in custom_fields.items():
124
- cf_id = project_cfs.get(key)
125
- if cf_id is None:
126
- raise AllureValidationError(f"Custom field '{key}' not found in project {self._project_id}.")
127
+ cf_info = project_cfs.get(key)
128
+ if cf_info is None:
129
+ missing_fields.append(key)
130
+ else:
131
+ cf_id = cf_info["id"]
132
+ allowed_values = cf_info["values"]
133
+
134
+ # Validate value if allowed_values are present
135
+ if allowed_values and value not in allowed_values:
136
+ invalid_values.append(f"'{key}': '{value}' (Allowed: {', '.join(allowed_values)})")
137
+ else:
138
+ resolved_custom_fields.append(
139
+ CustomFieldValueWithCfDto(custom_field=CustomFieldDto(id=cf_id, name=key), name=value)
140
+ )
127
141
 
128
- resolved_custom_fields.append(
129
- CustomFieldValueWithCfDto(custom_field=CustomFieldDto(id=cf_id, name=key), name=value)
142
+ error_messages = []
143
+
144
+ if missing_fields:
145
+ missing_list_str = "\n".join([f"- {name}" for name in missing_fields])
146
+ error_messages.append(
147
+ f"The following custom fields were not found in project {self._project_id}:\n{missing_list_str}"
130
148
  )
131
149
 
150
+ if invalid_values:
151
+ invalid_list_str = "\n".join([f"- {item}" for item in invalid_values])
152
+ error_messages.append(f"The following custom field values are invalid:\n{invalid_list_str}")
153
+
154
+ if error_messages:
155
+ full_error_msg = "\n\n".join(error_messages) + (
156
+ "\n\nUsage Hint:\n"
157
+ "1. Exclude all missing custom fields from your request.\n"
158
+ "2. Correct any invalid values to match the allowed options.\n"
159
+ "3. Only include fields that explicitly exist in the project configuration."
160
+ )
161
+ raise AllureValidationError(full_error_msg)
162
+
132
163
  # 3. Create TestCaseCreateV2Dto with validation
133
164
  tag_dtos = self._build_tag_dtos(tags)
134
165
  try:
@@ -185,6 +216,27 @@ class TestCaseService:
185
216
  """
186
217
  return await self._client.get_test_case(test_case_id)
187
218
 
219
+ async def get_custom_fields(self, name: str | None = None) -> list[dict[str, Any]]:
220
+ """Get custom fields for the project with optional name filtering.
221
+
222
+ This method uses the internal cache to avoid duplicate API calls when
223
+ both get_custom_fields and create_test_case are used in the same session.
224
+ """
225
+ # Use cached resolution method to get field mapping
226
+ cf_mapping = await self._get_resolved_custom_fields(self._project_id)
227
+
228
+ result = []
229
+ filter_name = name.lower() if name else None
230
+
231
+ # Convert the cached mapping back to the display format
232
+ for field_name, field_info in cf_mapping.items():
233
+ if filter_name and filter_name not in field_name.lower():
234
+ continue
235
+
236
+ result.append({"name": field_name, "required": field_info["required"], "values": field_info["values"]})
237
+
238
+ return result
239
+
188
240
  async def update_test_case(self, test_case_id: int, data: TestCaseUpdate) -> TestCaseDto:
189
241
  """Update an existing test case.
190
242
 
@@ -423,10 +475,17 @@ class TestCaseService:
423
475
  resolved_cfs = []
424
476
  project_cfs = await self._get_resolved_custom_fields(project_id)
425
477
  for key, value in data.custom_fields.items():
426
- cf_id = project_cfs.get(key)
427
- if cf_id:
478
+ cf_info = project_cfs.get(key)
479
+ if cf_info:
480
+ # For updates, we blindly trust if it exists, or should we validate?
481
+ # The plan implies validating create, let's also validate update to be safe,
482
+ # but typically update is just "prepare kwargs".
483
+ # If we want validation here, we should add it.
484
+ # For now, adapting to the new dict structure is required.
428
485
  resolved_cfs.append(
429
- CustomFieldValueWithCfDto(custom_field=CustomFieldDto(id=cf_id, name=key), name=value)
486
+ CustomFieldValueWithCfDto(
487
+ custom_field=CustomFieldDto(id=cf_info["id"], name=key), name=value
488
+ )
430
489
  )
431
490
  patch_kwargs["custom_fields"] = resolved_cfs
432
491
  has_changes = True
@@ -695,31 +754,31 @@ class TestCaseService:
695
754
  raise AllureValidationError(f"Invalid tag '{t}': {e}", suggestions=[hint]) from e
696
755
  return tag_dtos
697
756
 
698
- async def _get_resolved_custom_fields(self, project_id: int) -> dict[str, int]:
699
- """Get or fetch custom field name-to-id mapping for a project."""
757
+ async def _get_resolved_custom_fields(self, project_id: int) -> dict[str, dict[str, Any]]:
758
+ """Get or fetch custom field name-to-info mapping for a project."""
700
759
  if project_id in self._cf_cache:
701
760
  return self._cf_cache[project_id]
702
761
 
703
- try:
704
- from src.client.generated.api.test_case_custom_field_controller_api import TestCaseCustomFieldControllerApi
705
-
706
- api = TestCaseCustomFieldControllerApi(self._client.api_client)
707
- selection = TestCaseTreeSelectionDto(project_id=project_id)
708
- cfs = await api.get_custom_fields_with_values2(test_case_tree_selection_dto=selection)
709
-
710
- mapping = {}
711
- for cf_with_values in cfs:
712
- if cf_with_values.custom_field and cf_with_values.custom_field.custom_field:
713
- inner_cf = cf_with_values.custom_field.custom_field
714
- if inner_cf.name and inner_cf.id:
715
- mapping[inner_cf.name] = inner_cf.id
716
-
717
- self._cf_cache[project_id] = mapping
718
- return mapping
719
- except Exception as e:
720
- # If we fail to fetch CFs, we might want to warn or raise.
721
- # For now, let's raise as it's critical for resolving names to IDs.
722
- raise AllureValidationError(f"Failed to fetch custom fields for project {project_id}: {e}") from e
762
+ # Use the client wrapper method for consistent error handling and response processing
763
+ cfs = await self._client.get_custom_fields_with_values(project_id)
764
+ logger.debug("Fetched %d custom fields for project %d", len(cfs), project_id)
765
+ mapping = {}
766
+ for cf_with_values in cfs:
767
+ if cf_with_values.custom_field and cf_with_values.custom_field.custom_field:
768
+ inner_cf = cf_with_values.custom_field.custom_field
769
+ if inner_cf.name and inner_cf.id:
770
+ values = []
771
+ if cf_with_values.values:
772
+ values = [v.name for v in cf_with_values.values if v.name]
773
+
774
+ mapping[inner_cf.name] = {
775
+ "id": inner_cf.id,
776
+ "required": bool(cf_with_values.custom_field.required),
777
+ "values": values,
778
+ }
779
+
780
+ self._cf_cache[project_id] = mapping
781
+ return mapping
723
782
 
724
783
  def _build_custom_field_dtos(self, custom_fields: dict[str, str] | None) -> list[CustomFieldValueWithCfDto]:
725
784
  """DEPRECATED: Use inline resolution in create_test_case."""
src/tools/__init__.py CHANGED
@@ -2,6 +2,7 @@ from collections.abc import Awaitable, Callable
2
2
 
3
3
  from src.tools.create_test_case import create_test_case
4
4
  from src.tools.delete_test_case import delete_test_case
5
+ from src.tools.get_custom_fields import get_custom_fields
5
6
  from src.tools.link_shared_step import link_shared_step
6
7
  from src.tools.search import get_test_case_details, list_test_cases, search_test_cases
7
8
  from src.tools.shared_steps import create_shared_step, delete_shared_step, list_shared_steps, update_shared_step
@@ -13,6 +14,7 @@ __all__ = [
13
14
  "create_test_case",
14
15
  "delete_shared_step",
15
16
  "delete_test_case",
17
+ "get_custom_fields",
16
18
  "get_test_case_details",
17
19
  "link_shared_step",
18
20
  "list_shared_steps",
@@ -31,6 +33,7 @@ all_tools: list[ToolFn] = [
31
33
  update_test_case,
32
34
  delete_test_case,
33
35
  list_test_cases,
36
+ get_custom_fields,
34
37
  search_test_cases,
35
38
  create_shared_step,
36
39
  list_shared_steps,
@@ -0,0 +1,48 @@
1
+ from typing import Annotated
2
+
3
+ from pydantic import Field
4
+
5
+ from src.client import AllureClient
6
+ from src.services.test_case_service import TestCaseService
7
+
8
+
9
+ async def get_custom_fields(
10
+ name: Annotated[
11
+ str | None, Field(description="Optional case-insensitive name filter to search for specific custom fields.")
12
+ ] = None,
13
+ project_id: Annotated[
14
+ int | None, Field(description="Allure TestOps project ID to fetch custom fields from.")
15
+ ] = None,
16
+ ) -> str:
17
+ """Get available custom fields and their allowed values for the project.
18
+
19
+ Use this tool to discover what custom fields are available (e.g., 'Layer', 'Priority')
20
+ and what values are valid for them (e.g., 'UI', 'High'). This is essential before
21
+ creating or updating test cases to ensure you use valid field names and values.
22
+
23
+ Args:
24
+ name: Optional name filter to find a specific field (case-insensitive).
25
+ project_id: Optional project ID override.
26
+
27
+ Returns:
28
+ A list of custom fields with their required status and allowed values.
29
+ """
30
+ async with AllureClient.from_env(project=project_id) as client:
31
+ service = TestCaseService(client)
32
+ fields = await service.get_custom_fields(name=name)
33
+
34
+ if not fields:
35
+ if name:
36
+ return f"No custom fields found matching '{name}'."
37
+ return "No custom fields found for this project."
38
+
39
+ lines = [f"Found {len(fields)} custom fields:"]
40
+
41
+ for cf in fields:
42
+ field_name = cf["name"]
43
+ required = "required" if cf["required"] else "optional"
44
+ values = ", ".join(cf["values"]) if cf["values"] else "Any text/No allowed values"
45
+
46
+ lines.append(f"- {field_name} ({required}): {values}")
47
+
48
+ return "\n".join(lines)