alita-sdk 0.3.155__py3-none-any.whl → 0.3.157__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.
@@ -1,14 +1,16 @@
1
+ import copy
1
2
  import json
2
3
  import logging
3
4
  import re
4
- from typing import Any, Dict, List, Optional, Union
5
+ from typing import Any, Dict, List, Optional, Tuple
5
6
  from traceback import format_exc
6
7
 
7
8
  import requests
8
9
  from langchain_core.tools import ToolException
9
- from pydantic import Field, PrivateAttr, model_validator, create_model, SecretStr
10
+ from pydantic import Field, model_validator, create_model, SecretStr
10
11
 
11
12
  from ..elitea_base import BaseToolApiWrapper
13
+ from .postman_analysis import PostmanAnalyzer
12
14
 
13
15
  logger = logging.getLogger(__name__)
14
16
 
@@ -18,21 +20,17 @@ PostmanGetCollections = create_model(
18
20
  )
19
21
 
20
22
  PostmanGetCollection = create_model(
21
- "PostmanGetCollection",
22
- collection_id=(str, Field(
23
- description="The ID of the collection to retrieve"))
23
+ "PostmanGetCollection"
24
24
  )
25
25
 
26
26
  PostmanGetFolder = create_model(
27
27
  "PostmanGetFolder",
28
- collection_id=(str, Field(description="The ID of the collection")),
29
28
  folder_path=(str, Field(
30
29
  description="The path to the folder (e.g., 'API/Users' for nested folders)"))
31
30
  )
32
31
 
33
32
  PostmanGetFolderRequests = create_model(
34
33
  "PostmanGetFolderRequests",
35
- collection_id=(str, Field(description="The ID of the collection")),
36
34
  folder_path=(str, Field(description="The path to the folder")),
37
35
  include_details=(bool, Field(
38
36
  description="Include detailed request information", default=False))
@@ -40,8 +38,6 @@ PostmanGetFolderRequests = create_model(
40
38
 
41
39
  PostmanSearchRequests = create_model(
42
40
  "PostmanSearchRequests",
43
- collection_id=(str, Field(
44
- description="The ID of the collection to search in")),
45
41
  query=(str, Field(description="The search query")),
46
42
  search_in=(str, Field(
47
43
  description="Where to search: name, url, description, all", default="all")),
@@ -51,20 +47,22 @@ PostmanSearchRequests = create_model(
51
47
 
52
48
  PostmanAnalyzeCollection = create_model(
53
49
  "PostmanAnalyzeCollection",
54
- collection_id=(str, Field(
55
- description="The ID of the collection to analyze"))
50
+ include_improvements=(bool, Field(
51
+ description="Include improvement suggestions in the analysis", default=False))
56
52
  )
57
53
 
58
54
  PostmanAnalyzeFolder = create_model(
59
55
  "PostmanAnalyzeFolder",
60
- collection_id=(str, Field(description="The ID of the collection")),
61
- folder_path=(str, Field(description="The path to the folder to analyze"))
56
+ folder_path=(str, Field(description="The path to the folder to analyze")),
57
+ include_improvements=(bool, Field(
58
+ description="Include improvement suggestions in the analysis", default=False))
62
59
  )
63
60
 
64
- PostmanGetImprovementSuggestions = create_model(
65
- "PostmanGetImprovementSuggestions",
66
- collection_id=(str, Field(
67
- description="The ID of the collection to get improvements for"))
61
+ PostmanAnalyzeRequest = create_model(
62
+ "PostmanAnalyzeRequest",
63
+ request_path=(str, Field(description="The path to the request to analyze")),
64
+ include_improvements=(bool, Field(
65
+ description="Include improvement suggestions in the analysis", default=False))
68
66
  )
69
67
 
70
68
  PostmanCreateCollection = create_model(
@@ -78,36 +76,37 @@ PostmanCreateCollection = create_model(
78
76
  description="Optional default authentication", default=None))
79
77
  )
80
78
 
81
- PostmanUpdateCollection = create_model(
82
- "PostmanUpdateCollection",
83
- collection_id=(str, Field(
84
- description="The ID of the collection to update")),
85
- name=(Optional[str], Field(
86
- description="New name for the collection", default=None)),
87
- description=(Optional[str], Field(
88
- description="New description for the collection", default=None)),
89
- variables=(Optional[List[Dict]], Field(
90
- description="Updated collection variables", default=None)),
91
- auth=(Optional[Dict], Field(
92
- description="Updated authentication settings", default=None))
79
+ PostmanUpdateCollectionName = create_model(
80
+ "PostmanUpdateCollectionName",
81
+ name=(str, Field(description="New name for the collection"))
82
+ )
83
+
84
+ PostmanUpdateCollectionDescription = create_model(
85
+ "PostmanUpdateCollectionDescription",
86
+ description=(str, Field(description="New description for the collection"))
87
+ )
88
+
89
+ PostmanUpdateCollectionVariables = create_model(
90
+ "PostmanUpdateCollectionVariables",
91
+ variables=(List[Dict], Field(description="Updated collection variables"))
92
+ )
93
+
94
+ PostmanUpdateCollectionAuth = create_model(
95
+ "PostmanUpdateCollectionAuth",
96
+ auth=(Dict, Field(description="Updated authentication settings"))
93
97
  )
94
98
 
95
99
  PostmanDeleteCollection = create_model(
96
- "PostmanDeleteCollection",
97
- collection_id=(str, Field(
98
- description="The ID of the collection to delete"))
100
+ "PostmanDeleteCollection"
99
101
  )
100
102
 
101
103
  PostmanDuplicateCollection = create_model(
102
104
  "PostmanDuplicateCollection",
103
- collection_id=(str, Field(
104
- description="The ID of the collection to duplicate")),
105
105
  new_name=(str, Field(description="Name for the new collection copy"))
106
106
  )
107
107
 
108
108
  PostmanCreateFolder = create_model(
109
109
  "PostmanCreateFolder",
110
- collection_id=(str, Field(description="The ID of the collection")),
111
110
  name=(str, Field(description="Name of the new folder")),
112
111
  description=(Optional[str], Field(
113
112
  description="Optional description for the folder", default=None)),
@@ -119,7 +118,6 @@ PostmanCreateFolder = create_model(
119
118
 
120
119
  PostmanUpdateFolder = create_model(
121
120
  "PostmanUpdateFolder",
122
- collection_id=(str, Field(description="The ID of the collection")),
123
121
  folder_path=(str, Field(description="Path to the folder to update")),
124
122
  name=(Optional[str], Field(
125
123
  description="New name for the folder", default=None)),
@@ -131,13 +129,11 @@ PostmanUpdateFolder = create_model(
131
129
 
132
130
  PostmanDeleteFolder = create_model(
133
131
  "PostmanDeleteFolder",
134
- collection_id=(str, Field(description="The ID of the collection")),
135
132
  folder_path=(str, Field(description="Path to the folder to delete"))
136
133
  )
137
134
 
138
135
  PostmanMoveFolder = create_model(
139
136
  "PostmanMoveFolder",
140
- collection_id=(str, Field(description="The ID of the collection")),
141
137
  source_path=(str, Field(description="Current path of the folder to move")),
142
138
  target_path=(Optional[str], Field(
143
139
  description="New parent folder path", default=None))
@@ -145,7 +141,6 @@ PostmanMoveFolder = create_model(
145
141
 
146
142
  PostmanCreateRequest = create_model(
147
143
  "PostmanCreateRequest",
148
- collection_id=(str, Field(description="The ID of the collection")),
149
144
  folder_path=(Optional[str], Field(
150
145
  description="Path to the folder", default=None)),
151
146
  name=(str, Field(description="Name of the new request")),
@@ -165,38 +160,67 @@ PostmanCreateRequest = create_model(
165
160
  description="Optional pre-request script code", default=None))
166
161
  )
167
162
 
168
- PostmanUpdateRequest = create_model(
169
- "PostmanUpdateRequest",
170
- collection_id=(str, Field(description="The ID of the collection")),
171
- request_path=(str, Field(
172
- description="Path to the request (folder/requestName)")),
173
- name=(Optional[str], Field(
174
- description="New name for the request", default=None)),
175
- method=(Optional[str], Field(
176
- description="HTTP method for the request", default=None)),
177
- url=(Optional[str], Field(
178
- description="URL for the request", default=None)),
179
- description=(Optional[str], Field(
180
- description="Description for the request", default=None)),
181
- headers=(Optional[List[Dict]], Field(
182
- description="Request headers", default=None)),
183
- body=(Optional[Dict], Field(description="Request body", default=None)),
184
- auth=(Optional[Dict], Field(
185
- description="Request authentication", default=None)),
186
- tests=(Optional[str], Field(description="Test script code", default=None)),
187
- pre_request_script=(Optional[str], Field(
188
- description="Pre-request script code", default=None))
163
+ PostmanUpdateRequestName = create_model(
164
+ "PostmanUpdateRequestName",
165
+ request_path=(str, Field(description="Path to the request (folder/requestName)")),
166
+ name=(str, Field(description="New name for the request"))
167
+ )
168
+
169
+ PostmanUpdateRequestMethod = create_model(
170
+ "PostmanUpdateRequestMethod",
171
+ request_path=(str, Field(description="Path to the request (folder/requestName)")),
172
+ method=(str, Field(description="HTTP method for the request"))
173
+ )
174
+
175
+ PostmanUpdateRequestUrl = create_model(
176
+ "PostmanUpdateRequestUrl",
177
+ request_path=(str, Field(description="Path to the request (folder/requestName)")),
178
+ url=(str, Field(description="URL for the request"))
179
+ )
180
+
181
+ PostmanUpdateRequestDescription = create_model(
182
+ "PostmanUpdateRequestDescription",
183
+ request_path=(str, Field(description="Path to the request (folder/requestName)")),
184
+ description=(str, Field(description="Description for the request"))
185
+ )
186
+
187
+ PostmanUpdateRequestHeaders = create_model(
188
+ "PostmanUpdateRequestHeaders",
189
+ request_path=(str, Field(description="Path to the request (folder/requestName)")),
190
+ headers=(List[Dict], Field(description="Request headers"))
191
+ )
192
+
193
+ PostmanUpdateRequestBody = create_model(
194
+ "PostmanUpdateRequestBody",
195
+ request_path=(str, Field(description="Path to the request (folder/requestName)")),
196
+ body=(Dict, Field(description="Request body"))
197
+ )
198
+
199
+ PostmanUpdateRequestAuth = create_model(
200
+ "PostmanUpdateRequestAuth",
201
+ request_path=(str, Field(description="Path to the request (folder/requestName)")),
202
+ auth=(Dict, Field(description="Request authentication"))
203
+ )
204
+
205
+ PostmanUpdateRequestTests = create_model(
206
+ "PostmanUpdateRequestTests",
207
+ request_path=(str, Field(description="Path to the request (folder/requestName)")),
208
+ tests=(str, Field(description="Test script code"))
209
+ )
210
+
211
+ PostmanUpdateRequestPreScript = create_model(
212
+ "PostmanUpdateRequestPreScript",
213
+ request_path=(str, Field(description="Path to the request (folder/requestName)")),
214
+ pre_request_script=(str, Field(description="Pre-request script code"))
189
215
  )
190
216
 
191
217
  PostmanDeleteRequest = create_model(
192
218
  "PostmanDeleteRequest",
193
- collection_id=(str, Field(description="The ID of the collection")),
194
219
  request_path=(str, Field(description="Path to the request to delete"))
195
220
  )
196
221
 
197
222
  PostmanDuplicateRequest = create_model(
198
223
  "PostmanDuplicateRequest",
199
- collection_id=(str, Field(description="The ID of the collection")),
200
224
  source_path=(str, Field(description="Path to the request to duplicate")),
201
225
  new_name=(str, Field(description="Name for the duplicated request")),
202
226
  target_path=(Optional[str], Field(
@@ -205,13 +229,24 @@ PostmanDuplicateRequest = create_model(
205
229
 
206
230
  PostmanMoveRequest = create_model(
207
231
  "PostmanMoveRequest",
208
- collection_id=(str, Field(description="The ID of the collection")),
209
232
  source_path=(str, Field(
210
233
  description="Current path of the request to move")),
211
234
  target_path=(Optional[str], Field(
212
235
  description="New folder path", default=None))
213
236
  )
214
237
 
238
+ PostmanGetRequest = create_model(
239
+ "PostmanGetRequest",
240
+ request_path=(str, Field(
241
+ description="The path to the request (e.g., 'API/Users/Get User' or 'applications/recommendations')"))
242
+ )
243
+
244
+ PostmanGetRequestScript = create_model(
245
+ "PostmanGetRequestScript",
246
+ request_path=(str, Field(description="Path to the request (folder/requestName)")),
247
+ script_type=(str, Field(description="The type of script to retrieve: 'test' or 'prerequest'", default="prerequest"))
248
+ )
249
+
215
250
 
216
251
  class PostmanApiWrapper(BaseToolApiWrapper):
217
252
  """Wrapper for Postman API."""
@@ -221,7 +256,12 @@ class PostmanApiWrapper(BaseToolApiWrapper):
221
256
  collection_id: Optional[str] = None
222
257
  workspace_id: Optional[str] = None
223
258
  timeout: int = 30
224
- _session: requests.Session = PrivateAttr()
259
+ session: Any = None
260
+ analyzer: PostmanAnalyzer = None
261
+
262
+ model_config = {
263
+ "arbitrary_types_allowed": True
264
+ }
225
265
 
226
266
  @model_validator(mode='before')
227
267
  @classmethod
@@ -233,16 +273,14 @@ class PostmanApiWrapper(BaseToolApiWrapper):
233
273
  "`requests` package not found, please run "
234
274
  "`pip install requests`"
235
275
  )
276
+ values["session"] = requests.Session()
277
+ values["session"].headers.update({
278
+ 'X-API-Key': values.get('api_key'),
279
+ 'Content-Type': 'application/json'
280
+ })
281
+ values["analyzer"] = PostmanAnalyzer()
236
282
  return values
237
283
 
238
- def __init__(self, **data):
239
- super().__init__(**data)
240
- self._session = requests.Session()
241
- self._session.headers.update({
242
- 'X-API-Key': self.api_key.get_secret_value(),
243
- 'Content-Type': 'application/json',
244
- })
245
- # Removed ineffective timeout assignment. Timeout will be enforced in `_make_request`.
246
284
 
247
285
  def _make_request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
248
286
  """Make HTTP request to Postman API."""
@@ -250,7 +288,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
250
288
 
251
289
  try:
252
290
  logger.info(f"Making {method.upper()} request to {url}")
253
- response = self._session.request(method, url, timeout=self.timeout, **kwargs)
291
+ response = self.session.request(method, url, timeout=self.timeout, **kwargs)
254
292
  response.raise_for_status()
255
293
 
256
294
  if response.content:
@@ -258,8 +296,14 @@ class PostmanApiWrapper(BaseToolApiWrapper):
258
296
  return {}
259
297
 
260
298
  except requests.exceptions.RequestException as e:
261
- logger.error(f"Request failed: {e}")
262
- raise ToolException(f"Postman API request failed: {str(e)}")
299
+ error_details = ""
300
+ if hasattr(e, 'response') and e.response is not None:
301
+ try:
302
+ error_details = f" Response content: {e.response.text}"
303
+ except:
304
+ error_details = f" Response status: {e.response.status_code}"
305
+ logger.error(f"Request failed: {e}{error_details}")
306
+ raise ToolException(f"Postman API request failed: {str(e)}{error_details}")
263
307
  except json.JSONDecodeError as e:
264
308
  logger.error(f"Failed to decode JSON response: {e}")
265
309
  raise ToolException(
@@ -296,6 +340,20 @@ class PostmanApiWrapper(BaseToolApiWrapper):
296
340
  "args_schema": PostmanGetFolderRequests,
297
341
  "ref": self.get_folder_requests
298
342
  },
343
+ {
344
+ "name": "get_request",
345
+ "mode": "get_request",
346
+ "description": "Get a specific request by path",
347
+ "args_schema": PostmanGetRequest,
348
+ "ref": self.get_request
349
+ },
350
+ {
351
+ "name": "get_request_script",
352
+ "mode": "get_request_script",
353
+ "description": "Get the test or pre-request script content for a specific request",
354
+ "args_schema": PostmanGetRequestScript,
355
+ "ref": self.get_request_script
356
+ },
299
357
  {
300
358
  "name": "search_requests",
301
359
  "mode": "search_requests",
@@ -318,11 +376,11 @@ class PostmanApiWrapper(BaseToolApiWrapper):
318
376
  "ref": self.analyze_folder
319
377
  },
320
378
  {
321
- "name": "get_improvement_suggestions",
322
- "mode": "get_improvement_suggestions",
323
- "description": "Get improvement suggestions for a collection",
324
- "args_schema": PostmanGetImprovementSuggestions,
325
- "ref": self.get_improvement_suggestions
379
+ "name": "analyze_request",
380
+ "mode": "analyze_request",
381
+ "description": "Analyze a specific request within a collection",
382
+ "args_schema": PostmanAnalyzeRequest,
383
+ "ref": self.analyze_request
326
384
  },
327
385
  {
328
386
  "name": "create_collection",
@@ -332,19 +390,40 @@ class PostmanApiWrapper(BaseToolApiWrapper):
332
390
  "ref": self.create_collection
333
391
  },
334
392
  {
335
- "name": "update_collection",
336
- "mode": "update_collection",
337
- "description": "Update an existing collection (name, description, variables, auth)",
338
- "args_schema": PostmanUpdateCollection,
339
- "ref": self.update_collection
393
+ "name": "update_collection_name",
394
+ "mode": "update_collection_name",
395
+ "description": "Update collection name",
396
+ "args_schema": PostmanUpdateCollectionName,
397
+ "ref": self.update_collection_name
398
+ },
399
+ {
400
+ "name": "update_collection_description",
401
+ "mode": "update_collection_description",
402
+ "description": "Update collection description",
403
+ "args_schema": PostmanUpdateCollectionDescription,
404
+ "ref": self.update_collection_description
405
+ },
406
+ {
407
+ "name": "update_collection_variables",
408
+ "mode": "update_collection_variables",
409
+ "description": "Update collection variables",
410
+ "args_schema": PostmanUpdateCollectionVariables,
411
+ "ref": self.update_collection_variables
340
412
  },
341
413
  {
342
- "name": "delete_collection",
343
- "mode": "delete_collection",
344
- "description": "Delete a collection permanently",
345
- "args_schema": PostmanDeleteCollection,
346
- "ref": self.delete_collection
414
+ "name": "update_collection_auth",
415
+ "mode": "update_collection_auth",
416
+ "description": "Update collection authentication settings",
417
+ "args_schema": PostmanUpdateCollectionAuth,
418
+ "ref": self.update_collection_auth
347
419
  },
420
+ # {
421
+ # "name": "delete_collection",
422
+ # "mode": "delete_collection",
423
+ # "description": "Delete a collection permanently",
424
+ # "args_schema": PostmanDeleteCollection,
425
+ # "ref": self.delete_collection
426
+ # },
348
427
  {
349
428
  "name": "duplicate_collection",
350
429
  "mode": "duplicate_collection",
@@ -367,13 +446,13 @@ class PostmanApiWrapper(BaseToolApiWrapper):
367
446
  "args_schema": PostmanUpdateFolder,
368
447
  "ref": self.update_folder
369
448
  },
370
- {
371
- "name": "delete_folder",
372
- "mode": "delete_folder",
373
- "description": "Delete a folder and all its contents permanently",
374
- "args_schema": PostmanDeleteFolder,
375
- "ref": self.delete_folder
376
- },
449
+ # {
450
+ # "name": "delete_folder",
451
+ # "mode": "delete_folder",
452
+ # "description": "Delete a folder and all its contents permanently",
453
+ # "args_schema": PostmanDeleteFolder,
454
+ # "ref": self.delete_folder
455
+ # },
377
456
  {
378
457
  "name": "move_folder",
379
458
  "mode": "move_folder",
@@ -389,19 +468,75 @@ class PostmanApiWrapper(BaseToolApiWrapper):
389
468
  "ref": self.create_request
390
469
  },
391
470
  {
392
- "name": "update_request",
393
- "mode": "update_request",
394
- "description": "Update an existing API request",
395
- "args_schema": PostmanUpdateRequest,
396
- "ref": self.update_request
471
+ "name": "update_request_name",
472
+ "mode": "update_request_name",
473
+ "description": "Update request name",
474
+ "args_schema": PostmanUpdateRequestName,
475
+ "ref": self.update_request_name
476
+ },
477
+ {
478
+ "name": "update_request_method",
479
+ "mode": "update_request_method",
480
+ "description": "Update request HTTP method",
481
+ "args_schema": PostmanUpdateRequestMethod,
482
+ "ref": self.update_request_method
397
483
  },
398
484
  {
399
- "name": "delete_request",
400
- "mode": "delete_request",
401
- "description": "Delete an API request permanently",
402
- "args_schema": PostmanDeleteRequest,
403
- "ref": self.delete_request
485
+ "name": "update_request_url",
486
+ "mode": "update_request_url",
487
+ "description": "Update request URL",
488
+ "args_schema": PostmanUpdateRequestUrl,
489
+ "ref": self.update_request_url
404
490
  },
491
+ {
492
+ "name": "update_request_description",
493
+ "mode": "update_request_description",
494
+ "description": "Update request description",
495
+ "args_schema": PostmanUpdateRequestDescription,
496
+ "ref": self.update_request_description
497
+ },
498
+ {
499
+ "name": "update_request_headers",
500
+ "mode": "update_request_headers",
501
+ "description": "Update request headers",
502
+ "args_schema": PostmanUpdateRequestHeaders,
503
+ "ref": self.update_request_headers
504
+ },
505
+ {
506
+ "name": "update_request_body",
507
+ "mode": "update_request_body",
508
+ "description": "Update request body",
509
+ "args_schema": PostmanUpdateRequestBody,
510
+ "ref": self.update_request_body
511
+ },
512
+ {
513
+ "name": "update_request_auth",
514
+ "mode": "update_request_auth",
515
+ "description": "Update request authentication",
516
+ "args_schema": PostmanUpdateRequestAuth,
517
+ "ref": self.update_request_auth
518
+ },
519
+ {
520
+ "name": "update_request_tests",
521
+ "mode": "update_request_tests",
522
+ "description": "Update request test scripts",
523
+ "args_schema": PostmanUpdateRequestTests,
524
+ "ref": self.update_request_tests
525
+ },
526
+ {
527
+ "name": "update_request_pre_script",
528
+ "mode": "update_request_pre_script",
529
+ "description": "Update request pre-request scripts",
530
+ "args_schema": PostmanUpdateRequestPreScript,
531
+ "ref": self.update_request_pre_script
532
+ },
533
+ # {
534
+ # "name": "delete_request",
535
+ # "mode": "delete_request",
536
+ # "description": "Delete an API request permanently",
537
+ # "args_schema": PostmanDeleteRequest,
538
+ # "ref": self.delete_request
539
+ # },
405
540
  {
406
541
  "name": "duplicate_request",
407
542
  "mode": "duplicate_request",
@@ -432,25 +567,25 @@ class PostmanApiWrapper(BaseToolApiWrapper):
432
567
  logger.error(f"Exception when getting collections: {stacktrace}")
433
568
  raise ToolException(f"Unable to get collections: {str(e)}")
434
569
 
435
- def get_collection(self, collection_id: str, **kwargs) -> str:
570
+ def get_collection(self, **kwargs) -> str:
436
571
  """Get a specific collection by ID."""
437
572
  try:
438
573
  response = self._make_request(
439
- 'GET', f'/collections/{collection_id}')
574
+ 'GET', f'/collections/{self.collection_id}')
440
575
  return json.dumps(response, indent=2)
441
576
  except Exception as e:
442
577
  stacktrace = format_exc()
443
578
  logger.error(
444
- f"Exception when getting collection {collection_id}: {stacktrace}")
579
+ f"Exception when getting collection {self.collection_id}: {stacktrace}")
445
580
  raise ToolException(
446
- f"Unable to get collection {collection_id}: {str(e)}")
581
+ f"Unable to get collection {self.collection_id}: {str(e)}")
447
582
 
448
- def get_folder(self, collection_id: str, folder_path: str, **kwargs) -> str:
583
+ def get_folder(self, folder_path: str, **kwargs) -> str:
449
584
  """Get folders from a collection by path."""
450
585
  try:
451
586
  collection = self._make_request(
452
- 'GET', f'/collections/{collection_id}')
453
- folders = self._find_folders_by_path(
587
+ 'GET', f'/collections/{self.collection_id}')
588
+ folders = self.analyzer.find_folders_by_path(
454
589
  collection['collection']['item'], folder_path)
455
590
  return json.dumps(folders, indent=2)
456
591
  except Exception as e:
@@ -458,21 +593,21 @@ class PostmanApiWrapper(BaseToolApiWrapper):
458
593
  logger.error(
459
594
  f"Exception when getting folder {folder_path}: {stacktrace}")
460
595
  raise ToolException(
461
- f"Unable to get folder {folder_path} from collection {collection_id}: {str(e)}")
596
+ f"Unable to get folder {folder_path} from collection {self.collection_id}: {str(e)}")
462
597
 
463
- def get_folder_requests(self, collection_id: str, folder_path: str, include_details: bool = False, **kwargs) -> str:
598
+ def get_folder_requests(self, folder_path: str, include_details: bool = False, **kwargs) -> str:
464
599
  """Get detailed information about all requests in a folder."""
465
600
  try:
466
601
  collection = self._make_request(
467
- 'GET', f'/collections/{collection_id}')
468
- folders = self._find_folders_by_path(
602
+ 'GET', f'/collections/{self.collection_id}')
603
+ folders = self.analyzer.find_folders_by_path(
469
604
  collection['collection']['item'], folder_path)
470
605
 
471
606
  if not folders:
472
- raise ToolException(f"Folder '{folder_path}' not found in collection '{collection_id}'.")
607
+ raise ToolException(f"Folder '{folder_path}' not found in collection '{self.collection_id}'.")
473
608
 
474
609
  folder = folders[0]
475
- requests = self._extract_requests_from_items(
610
+ requests = self.analyzer.extract_requests_from_items(
476
611
  folder.get('item', []), include_details)
477
612
 
478
613
  result = {
@@ -490,16 +625,16 @@ class PostmanApiWrapper(BaseToolApiWrapper):
490
625
  raise ToolException(
491
626
  f"Unable to get requests from folder {folder_path}: {str(e)}")
492
627
 
493
- def search_requests(self, collection_id: str, query: str, search_in: str = "all", method: str = None, **kwargs) -> str:
628
+ def search_requests(self, query: str, search_in: str = "all", method: str = None, **kwargs) -> str:
494
629
  """Search for requests across the collection."""
495
630
  try:
496
631
  collection = self._make_request(
497
- 'GET', f'/collections/{collection_id}')
498
- requests = self._search_requests_in_items(
632
+ 'GET', f'/collections/{self.collection_id}')
633
+ requests = self.analyzer.search_requests_in_items(
499
634
  collection['collection']['item'], query, search_in, method)
500
635
 
501
636
  result = {
502
- "collection_id": collection_id,
637
+ "collection_id": self.collection_id,
503
638
  "query": query,
504
639
  "search_in": search_in,
505
640
  "method_filter": method,
@@ -512,27 +647,33 @@ class PostmanApiWrapper(BaseToolApiWrapper):
512
647
  stacktrace = format_exc()
513
648
  logger.error(f"Exception when searching requests: {stacktrace}")
514
649
  raise ToolException(
515
- f"Unable to search requests in collection {collection_id}: {str(e)}")
650
+ f"Unable to search requests in collection {self.collection_id}: {str(e)}")
516
651
 
517
- def analyze_collection(self, collection_id: str, **kwargs) -> str:
652
+ def analyze_collection(self, include_improvements: bool = False, **kwargs) -> str:
518
653
  """Analyze a collection for API quality and best practices."""
519
654
  try:
520
655
  collection = self._make_request(
521
- 'GET', f'/collections/{collection_id}')
522
- analysis = self._perform_collection_analysis(collection)
656
+ 'GET', f'/collections/{self.collection_id}')
657
+ analysis = self.analyzer.perform_collection_analysis(collection)
658
+
659
+ if include_improvements:
660
+ improvements = self.analyzer.generate_improvements(analysis)
661
+ analysis["improvements"] = improvements
662
+ analysis["improvement_count"] = len(improvements)
663
+
523
664
  return json.dumps(analysis, indent=2)
524
665
  except Exception as e:
525
666
  stacktrace = format_exc()
526
667
  logger.error(f"Exception when analyzing collection: {stacktrace}")
527
668
  raise ToolException(
528
- f"Unable to analyze collection {collection_id}: {str(e)}")
669
+ f"Unable to analyze collection {self.collection_id}: {str(e)}")
529
670
 
530
- def analyze_folder(self, collection_id: str, folder_path: str, **kwargs) -> str:
671
+ def analyze_folder(self, folder_path: str, include_improvements: bool = False, **kwargs) -> str:
531
672
  """Analyze a specific folder within a collection."""
532
673
  try:
533
674
  collection = self._make_request(
534
- 'GET', f'/collections/{collection_id}')
535
- folders = self._find_folders_by_path(
675
+ 'GET', f'/collections/{self.collection_id}')
676
+ folders = self.analyzer.find_folders_by_path(
536
677
  collection['collection']['item'], folder_path)
537
678
 
538
679
  if not folders:
@@ -540,7 +681,13 @@ class PostmanApiWrapper(BaseToolApiWrapper):
540
681
 
541
682
  folder_analyses = []
542
683
  for folder in folders:
543
- analysis = self._perform_folder_analysis(folder, folder_path)
684
+ analysis = self.analyzer.perform_folder_analysis(folder, folder_path)
685
+
686
+ if include_improvements:
687
+ improvements = self.analyzer.generate_folder_improvements(analysis)
688
+ analysis["improvements"] = improvements
689
+ analysis["improvement_count"] = len(improvements)
690
+
544
691
  folder_analyses.append(analysis)
545
692
 
546
693
  return json.dumps(folder_analyses, indent=2)
@@ -550,28 +697,35 @@ class PostmanApiWrapper(BaseToolApiWrapper):
550
697
  raise ToolException(
551
698
  f"Unable to analyze folder {folder_path}: {str(e)}")
552
699
 
553
- def get_improvement_suggestions(self, collection_id: str, **kwargs) -> str:
554
- """Get improvement suggestions for a collection."""
700
+ def analyze_request(self, request_path: str, include_improvements: bool = False, **kwargs) -> str:
701
+ """Analyze a specific request within a collection."""
555
702
  try:
556
703
  collection = self._make_request(
557
- 'GET', f'/collections/{collection_id}')
558
- analysis = self._perform_collection_analysis(collection)
559
- improvements = self._generate_improvements(analysis)
704
+ 'GET', f'/collections/{self.collection_id}')
705
+ collection_data = collection["collection"]
560
706
 
561
- result = {
562
- "collection_id": collection_id,
563
- "collection_name": analysis["collection_name"],
564
- "improvement_count": len(improvements),
565
- "improvements": improvements
566
- }
707
+ # Find the request
708
+ request_item = self.analyzer.find_request_by_path(
709
+ collection_data["item"], request_path)
710
+ if not request_item:
711
+ raise ToolException(f"Request '{request_path}' not found")
567
712
 
568
- return json.dumps(result, indent=2)
713
+ # Perform request analysis
714
+ analysis = self.analyzer.perform_request_analysis(request_item)
715
+ analysis["request_path"] = request_path
716
+ analysis["collection_id"] = self.collection_id
717
+
718
+ if include_improvements:
719
+ improvements = self.analyzer.generate_request_improvements(analysis)
720
+ analysis["improvements"] = improvements
721
+ analysis["improvement_count"] = len(improvements)
722
+
723
+ return json.dumps(analysis, indent=2)
569
724
  except Exception as e:
570
725
  stacktrace = format_exc()
571
- logger.error(
572
- f"Exception when generating improvements: {stacktrace}")
726
+ logger.error(f"Exception when analyzing request: {stacktrace}")
573
727
  raise ToolException(
574
- f"Unable to generate improvements for collection {collection_id}: {str(e)}")
728
+ f"Unable to analyze request {request_path}: {str(e)}")
575
729
 
576
730
  # =================================================================
577
731
  # COLLECTION MANAGEMENT METHODS
@@ -608,52 +762,104 @@ class PostmanApiWrapper(BaseToolApiWrapper):
608
762
  raise ToolException(
609
763
  f"Unable to create collection '{name}': {str(e)}")
610
764
 
611
- def update_collection(self, collection_id: str, name: str = None, description: str = None,
612
- variables: List[Dict] = None, auth: Dict = None, **kwargs) -> str:
613
- """Update an existing collection."""
765
+ def update_collection_name(self, name: str, **kwargs) -> str:
766
+ """Update collection name."""
614
767
  try:
615
768
  # Get current collection
616
769
  current = self._make_request(
617
- 'GET', f'/collections/{collection_id}')
770
+ 'GET', f'/collections/{self.collection_id}')
618
771
  collection_data = current["collection"]
619
772
 
620
- # Update fields if provided
621
- if name:
622
- collection_data["info"]["name"] = name
623
- if description is not None:
624
- collection_data["info"]["description"] = description
625
- if variables is not None:
626
- collection_data["variable"] = variables
627
- if auth is not None:
628
- collection_data["auth"] = auth
773
+ # Update name
774
+ collection_data["info"]["name"] = name
775
+
776
+ response = self._make_request('PUT', f'/collections/{self.collection_id}',
777
+ json={"collection": collection_data})
778
+ return json.dumps(response, indent=2)
779
+ except Exception as e:
780
+ stacktrace = format_exc()
781
+ logger.error(f"Exception when updating collection name: {stacktrace}")
782
+ raise ToolException(
783
+ f"Unable to update collection {self.collection_id} name: {str(e)}")
784
+
785
+ def update_collection_description(self, description: str, **kwargs) -> str:
786
+ """Update collection description."""
787
+ try:
788
+ # Get current collection
789
+ current = self._make_request(
790
+ 'GET', f'/collections/{self.collection_id}')
791
+ collection_data = current["collection"]
792
+
793
+ # Update description
794
+ collection_data["info"]["description"] = description
795
+
796
+ response = self._make_request('PUT', f'/collections/{self.collection_id}',
797
+ json={"collection": collection_data})
798
+ return json.dumps(response, indent=2)
799
+ except Exception as e:
800
+ stacktrace = format_exc()
801
+ logger.error(f"Exception when updating collection description: {stacktrace}")
802
+ raise ToolException(
803
+ f"Unable to update collection {self.collection_id} description: {str(e)}")
804
+
805
+ def update_collection_variables(self, variables: List[Dict], **kwargs) -> str:
806
+ """Update collection variables."""
807
+ try:
808
+ # Get current collection
809
+ current = self._make_request(
810
+ 'GET', f'/collections/{self.collection_id}')
811
+ collection_data = current["collection"]
812
+
813
+ # Update variables
814
+ collection_data["variable"] = variables
815
+
816
+ response = self._make_request('PUT', f'/collections/{self.collection_id}',
817
+ json={"collection": collection_data})
818
+ return json.dumps(response, indent=2)
819
+ except Exception as e:
820
+ stacktrace = format_exc()
821
+ logger.error(f"Exception when updating collection variables: {stacktrace}")
822
+ raise ToolException(
823
+ f"Unable to update collection {self.collection_id} variables: {str(e)}")
629
824
 
630
- response = self._make_request('PUT', f'/collections/{collection_id}',
825
+ def update_collection_auth(self, auth: Dict, **kwargs) -> str:
826
+ """Update collection authentication settings."""
827
+ try:
828
+ # Get current collection
829
+ current = self._make_request(
830
+ 'GET', f'/collections/{self.collection_id}')
831
+ collection_data = current["collection"]
832
+
833
+ # Update auth
834
+ collection_data["auth"] = auth
835
+
836
+ response = self._make_request('PUT', f'/collections/{self.collection_id}',
631
837
  json={"collection": collection_data})
632
838
  return json.dumps(response, indent=2)
633
839
  except Exception as e:
634
840
  stacktrace = format_exc()
635
- logger.error(f"Exception when updating collection: {stacktrace}")
841
+ logger.error(f"Exception when updating collection auth: {stacktrace}")
636
842
  raise ToolException(
637
- f"Unable to update collection {collection_id}: {str(e)}")
843
+ f"Unable to update collection {self.collection_id} auth: {str(e)}")
638
844
 
639
- def delete_collection(self, collection_id: str, **kwargs) -> str:
845
+ def delete_collection(self, **kwargs) -> str:
640
846
  """Delete a collection permanently."""
641
847
  try:
642
848
  response = self._make_request(
643
- 'DELETE', f'/collections/{collection_id}')
644
- return json.dumps({"message": f"Collection {collection_id} deleted successfully"}, indent=2)
849
+ 'DELETE', f'/collections/{self.collection_id}')
850
+ return json.dumps({"message": f"Collection {self.collection_id} deleted successfully"}, indent=2)
645
851
  except Exception as e:
646
852
  stacktrace = format_exc()
647
853
  logger.error(f"Exception when deleting collection: {stacktrace}")
648
854
  raise ToolException(
649
- f"Unable to delete collection {collection_id}: {str(e)}")
855
+ f"Unable to delete collection {self.collection_id}: {str(e)}")
650
856
 
651
- def duplicate_collection(self, collection_id: str, new_name: str, **kwargs) -> str:
857
+ def duplicate_collection(self, new_name: str, **kwargs) -> str:
652
858
  """Create a copy of an existing collection."""
653
859
  try:
654
860
  # Get the original collection
655
861
  original = self._make_request(
656
- 'GET', f'/collections/{collection_id}')
862
+ 'GET', f'/collections/{self.collection_id}')
657
863
  collection_data = original["collection"]
658
864
 
659
865
  # Update the name and remove IDs to create a new collection
@@ -662,7 +868,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
662
868
  del collection_data["info"]["_postman_id"]
663
869
 
664
870
  # Remove item IDs recursively
665
- self._remove_item_ids(collection_data.get("item", []))
871
+ self.analyzer.remove_item_ids(collection_data.get("item", []))
666
872
 
667
873
  response = self._make_request(
668
874
  'POST', '/collections', json={"collection": collection_data})
@@ -672,19 +878,19 @@ class PostmanApiWrapper(BaseToolApiWrapper):
672
878
  logger.error(
673
879
  f"Exception when duplicating collection: {stacktrace}")
674
880
  raise ToolException(
675
- f"Unable to duplicate collection {collection_id}: {str(e)}")
881
+ f"Unable to duplicate collection {self.collection_id}: {str(e)}")
676
882
 
677
883
  # =================================================================
678
884
  # FOLDER MANAGEMENT METHODS
679
885
  # =================================================================
680
886
 
681
- def create_folder(self, collection_id: str, name: str, description: str = None,
887
+ def create_folder(self, name: str, description: str = None,
682
888
  parent_path: str = None, auth: Dict = None, **kwargs) -> str:
683
889
  """Create a new folder in a collection."""
684
890
  try:
685
891
  # Get current collection
686
892
  collection = self._make_request(
687
- 'GET', f'/collections/{collection_id}')
893
+ 'GET', f'/collections/{self.collection_id}')
688
894
  collection_data = collection["collection"]
689
895
 
690
896
  # Create folder item
@@ -700,7 +906,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
700
906
 
701
907
  # Add folder to appropriate location
702
908
  if parent_path:
703
- parent_folders = self._find_folders_by_path(
909
+ parent_folders = self.analyzer.find_folders_by_path(
704
910
  collection_data["item"], parent_path)
705
911
  if not parent_folders:
706
912
  raise ToolException(
@@ -710,7 +916,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
710
916
  collection_data["item"].append(folder_item)
711
917
 
712
918
  # Update collection
713
- response = self._make_request('PUT', f'/collections/{collection_id}',
919
+ response = self._make_request('PUT', f'/collections/{self.collection_id}',
714
920
  json={"collection": collection_data})
715
921
  return json.dumps({"message": f"Folder '{name}' created successfully"}, indent=2)
716
922
  except Exception as e:
@@ -718,53 +924,78 @@ class PostmanApiWrapper(BaseToolApiWrapper):
718
924
  logger.error(f"Exception when creating folder: {stacktrace}")
719
925
  raise ToolException(f"Unable to create folder '{name}': {str(e)}")
720
926
 
721
- def update_folder(self, collection_id: str, folder_path: str, name: str = None,
722
- description: str = None, auth: Dict = None, **kwargs) -> str:
723
- """Update folder properties."""
724
- try:
725
- # Get current collection
726
- collection = self._make_request(
727
- 'GET', f'/collections/{collection_id}')
728
- collection_data = collection["collection"]
927
+ def _get_folder_id(self, folder_path: str) -> str:
928
+ """Helper method to get folder ID by path."""
929
+ collection = self._make_request(
930
+ 'GET', f'/collections/{self.collection_id}')
931
+ collection_data = collection["collection"]
729
932
 
730
- # Find the folder
731
- folders = self._find_folders_by_path(
732
- collection_data["item"], folder_path)
733
- if not folders:
734
- raise ToolException(f"Folder '{folder_path}' not found")
933
+ # Find the folder
934
+ folders = self.analyzer.find_folders_by_path(
935
+ collection_data["item"], folder_path)
936
+ if not folders:
937
+ raise ToolException(f"Folder '{folder_path}' not found")
735
938
 
736
- folder = folders[0]
939
+ folder = folders[0]
940
+
941
+ # Get the folder ID
942
+ folder_id = folder.get("id")
943
+ if not folder_id:
944
+ # If ID is not available directly, try to use the item ID
945
+ if "_postman_id" in folder:
946
+ folder_id = folder["_postman_id"]
947
+ else:
948
+ raise ToolException(f"Folder ID not found for '{folder_path}'")
949
+
950
+ return folder_id
737
951
 
738
- # Update fields if provided
952
+ def update_folder(self, folder_path: str, name: str = None,
953
+ description: str = None, auth: Dict = None, **kwargs) -> str:
954
+ """Update folder properties using the direct folder endpoint."""
955
+ try:
956
+ # Get the folder ID
957
+ folder_id = self._get_folder_id(folder_path)
958
+
959
+ # Create update payload
960
+ folder_update = {}
739
961
  if name:
740
- folder["name"] = name
962
+ folder_update["name"] = name
741
963
  if description is not None:
742
- folder["description"] = description
964
+ folder_update["description"] = description
743
965
  if auth is not None:
744
- folder["auth"] = auth
745
-
746
- # Update collection
747
- response = self._make_request('PUT', f'/collections/{collection_id}',
748
- json={"collection": collection_data})
749
- return json.dumps({"message": f"Folder '{folder_path}' updated successfully"}, indent=2)
966
+ folder_update["auth"] = auth
967
+
968
+ # Only update if we have properties to change
969
+ if folder_update:
970
+ # Update folder using the direct API endpoint
971
+ response = self._make_request('PUT', f'/collections/{self.collection_id}/folders/{folder_id}',
972
+ json=folder_update)
973
+ return json.dumps({"success": True, "message": f"Folder '{folder_path}' updated successfully"}, indent=2)
974
+ else:
975
+ return json.dumps({"success": True, "message": f"No changes requested for folder '{folder_path}'"}, indent=2)
976
+ except Exception as e:
977
+ stacktrace = format_exc()
978
+ logger.error(f"Exception when updating folder: {stacktrace}")
979
+ raise ToolException(
980
+ f"Unable to update folder '{folder_path}': {str(e)}")
750
981
  except Exception as e:
751
982
  stacktrace = format_exc()
752
983
  logger.error(f"Exception when updating folder: {stacktrace}")
753
984
  raise ToolException(
754
985
  f"Unable to update folder '{folder_path}': {str(e)}")
755
986
 
756
- def delete_folder(self, collection_id: str, folder_path: str, **kwargs) -> str:
987
+ def delete_folder(self, folder_path: str, **kwargs) -> str:
757
988
  """Delete a folder and all its contents permanently."""
758
989
  try:
759
990
  # Get current collection
760
991
  collection = self._make_request(
761
- 'GET', f'/collections/{collection_id}')
992
+ 'GET', f'/collections/{self.collection_id}')
762
993
  collection_data = collection["collection"]
763
994
 
764
995
  # Find and remove the folder
765
- if self._remove_folder_by_path(collection_data["item"], folder_path):
996
+ if self.analyzer.remove_folder_by_path(collection_data["item"], folder_path):
766
997
  # Update collection
767
- response = self._make_request('PUT', f'/collections/{collection_id}',
998
+ response = self._make_request('PUT', f'/collections/{self.collection_id}',
768
999
  json={"collection": collection_data})
769
1000
  return json.dumps({"message": f"Folder '{folder_path}' deleted successfully"}, indent=2)
770
1001
  else:
@@ -775,16 +1006,16 @@ class PostmanApiWrapper(BaseToolApiWrapper):
775
1006
  raise ToolException(
776
1007
  f"Unable to delete folder '{folder_path}': {str(e)}")
777
1008
 
778
- def move_folder(self, collection_id: str, source_path: str, target_path: str = None, **kwargs) -> str:
1009
+ def move_folder(self, source_path: str, target_path: str = None, **kwargs) -> str:
779
1010
  """Move a folder to a different location within the collection."""
780
1011
  try:
781
1012
  # Get current collection
782
1013
  collection = self._make_request(
783
- 'GET', f'/collections/{collection_id}')
1014
+ 'GET', f'/collections/{self.collection_id}')
784
1015
  collection_data = collection["collection"]
785
1016
 
786
1017
  # Find source folder
787
- source_folder = self._find_folders_by_path(
1018
+ source_folder = self.analyzer.find_folders_by_path(
788
1019
  collection_data["item"], source_path)
789
1020
  if not source_folder:
790
1021
  raise ToolException(f"Source folder '{source_path}' not found")
@@ -792,11 +1023,11 @@ class PostmanApiWrapper(BaseToolApiWrapper):
792
1023
  folder_data = source_folder[0].copy()
793
1024
 
794
1025
  # Remove from source location
795
- self._remove_folder_by_path(collection_data["item"], source_path)
1026
+ self.analyzer.remove_folder_by_path(collection_data["item"], source_path)
796
1027
 
797
1028
  # Add to target location
798
1029
  if target_path:
799
- target_folders = self._find_folders_by_path(
1030
+ target_folders = self.analyzer.find_folders_by_path(
800
1031
  collection_data["item"], target_path)
801
1032
  if not target_folders:
802
1033
  raise ToolException(
@@ -806,7 +1037,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
806
1037
  collection_data["item"].append(folder_data)
807
1038
 
808
1039
  # Update collection
809
- response = self._make_request('PUT', f'/collections/{collection_id}',
1040
+ response = self._make_request('PUT', f'/collections/{self.collection_id}',
810
1041
  json={"collection": collection_data})
811
1042
  return json.dumps({"message": f"Folder moved from '{source_path}' to '{target_path or 'root'}'"}, indent=2)
812
1043
  except Exception as e:
@@ -819,7 +1050,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
819
1050
  # REQUEST MANAGEMENT METHODS
820
1051
  # =================================================================
821
1052
 
822
- def create_request(self, collection_id: str, name: str, method: str, url: str,
1053
+ def create_request(self, name: str, method: str, url: str,
823
1054
  folder_path: str = None, description: str = None, headers: List[Dict] = None,
824
1055
  body: Dict = None, auth: Dict = None, tests: str = None,
825
1056
  pre_request_script: str = None, **kwargs) -> str:
@@ -827,7 +1058,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
827
1058
  try:
828
1059
  # Get current collection
829
1060
  collection = self._make_request(
830
- 'GET', f'/collections/{collection_id}')
1061
+ 'GET', f'/collections/{self.collection_id}')
831
1062
  collection_data = collection["collection"]
832
1063
 
833
1064
  # Create request item
@@ -870,7 +1101,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
870
1101
 
871
1102
  # Add request to appropriate location
872
1103
  if folder_path:
873
- folders = self._find_folders_by_path(
1104
+ folders = self.analyzer.find_folders_by_path(
874
1105
  collection_data["item"], folder_path)
875
1106
  if not folders:
876
1107
  raise ToolException(f"Folder '{folder_path}' not found")
@@ -879,7 +1110,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
879
1110
  collection_data["item"].append(request_item)
880
1111
 
881
1112
  # Update collection
882
- response = self._make_request('PUT', f'/collections/{collection_id}',
1113
+ response = self._make_request('PUT', f'/collections/{self.collection_id}',
883
1114
  json={"collection": collection_data})
884
1115
  return json.dumps({"message": f"Request '{name}' created successfully"}, indent=2)
885
1116
  except Exception as e:
@@ -887,92 +1118,263 @@ class PostmanApiWrapper(BaseToolApiWrapper):
887
1118
  logger.error(f"Exception when creating request: {stacktrace}")
888
1119
  raise ToolException(f"Unable to create request '{name}': {str(e)}")
889
1120
 
890
- def update_request(self, collection_id: str, request_path: str, name: str = None,
891
- method: str = None, url: str = None, description: str = None,
892
- headers: List[Dict] = None, body: Dict = None, auth: Dict = None,
893
- tests: str = None, pre_request_script: str = None, **kwargs) -> str:
894
- """Update an existing API request."""
1121
+ def update_request_name(self, request_path: str, name: str, **kwargs) -> str:
1122
+ """Update request name."""
895
1123
  try:
896
- # Get current collection
897
- collection = self._make_request(
898
- 'GET', f'/collections/{collection_id}')
899
- collection_data = collection["collection"]
1124
+ # Get request item and ID
1125
+ request_item, request_id, _ = self._get_request_item_and_id(request_path)
1126
+
1127
+ # Create update payload
1128
+ request_update = {
1129
+ "name": name
1130
+ }
900
1131
 
901
- # Find the request
902
- request_item = self._find_request_by_path(
903
- collection_data["item"], request_path)
904
- if not request_item:
905
- raise ToolException(f"Request '{request_path}' not found")
1132
+ # Update the name field
1133
+ response = self._make_request('PUT', f'/collections/{self.collection_id}/requests/{request_id}',
1134
+ json=request_update)
1135
+ return json.dumps({"success": True, "message": f"Request '{request_path}' name updated successfully"}, indent=2)
1136
+ except Exception as e:
1137
+ stacktrace = format_exc()
1138
+ logger.error(f"Exception when updating request name: {stacktrace}")
1139
+ raise ToolException(
1140
+ f"Unable to update request '{request_path}' name: {str(e)}")
906
1141
 
907
- # Update fields if provided
908
- if name:
909
- request_item["name"] = name
910
- if method:
911
- request_item["request"]["method"] = method.upper()
912
- if url:
913
- request_item["request"]["url"] = url
914
- if description is not None:
915
- request_item["request"]["description"] = description
916
- if headers is not None:
917
- request_item["request"]["header"] = headers
918
- if body is not None:
919
- request_item["request"]["body"] = body
920
- if auth is not None:
921
- request_item["request"]["auth"] = auth
1142
+ def update_request_method(self, request_path: str, method: str, **kwargs) -> str:
1143
+ """Update request HTTP method."""
1144
+ try:
1145
+ # Get request item and ID
1146
+ request_item, request_id, _ = self._get_request_item_and_id(request_path)
1147
+
1148
+ # Create update payload
1149
+ request_update = {
1150
+ "method": method.upper()
1151
+ }
922
1152
 
923
- # Update events
924
- if tests is not None or pre_request_script is not None:
925
- events = request_item.get("event", [])
926
-
927
- if pre_request_script is not None:
928
- # Remove existing prerequest events
929
- events = [e for e in events if e.get(
930
- "listen") != "prerequest"]
931
- if pre_request_script:
932
- events.append({
933
- "listen": "prerequest",
934
- "script": {
935
- "exec": pre_request_script.split('\n'),
936
- "type": "text/javascript"
937
- }
938
- })
939
-
940
- if tests is not None:
941
- # Remove existing test events
942
- events = [e for e in events if e.get("listen") != "test"]
943
- if tests:
944
- events.append({
945
- "listen": "test",
946
- "script": {
947
- "exec": tests.split('\n'),
948
- "type": "text/javascript"
949
- }
950
- })
1153
+ # Update the method field
1154
+ response = self._make_request('PUT', f'/collections/{self.collection_id}/requests/{request_id}',
1155
+ json=request_update)
1156
+ return json.dumps({"success": True, "message": f"Request '{request_path}' method updated successfully"}, indent=2)
1157
+ except Exception as e:
1158
+ stacktrace = format_exc()
1159
+ logger.error(f"Exception when updating request method: {stacktrace}")
1160
+ raise ToolException(
1161
+ f"Unable to update request '{request_path}' method: {str(e)}")
951
1162
 
952
- request_item["event"] = events
1163
+ def update_request_url(self, request_path: str, url: str, **kwargs) -> str:
1164
+ """Update request URL."""
1165
+ try:
1166
+ # Get request item and ID
1167
+ request_item, request_id, _ = self._get_request_item_and_id(request_path)
1168
+
1169
+ # Create update payload
1170
+ request_update = {
1171
+ "url": url
1172
+ }
953
1173
 
954
- # Update collection
955
- response = self._make_request('PUT', f'/collections/{collection_id}',
956
- json={"collection": collection_data})
957
- return json.dumps({"message": f"Request '{request_path}' updated successfully"}, indent=2)
1174
+ # Update the URL field
1175
+ response = self._make_request('PUT', f'/collections/{self.collection_id}/requests/{request_id}',
1176
+ json=request_update)
1177
+ return json.dumps({"success": True, "message": f"Request '{request_path}' URL updated successfully"}, indent=2)
1178
+ except Exception as e:
1179
+ stacktrace = format_exc()
1180
+ logger.error(f"Exception when updating request URL: {stacktrace}")
1181
+ raise ToolException(
1182
+ f"Unable to update request '{request_path}' URL: {str(e)}")
1183
+
1184
+ def _get_request_item_and_id(self, request_path: str) -> Tuple[Dict, str, Dict]:
1185
+ """Helper method to get request item and ID by path. Returns (request_item, request_id, collection_data)."""
1186
+ collection = self._make_request(
1187
+ 'GET', f'/collections/{self.collection_id}')
1188
+ collection_data = collection["collection"]
1189
+
1190
+ # Find the request
1191
+ request_item = self.analyzer.find_request_by_path(
1192
+ collection_data["item"], request_path)
1193
+ if not request_item:
1194
+ raise ToolException(f"Request '{request_path}' not found")
1195
+
1196
+ # Get the request ID
1197
+ request_id = request_item.get("id")
1198
+ if not request_id:
1199
+ # If ID is not available directly, try to use the full item ID path
1200
+ if "_postman_id" in request_item:
1201
+ request_id = request_item["_postman_id"]
1202
+ else:
1203
+ raise ToolException(f"Request ID not found for '{request_path}'")
1204
+
1205
+ return request_item, request_id, collection_data
1206
+
1207
+ def update_request_description(self, request_path: str, description: str, **kwargs) -> str:
1208
+ """Update request description."""
1209
+ try:
1210
+ # Get request item and ID
1211
+ request_item, request_id, _ = self._get_request_item_and_id(request_path)
1212
+
1213
+ # For description update, we need to properly format the payload
1214
+ # according to Postman API requirements
1215
+ request_update = {
1216
+ "description": description
1217
+ }
1218
+
1219
+ # Update only the description field
1220
+ response = self._make_request('PUT', f'/collections/{self.collection_id}/requests/{request_id}',
1221
+ json=request_update)
1222
+ return json.dumps({"success": True, "message": f"Request '{request_path}' description updated successfully"}, indent=2)
1223
+ except Exception as e:
1224
+ stacktrace = format_exc()
1225
+ logger.error(f"Exception when updating request description: {stacktrace}")
1226
+ raise ToolException(
1227
+ f"Unable to update request '{request_path}' description: {str(e)}")
1228
+
1229
+ def update_request_headers(self, request_path: str, headers: List[Dict], **kwargs) -> str:
1230
+ """Update request headers."""
1231
+ try:
1232
+ # Get request item and ID
1233
+ request_item, request_id, _ = self._get_request_item_and_id(request_path)
1234
+
1235
+ # Create update payload
1236
+ request_update = {
1237
+ "header": headers
1238
+ }
1239
+
1240
+ # Update the headers field
1241
+ response = self._make_request('PUT', f'/collections/{self.collection_id}/requests/{request_id}',
1242
+ json=request_update)
1243
+ return json.dumps({"success": True, "message": f"Request '{request_path}' headers updated successfully"}, indent=2)
1244
+ except Exception as e:
1245
+ stacktrace = format_exc()
1246
+ logger.error(f"Exception when updating request headers: {stacktrace}")
1247
+ raise ToolException(
1248
+ f"Unable to update request '{request_path}' headers: {str(e)}")
1249
+
1250
+ def update_request_body(self, request_path: str, body: Dict, **kwargs) -> str:
1251
+ """Update request body."""
1252
+ try:
1253
+ # Get request item and ID
1254
+ request_item, request_id, _ = self._get_request_item_and_id(request_path)
1255
+
1256
+ # Create update payload
1257
+ request_update = {
1258
+ "body": body
1259
+ }
1260
+
1261
+ # Update the body field
1262
+ response = self._make_request('PUT', f'/collections/{self.collection_id}/requests/{request_id}',
1263
+ json=request_update)
1264
+ return json.dumps({"success": True, "message": f"Request '{request_path}' body updated successfully"}, indent=2)
1265
+ except Exception as e:
1266
+ stacktrace = format_exc()
1267
+ logger.error(f"Exception when updating request body: {stacktrace}")
1268
+ raise ToolException(
1269
+ f"Unable to update request '{request_path}' body: {str(e)}")
1270
+
1271
+ def update_request_auth(self, request_path: str, auth: Dict, **kwargs) -> str:
1272
+ """Update request authentication."""
1273
+ try:
1274
+ # Get request item and ID
1275
+ request_item, request_id, _ = self._get_request_item_and_id(request_path)
1276
+
1277
+ # Create update payload
1278
+ request_update = {
1279
+ "auth": auth
1280
+ }
1281
+
1282
+ # Update the auth field
1283
+ response = self._make_request('PUT', f'/collections/{self.collection_id}/requests/{request_id}',
1284
+ json=request_update)
1285
+ return json.dumps({"success": True, "message": f"Request '{request_path}' auth updated successfully"}, indent=2)
1286
+ except Exception as e:
1287
+ stacktrace = format_exc()
1288
+ logger.error(f"Exception when updating request auth: {stacktrace}")
1289
+ raise ToolException(
1290
+ f"Unable to update request '{request_path}' auth: {str(e)}")
1291
+
1292
+ def update_request_tests(self, request_path: str, tests: str, **kwargs) -> str:
1293
+ """Update request test scripts."""
1294
+ try:
1295
+ # Get the request ID
1296
+ _, request_id, _ = self._get_request_item_and_id(request_path)
1297
+
1298
+ # Get current request to preserve existing data
1299
+ current_request = self._make_request('GET', f'/collections/{self.collection_id}/requests/{request_id}')
1300
+ request_data = current_request.get("data", {})
1301
+
1302
+ # Prepare the events array - preserve any non-test events
1303
+ events = [event for event in request_data.get("events", []) if event.get("listen") != "test"]
1304
+
1305
+ # Add the new test script
1306
+ events.append({
1307
+ "listen": "test",
1308
+ "script": {
1309
+ "type": "text/javascript",
1310
+ "exec": tests.strip().split('\n')
1311
+ }
1312
+ })
1313
+
1314
+ # Update the events array in the request data
1315
+ request_data["events"] = events
1316
+
1317
+ # Update the request using the individual request endpoint
1318
+ response = self._make_request('PUT', f'/collections/{self.collection_id}/requests/{request_id}',
1319
+ json=request_data)
1320
+
1321
+ logger.info(f"Test script updated successfully for request '{request_path}'")
1322
+ return json.dumps({"success": True, "message": f"Request '{request_path}' tests updated successfully"}, indent=2)
1323
+ except Exception as e:
1324
+ stacktrace = format_exc()
1325
+ logger.error(f"Exception when updating request tests: {stacktrace}")
1326
+ raise ToolException(
1327
+ f"Unable to update request '{request_path}' tests: {str(e)}")
1328
+
1329
+ def update_request_pre_script(self, request_path: str, pre_request_script: str, **kwargs) -> str:
1330
+ """Update request pre-request scripts."""
1331
+ try:
1332
+ # Get the request ID
1333
+ _, request_id, _ = self._get_request_item_and_id(request_path)
1334
+
1335
+ # Get current request to preserve existing data
1336
+ current_request = self._make_request('GET', f'/collections/{self.collection_id}/requests/{request_id}')
1337
+ request_data = current_request.get("data", {})
1338
+
1339
+ # Prepare the events array - preserve any non-prerequest events
1340
+ events = [event for event in request_data.get("events", []) if event.get("listen") != "prerequest"]
1341
+
1342
+ # Add the new prerequest script
1343
+ events.append({
1344
+ "listen": "prerequest",
1345
+ "script": {
1346
+ "type": "text/javascript",
1347
+ "exec": pre_request_script.strip().split('\n')
1348
+ }
1349
+ })
1350
+
1351
+ # Update the events array in the request data
1352
+ request_data["events"] = events
1353
+
1354
+ # Update the request using the individual request endpoint
1355
+ response = self._make_request('PUT', f'/collections/{self.collection_id}/requests/{request_id}',
1356
+ json=request_data)
1357
+
1358
+ logger.info(f"Pre-request script updated successfully for request '{request_path}'")
1359
+ return json.dumps({"success": True, "message": f"Request '{request_path}' pre-script updated successfully"}, indent=2)
958
1360
  except Exception as e:
959
1361
  stacktrace = format_exc()
960
- logger.error(f"Exception when updating request: {stacktrace}")
1362
+ logger.error(f"Exception when updating request pre-script: {stacktrace}")
961
1363
  raise ToolException(
962
- f"Unable to update request '{request_path}': {str(e)}")
1364
+ f"Unable to update request '{request_path}' pre-script: {str(e)}")
963
1365
 
964
- def delete_request(self, collection_id: str, request_path: str, **kwargs) -> str:
1366
+ def delete_request(self, request_path: str, **kwargs) -> str:
965
1367
  """Delete an API request permanently."""
966
1368
  try:
967
1369
  # Get current collection
968
1370
  collection = self._make_request(
969
- 'GET', f'/collections/{collection_id}')
1371
+ 'GET', f'/collections/{self.collection_id}')
970
1372
  collection_data = collection["collection"]
971
1373
 
972
1374
  # Find and remove the request
973
- if self._remove_request_by_path(collection_data["item"], request_path):
1375
+ if self.analyzer.remove_request_by_path(collection_data["item"], request_path):
974
1376
  # Update collection
975
- response = self._make_request('PUT', f'/collections/{collection_id}',
1377
+ response = self._make_request('PUT', f'/collections/{self.collection_id}',
976
1378
  json={"collection": collection_data})
977
1379
  return json.dumps({"message": f"Request '{request_path}' deleted successfully"}, indent=2)
978
1380
  else:
@@ -983,17 +1385,17 @@ class PostmanApiWrapper(BaseToolApiWrapper):
983
1385
  raise ToolException(
984
1386
  f"Unable to delete request '{request_path}': {str(e)}")
985
1387
 
986
- def duplicate_request(self, collection_id: str, source_path: str, new_name: str,
1388
+ def duplicate_request(self, source_path: str, new_name: str,
987
1389
  target_path: str = None, **kwargs) -> str:
988
1390
  """Create a copy of an existing API request."""
989
1391
  try:
990
1392
  # Get current collection
991
1393
  collection = self._make_request(
992
- 'GET', f'/collections/{collection_id}')
1394
+ 'GET', f'/collections/{self.collection_id}')
993
1395
  collection_data = collection["collection"]
994
1396
 
995
1397
  # Find source request
996
- source_request = self._find_request_by_path(
1398
+ source_request = self.analyzer.find_request_by_path(
997
1399
  collection_data["item"], source_path)
998
1400
  if not source_request:
999
1401
  raise ToolException(
@@ -1009,7 +1411,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
1009
1411
 
1010
1412
  # Add to target location
1011
1413
  if target_path:
1012
- folders = self._find_folders_by_path(
1414
+ folders = self.analyzer.find_folders_by_path(
1013
1415
  collection_data["item"], target_path)
1014
1416
  if not folders:
1015
1417
  raise ToolException(
@@ -1019,14 +1421,14 @@ class PostmanApiWrapper(BaseToolApiWrapper):
1019
1421
  # Add to same location as source
1020
1422
  source_folder_path = "/".join(source_path.split("/")[:-1])
1021
1423
  if source_folder_path:
1022
- folders = self._find_folders_by_path(
1424
+ folders = self.analyzer.find_folders_by_path(
1023
1425
  collection_data["item"], source_folder_path)
1024
1426
  folders[0]["item"].append(request_copy)
1025
1427
  else:
1026
1428
  collection_data["item"].append(request_copy)
1027
1429
 
1028
1430
  # Update collection
1029
- response = self._make_request('PUT', f'/collections/{collection_id}',
1431
+ response = self._make_request('PUT', f'/collections/{self.collection_id}',
1030
1432
  json={"collection": collection_data})
1031
1433
  return json.dumps({"message": f"Request duplicated as '{new_name}'"}, indent=2)
1032
1434
  except Exception as e:
@@ -1035,16 +1437,16 @@ class PostmanApiWrapper(BaseToolApiWrapper):
1035
1437
  raise ToolException(
1036
1438
  f"Unable to duplicate request '{source_path}': {str(e)}")
1037
1439
 
1038
- def move_request(self, collection_id: str, source_path: str, target_path: str = None, **kwargs) -> str:
1440
+ def move_request(self, source_path: str, target_path: str = None, **kwargs) -> str:
1039
1441
  """Move an API request to a different folder."""
1040
1442
  try:
1041
1443
  # Get current collection
1042
1444
  collection = self._make_request(
1043
- 'GET', f'/collections/{collection_id}')
1445
+ 'GET', f'/collections/{self.collection_id}')
1044
1446
  collection_data = collection["collection"]
1045
1447
 
1046
1448
  # Find source request
1047
- source_request = self._find_request_by_path(
1449
+ source_request = self.analyzer.find_request_by_path(
1048
1450
  collection_data["item"], source_path)
1049
1451
  if not source_request:
1050
1452
  raise ToolException(
@@ -1053,11 +1455,11 @@ class PostmanApiWrapper(BaseToolApiWrapper):
1053
1455
  request_data = json.loads(json.dumps(source_request)) # Deep copy
1054
1456
 
1055
1457
  # Remove from source location
1056
- self._remove_request_by_path(collection_data["item"], source_path)
1458
+ self.analyzer.remove_request_by_path(collection_data["item"], source_path)
1057
1459
 
1058
1460
  # Add to target location
1059
1461
  if target_path:
1060
- folders = self._find_folders_by_path(
1462
+ folders = self.analyzer.find_folders_by_path(
1061
1463
  collection_data["item"], target_path)
1062
1464
  if not folders:
1063
1465
  raise ToolException(
@@ -1067,7 +1469,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
1067
1469
  collection_data["item"].append(request_data)
1068
1470
 
1069
1471
  # Update collection
1070
- response = self._make_request('PUT', f'/collections/{collection_id}',
1472
+ response = self._make_request('PUT', f'/collections/{self.collection_id}',
1071
1473
  json={"collection": collection_data})
1072
1474
  return json.dumps({"message": f"Request moved from '{source_path}' to '{target_path or 'root'}'"}, indent=2)
1073
1475
  except Exception as e:
@@ -1075,958 +1477,75 @@ class PostmanApiWrapper(BaseToolApiWrapper):
1075
1477
  logger.error(f"Exception when moving request: {stacktrace}")
1076
1478
  raise ToolException(
1077
1479
  f"Unable to move request '{source_path}': {str(e)}")
1078
-
1480
+
1079
1481
  # =================================================================
1080
1482
  # HELPER METHODS
1081
1483
  # =================================================================
1082
1484
 
1083
- def _find_folders_by_path(self, items: List[Dict], path: str) -> List[Dict]:
1084
- """Find folders by path (supports nested paths like 'API/Users')."""
1085
- path_parts = [part.strip() for part in path.split('/') if part.strip()]
1086
- if not path_parts:
1087
- return items
1088
-
1089
- results = []
1090
-
1091
- def find_in_items(current_items: List[Dict], current_path: List[str], depth: int = 0):
1092
- if depth >= len(current_path):
1093
- results.extend(current_items)
1094
- return
1095
-
1096
- target_name = current_path[depth]
1097
- for item in current_items:
1098
- if (item.get('name', '').lower() == target_name.lower() or
1099
- target_name.lower() in item.get('name', '').lower()) and item.get('item'):
1100
- if depth == len(current_path) - 1:
1101
- # This is the target folder
1102
- results.append(item)
1103
- else:
1104
- # Continue searching in subfolders
1105
- find_in_items(item['item'], current_path, depth + 1)
1106
-
1107
- find_in_items(items, path_parts)
1108
- return results
1109
-
1110
- def _extract_requests_from_items(self, items: List[Dict], include_details: bool = False) -> List[Dict]:
1111
- """Extract requests from items recursively."""
1112
- requests = []
1113
-
1114
- for item in items:
1115
- if item.get('request'):
1116
- # This is a request
1117
- request_data = {
1118
- "name": item.get('name'),
1119
- "method": item['request'].get('method'),
1120
- "url": item['request'].get('url')
1121
- }
1122
-
1123
- if include_details:
1124
- request_data.update({
1125
- "description": item.get('description'),
1126
- "headers": item['request'].get('header', []),
1127
- "body": item['request'].get('body'),
1128
- "auth": item['request'].get('auth'),
1129
- "tests": [e for e in item.get('event', []) if e.get('listen') == 'test'],
1130
- "pre_request_scripts": [e for e in item.get('event', []) if e.get('listen') == 'prerequest']
1131
- })
1132
-
1133
- requests.append(request_data)
1134
- elif item.get('item'):
1135
- # This is a folder, recurse
1136
- requests.extend(self._extract_requests_from_items(
1137
- item['item'], include_details))
1138
-
1139
- return requests
1140
-
1141
- def _search_requests_in_items(self, items: List[Dict], query: str, search_in: str, method: str = None) -> List[Dict]:
1142
- """Search for requests in items recursively."""
1143
- results = []
1144
- query_lower = query.lower()
1145
-
1146
- for item in items:
1147
- if item.get('request'):
1148
- # This is a request
1149
- request = item['request']
1150
- matches = False
1151
-
1152
- # Check method filter first
1153
- if method and request.get('method', '').upper() != method.upper():
1154
- continue
1155
-
1156
- # Check search criteria
1157
- if search_in == 'all' or search_in == 'name':
1158
- if query_lower in item.get('name', '').lower():
1159
- matches = True
1160
-
1161
- if search_in == 'all' or search_in == 'url':
1162
- url = request.get('url', '')
1163
- if isinstance(url, dict):
1164
- url = url.get('raw', '')
1165
- if query_lower in url.lower():
1166
- matches = True
1167
-
1168
- if search_in == 'all' or search_in == 'description':
1169
- description = item.get(
1170
- 'description', '') or request.get('description', '')
1171
- if query_lower in description.lower():
1172
- matches = True
1173
-
1174
- if matches:
1175
- results.append({
1176
- "name": item.get('name'),
1177
- "method": request.get('method'),
1178
- "url": request.get('url'),
1179
- "description": item.get('description') or request.get('description'),
1180
- "path": self._get_item_path(items, item)
1181
- })
1182
-
1183
- elif item.get('item'):
1184
- # This is a folder, recurse
1185
- results.extend(self._search_requests_in_items(
1186
- item['item'], query, search_in, method))
1187
-
1188
- return results
1189
-
1190
- def _get_item_path(self, root_items: List[Dict], target_item: Dict, current_path: str = "") -> str:
1191
- """Get the path of an item within the collection structure."""
1192
- for item in root_items:
1193
- item_path = f"{current_path}/{item['name']}" if current_path else item['name']
1194
-
1195
- if item == target_item:
1196
- return item_path
1197
-
1198
- if item.get('item'):
1199
- result = self._get_item_path(
1200
- item['item'], target_item, item_path)
1201
- if result:
1202
- return result
1203
-
1204
- return ""
1205
-
1206
- def _find_request_by_path(self, items: List[Dict], request_path: str) -> Optional[Dict]:
1207
- """Find a request by its path."""
1208
- path_parts = [part.strip()
1209
- for part in request_path.split('/') if part.strip()]
1210
- if not path_parts:
1211
- return None
1212
-
1213
- current_items = items
1214
-
1215
- # Navigate through folders to the request
1216
- for i, part in enumerate(path_parts):
1217
- found = False
1218
- for item in current_items:
1219
- if item.get('name', '').lower() == part.lower():
1220
- if i == len(path_parts) - 1:
1221
- # This should be the request
1222
- if item.get('request'):
1223
- return item
1224
- else:
1225
- return None
1485
+ def get_request(self, request_path: str, **kwargs) -> str:
1486
+ """Get a specific request by path.
1487
+
1488
+ Uses the _get_request_item_and_id helper to find the request and then fetches complete
1489
+ information using the Postman API endpoint for individual requests.
1490
+ """
1491
+ try:
1492
+ # Get request item and ID
1493
+ _, request_id, _ = self._get_request_item_and_id(request_path)
1494
+
1495
+ # Fetch the complete request information from the API
1496
+ response = self._make_request(
1497
+ 'GET', f'/collections/{self.collection_id}/requests/{request_id}'
1498
+ )
1499
+
1500
+ return json.dumps(response, indent=2)
1501
+ except Exception as e:
1502
+ stacktrace = format_exc()
1503
+ logger.error(f"Exception when getting request: {stacktrace}")
1504
+ raise ToolException(
1505
+ f"Unable to get request '{request_path}': {str(e)}")
1506
+
1507
+ def get_request_script(self, request_path: str, script_type: str = "prerequest", **kwargs) -> str:
1508
+ """
1509
+ Get the script (pre-request or test) for a request by path.
1510
+
1511
+ Args:
1512
+ request_path: Path to the request within the collection
1513
+ script_type: The type of script to retrieve ("prerequest" or "test")
1514
+
1515
+ Returns:
1516
+ The script content as JSON string, or an error message if the script doesn't exist
1517
+ """
1518
+ try:
1519
+ # Get the request ID and fetch current request data
1520
+ _, request_id, _ = self._get_request_item_and_id(request_path)
1521
+
1522
+ # Get current request to have the latest version with updated scripts
1523
+ current_request = self._make_request('GET', f'/collections/{self.collection_id}/requests/{request_id}')
1524
+ request_data = current_request.get("data", {})
1525
+
1526
+ # Find the script by type
1527
+ script_content = None
1528
+ for event in request_data.get("events", []):
1529
+ if event.get("listen") == script_type:
1530
+ script = event.get("script", {})
1531
+ exec_content = script.get("exec", [])
1532
+ if isinstance(exec_content, list):
1533
+ script_content = "\n".join(exec_content)
1226
1534
  else:
1227
- # This should be a folder
1228
- if item.get('item'):
1229
- current_items = item['item']
1230
- found = True
1231
- break
1232
- else:
1233
- return None
1234
-
1235
- if not found:
1236
- return None
1237
-
1238
- return None
1239
-
1240
- def _remove_folder_by_path(self, items: List[Dict], folder_path: str) -> bool:
1241
- """Remove a folder by its path."""
1242
- path_parts = [part.strip()
1243
- for part in folder_path.split('/') if part.strip()]
1244
- if not path_parts:
1245
- return False
1246
-
1247
- if len(path_parts) == 1:
1248
- # Remove from current level
1249
- for i, item in enumerate(items):
1250
- if item.get('name', '').lower() == path_parts[0].lower() and item.get('item') is not None:
1251
- del items[i]
1252
- return True
1253
- return False
1254
- else:
1255
- # Navigate to parent folder
1256
- parent_path = '/'.join(path_parts[:-1])
1257
- parent_folders = self._find_folders_by_path(items, parent_path)
1258
- if parent_folders:
1259
- return self._remove_folder_by_path(parent_folders[0]['item'], path_parts[-1])
1260
- return False
1261
-
1262
- def _remove_request_by_path(self, items: List[Dict], request_path: str) -> bool:
1263
- """Remove a request by its path."""
1264
- path_parts = [part.strip()
1265
- for part in request_path.split('/') if part.strip()]
1266
- if not path_parts:
1267
- return False
1268
-
1269
- if len(path_parts) == 1:
1270
- # Remove from current level
1271
- for i, item in enumerate(items):
1272
- if item.get('name', '').lower() == path_parts[0].lower() and item.get('request'):
1273
- del items[i]
1274
- return True
1275
- return False
1276
- else:
1277
- # Navigate to parent folder
1278
- parent_path = '/'.join(path_parts[:-1])
1279
- parent_folders = self._find_folders_by_path(items, parent_path)
1280
- if parent_folders:
1281
- return self._remove_request_by_path(parent_folders[0]['item'], path_parts[-1])
1282
- return False
1283
-
1284
- def _remove_item_ids(self, items: List[Dict]):
1285
- """Remove IDs from items recursively for duplication."""
1286
- for item in items:
1287
- if 'id' in item:
1288
- del item['id']
1289
- if item.get('item'):
1290
- self._remove_item_ids(item['item'])
1291
-
1292
- # =================================================================
1293
- # ANALYSIS HELPER METHODS
1294
- # =================================================================
1295
-
1296
- def _perform_collection_analysis(self, collection: Dict) -> Dict:
1297
- """Perform comprehensive analysis of a collection."""
1298
- collection_data = collection['collection']
1299
- folders = self._analyze_folders(collection_data.get('item', []))
1300
- total_requests = self._count_requests(collection_data.get('item', []))
1301
- issues = self._identify_collection_issues(collection_data)
1302
- score = self._calculate_quality_score(collection_data, folders, issues)
1303
- recommendations = self._generate_recommendations(issues)
1304
-
1305
- return {
1306
- "collection_id": collection_data['info'].get('_postman_id', ''),
1307
- "collection_name": collection_data['info'].get('name', ''),
1308
- "total_requests": total_requests,
1309
- "folders": folders,
1310
- "issues": issues,
1311
- "recommendations": recommendations,
1312
- "score": score,
1313
- "overall_security_score": self._calculate_overall_security_score(folders),
1314
- "overall_performance_score": self._calculate_overall_performance_score(folders),
1315
- "overall_documentation_score": self._calculate_overall_documentation_score(folders)
1316
- }
1317
-
1318
- def _analyze_folders(self, items: List[Dict], base_path: str = "") -> List[Dict]:
1319
- """Analyze all folders in a collection."""
1320
- folders = []
1321
-
1322
- for item in items:
1323
- if item.get('item') is not None: # This is a folder
1324
- folder_path = f"{base_path}/{item['name']}" if base_path else item['name']
1325
- analysis = self._perform_folder_analysis(item, folder_path)
1326
- folders.append(analysis)
1327
-
1328
- # Recursively analyze subfolders
1329
- subfolders = self._analyze_folders(item['item'], folder_path)
1330
- folders.extend(subfolders)
1331
-
1332
- return folders
1333
-
1334
- def _perform_folder_analysis(self, folder: Dict, path: str) -> Dict:
1335
- """Perform analysis of a specific folder."""
1336
- requests = self._analyze_requests(folder.get('item', []))
1337
- request_count = self._count_requests(folder.get('item', []))
1338
- issues = self._identify_folder_issues(folder, requests)
1339
-
1340
- return {
1341
- "name": folder['name'],
1342
- "path": path,
1343
- "request_count": request_count,
1344
- "requests": requests,
1345
- "issues": issues,
1346
- "has_consistent_naming": self._check_consistent_naming(folder.get('item', [])),
1347
- "has_proper_structure": bool(folder.get('description') and folder.get('item')),
1348
- "auth_consistency": self._check_auth_consistency(requests),
1349
- "avg_documentation_quality": self._calculate_avg_documentation_quality(requests),
1350
- "avg_security_score": self._calculate_avg_security_score(requests),
1351
- "avg_performance_score": self._calculate_avg_performance_score(requests)
1352
- }
1353
-
1354
- def _analyze_requests(self, items: List[Dict]) -> List[Dict]:
1355
- """Analyze requests within a folder."""
1356
- requests = []
1357
-
1358
- for item in items:
1359
- if item.get('request'): # This is a request
1360
- analysis = self._perform_request_analysis(item)
1361
- requests.append(analysis)
1362
-
1363
- return requests
1364
-
1365
- def _perform_request_analysis(self, item: Dict) -> Dict:
1366
- """Perform comprehensive analysis of a specific request."""
1367
- request = item['request']
1368
- issues = []
1369
-
1370
- # Basic checks
1371
- has_auth = bool(request.get('auth')
1372
- or self._has_auth_in_headers(request))
1373
- has_description = bool(item.get('description')
1374
- or request.get('description'))
1375
- has_tests = bool([e for e in item.get('event', [])
1376
- if e.get('listen') == 'test'])
1377
- has_examples = bool(item.get('response', []))
1378
-
1379
- # Enhanced analysis
1380
- url = request.get('url', '')
1381
- if isinstance(url, dict):
1382
- url = url.get('raw', '')
1383
-
1384
- has_hardcoded_url = self._detect_hardcoded_url(url)
1385
- has_hardcoded_data = self._detect_hardcoded_data(request)
1386
- has_proper_headers = self._validate_headers(request)
1387
- has_variables = self._detect_variable_usage(request)
1388
- has_error_handling = self._detect_error_handling(item)
1389
- follows_naming_convention = self._validate_naming_convention(
1390
- item['name'])
1391
- has_security_issues = self._detect_security_issues(request)
1392
- has_performance_issues = self._detect_performance_issues(request)
1393
-
1394
- # Calculate scores
1395
- security_score = self._calculate_security_score(
1396
- request, has_auth, has_security_issues)
1397
- performance_score = self._calculate_performance_score(
1398
- request, has_performance_issues)
1399
-
1400
- # Generate issues
1401
- self._generate_request_issues(issues, item, {
1402
- 'has_description': has_description,
1403
- 'has_auth': has_auth,
1404
- 'has_tests': has_tests,
1405
- 'has_hardcoded_url': has_hardcoded_url,
1406
- 'has_hardcoded_data': has_hardcoded_data,
1407
- 'has_proper_headers': has_proper_headers,
1408
- 'has_security_issues': has_security_issues,
1409
- 'follows_naming_convention': follows_naming_convention
1410
- })
1411
-
1412
- return {
1413
- "name": item['name'],
1414
- "method": request.get('method'),
1415
- "url": url,
1416
- "has_auth": has_auth,
1417
- "has_description": has_description,
1418
- "has_tests": has_tests,
1419
- "has_examples": has_examples,
1420
- "issues": issues,
1421
- "has_hardcoded_url": has_hardcoded_url,
1422
- "has_hardcoded_data": has_hardcoded_data,
1423
- "has_proper_headers": has_proper_headers,
1424
- "has_variables": has_variables,
1425
- "has_error_handling": has_error_handling,
1426
- "follows_naming_convention": follows_naming_convention,
1427
- "has_security_issues": has_security_issues,
1428
- "has_performance_issues": has_performance_issues,
1429
- "auth_type": request.get('auth', {}).get('type'),
1430
- "response_examples": len(item.get('response', [])),
1431
- "test_coverage": self._assess_test_coverage(item),
1432
- "documentation_quality": self._assess_documentation_quality(item),
1433
- "security_score": security_score,
1434
- "performance_score": performance_score
1435
- }
1436
-
1437
- def _count_requests(self, items: List[Dict]) -> int:
1438
- """Count total requests in items."""
1439
- count = 0
1440
- for item in items:
1441
- if item.get('request'):
1442
- count += 1
1443
- elif item.get('item'):
1444
- count += self._count_requests(item['item'])
1445
- return count
1446
-
1447
- def _has_auth_in_headers(self, request: Dict) -> bool:
1448
- """Check if request has authentication in headers."""
1449
- headers = request.get('header', [])
1450
- auth_headers = ['authorization', 'x-api-key', 'x-auth-token']
1451
- return any(h.get('key', '').lower() in auth_headers for h in headers)
1452
-
1453
- def _detect_hardcoded_url(self, url: str) -> bool:
1454
- """Detect hardcoded URLs that should use variables."""
1455
- hardcoded_patterns = [
1456
- r'^https?://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', # IP addresses
1457
- r'^https?://localhost', # localhost
1458
- # Direct domains
1459
- r'^https?://[a-zA-Z0-9.-]+\.(com|org|net|io|dev)',
1460
- r'api\.example\.com', # Example domains
1461
- r'staging\.|dev\.|test\.' # Environment-specific
1462
- ]
1463
- return any(re.search(pattern, url) for pattern in hardcoded_patterns) and '{{' not in url
1464
-
1465
- def _detect_hardcoded_data(self, request: Dict) -> bool:
1466
- """Detect hardcoded data in request body and headers."""
1467
- # Check headers
1468
- headers = request.get('header', [])
1469
- has_hardcoded_headers = any(
1470
- ('token' in h.get('key', '').lower() or
1471
- 'key' in h.get('key', '').lower() or
1472
- 'secret' in h.get('key', '').lower()) and
1473
- '{{' not in h.get('value', '')
1474
- for h in headers
1475
- )
1476
-
1477
- # Check body
1478
- has_hardcoded_body = False
1479
- body = request.get('body', {})
1480
- if body.get('raw'):
1481
- try:
1482
- body_data = json.loads(body['raw'])
1483
- has_hardcoded_body = self._contains_hardcoded_values(body_data)
1484
- except json.JSONDecodeError:
1485
- # If not JSON, check for common patterns
1486
- has_hardcoded_body = re.search(
1487
- r'("api_key"|"token"|"password"):\s*"[^{]', body['raw']) is not None
1488
-
1489
- return has_hardcoded_headers or has_hardcoded_body
1490
-
1491
- def _contains_hardcoded_values(self, obj: Any) -> bool:
1492
- """Check if object contains hardcoded values that should be variables."""
1493
- if not isinstance(obj, dict):
1494
- return False
1495
-
1496
- for key, value in obj.items():
1497
- if isinstance(value, str):
1498
- # Check for sensitive keys
1499
- if key.lower() in ['token', 'key', 'secret', 'password', 'api_key', 'client_id', 'client_secret']:
1500
- if '{{' not in value:
1501
- return True
1502
- # Check for email patterns, URLs
1503
- if re.search(r'@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', value) or value.startswith('http'):
1504
- if '{{' not in value:
1505
- return True
1506
- elif isinstance(value, dict):
1507
- if self._contains_hardcoded_values(value):
1508
- return True
1509
-
1510
- return False
1511
-
1512
- def _validate_headers(self, request: Dict) -> bool:
1513
- """Validate request headers."""
1514
- headers = request.get('header', [])
1515
- header_names = [h.get('key', '').lower() for h in headers]
1516
- method = request.get('method', '').upper()
1517
-
1518
- # Check for essential headers
1519
- if method in ['POST', 'PUT', 'PATCH'] and request.get('body'):
1520
- if 'content-type' not in header_names:
1521
- return False
1522
-
1523
- if method in ['GET', 'POST', 'PUT', 'PATCH']:
1524
- if 'accept' not in header_names:
1525
- return False
1526
-
1527
- return True
1528
-
1529
- def _detect_variable_usage(self, request: Dict) -> bool:
1530
- """Detect variable usage in request."""
1531
- url = request.get('url', '')
1532
- if isinstance(url, dict):
1533
- url = url.get('raw', '')
1534
-
1535
- has_url_variables = '{{' in url
1536
- has_header_variables = any('{{' in h.get('value', '')
1537
- for h in request.get('header', []))
1538
-
1539
- has_body_variables = False
1540
- body = request.get('body', {})
1541
- if body.get('raw'):
1542
- has_body_variables = '{{' in body['raw']
1543
-
1544
- return has_url_variables or has_header_variables or has_body_variables
1545
-
1546
- def _detect_error_handling(self, item: Dict) -> bool:
1547
- """Detect error handling in tests."""
1548
- test_scripts = [e for e in item.get(
1549
- 'event', []) if e.get('listen') == 'test']
1550
-
1551
- for script in test_scripts:
1552
- script_code = '\n'.join(script.get('script', {}).get('exec', []))
1553
- if ('4' in script_code or '5' in script_code or
1554
- 'error' in script_code.lower() or 'fail' in script_code.lower()):
1555
- return True
1556
-
1557
- return False
1558
-
1559
- def _validate_naming_convention(self, name: str) -> bool:
1560
- """Validate naming convention."""
1561
- has_consistent_case = re.match(
1562
- r'^[a-zA-Z][a-zA-Z0-9\s\-_]*$', name) is not None
1563
- has_descriptive_name = len(
1564
- name) > 3 and 'test' not in name.lower() and 'temp' not in name.lower()
1565
- return has_consistent_case and has_descriptive_name
1566
-
1567
- def _detect_security_issues(self, request: Dict) -> bool:
1568
- """Detect security issues."""
1569
- url = request.get('url', '')
1570
- if isinstance(url, dict):
1571
- url = url.get('raw', '')
1572
-
1573
- # Check for exposed credentials in URL
1574
- if re.search(r'[?&](token|key|password|secret)=([^&\s]+)', url):
1575
- return True
1576
-
1577
- # Check for weak authentication
1578
- auth = request.get('auth', {})
1579
- if auth.get('type') == 'basic' and not url.startswith('https'):
1580
- return True
1581
-
1582
- # Check headers for exposed credentials
1583
- headers = request.get('header', [])
1584
- return any('secret' in h.get('key', '').lower() or 'password' in h.get('key', '').lower()
1585
- for h in headers)
1586
-
1587
- def _detect_performance_issues(self, request: Dict) -> bool:
1588
- """Detect performance issues."""
1589
- # Large request body
1590
- body = request.get('body', {})
1591
- if body.get('raw') and len(body['raw']) > 10000:
1592
- return True
1593
-
1594
- # Too many headers
1595
- if len(request.get('header', [])) > 20:
1596
- return True
1597
-
1598
- # Too many query parameters
1599
- url = request.get('url', '')
1600
- if isinstance(url, dict):
1601
- url = url.get('raw', '')
1602
-
1603
- query_params = url.split('?')[1] if '?' in url else ''
1604
- if query_params and len(query_params.split('&')) > 15:
1605
- return True
1606
-
1607
- return False
1608
-
1609
- def _calculate_security_score(self, request: Dict, has_auth: bool, has_security_issues: bool) -> int:
1610
- """Calculate security score."""
1611
- score = 100
1612
- method = request.get('method', '').upper()
1613
-
1614
- if not has_auth and method in ['POST', 'PUT', 'PATCH', 'DELETE']:
1615
- score -= 40
1616
-
1617
- if has_security_issues:
1618
- score -= 30
1619
-
1620
- url = request.get('url', '')
1621
- if isinstance(url, dict):
1622
- url = url.get('raw', '')
1623
-
1624
- if url.startswith('http://'):
1625
- score -= 20
1626
-
1627
- auth = request.get('auth', {})
1628
- if auth.get('type') == 'basic':
1629
- score -= 10
1630
-
1631
- return max(0, score)
1632
-
1633
- def _calculate_performance_score(self, request: Dict, has_performance_issues: bool) -> int:
1634
- """Calculate performance score."""
1635
- score = 100
1636
-
1637
- if has_performance_issues:
1638
- score -= 50
1639
-
1640
- headers = request.get('header', [])
1641
- header_names = [h.get('key', '').lower() for h in headers]
1642
-
1643
- if 'cache-control' not in header_names:
1644
- score -= 10
1645
-
1646
- if 'accept-encoding' not in header_names:
1647
- score -= 10
1648
-
1649
- return max(0, score)
1650
-
1651
- def _assess_test_coverage(self, item: Dict) -> str:
1652
- """Assess test coverage."""
1653
- test_scripts = [e for e in item.get(
1654
- 'event', []) if e.get('listen') == 'test']
1655
-
1656
- if not test_scripts:
1657
- return 'none'
1658
-
1659
- all_test_code = '\n'.join([
1660
- '\n'.join(script.get('script', {}).get('exec', []))
1661
- for script in test_scripts
1662
- ])
1663
-
1664
- checks = [
1665
- 'pm.response.code' in all_test_code or 'status' in all_test_code,
1666
- 'responseTime' in all_test_code,
1667
- 'pm.response.json' in all_test_code or 'body' in all_test_code,
1668
- '4' in all_test_code or '5' in all_test_code
1669
- ]
1670
-
1671
- check_count = sum(checks)
1672
-
1673
- if check_count >= 3:
1674
- return 'comprehensive'
1675
- elif check_count >= 1:
1676
- return 'basic'
1677
-
1678
- return 'none'
1679
-
1680
- def _assess_documentation_quality(self, item: Dict) -> str:
1681
- """Assess documentation quality."""
1682
- description = item.get('description', '') or item.get(
1683
- 'request', {}).get('description', '')
1684
-
1685
- if not description:
1686
- return 'none'
1687
-
1688
- description_lower = description.lower()
1689
- quality_factors = [
1690
- 'parameter' in description_lower,
1691
- 'response' in description_lower,
1692
- 'example' in description_lower,
1693
- 'auth' in description_lower,
1694
- 'error' in description_lower
1695
- ]
1696
-
1697
- factor_count = sum(quality_factors)
1698
-
1699
- if factor_count >= 4:
1700
- return 'excellent'
1701
- elif factor_count >= 2:
1702
- return 'good'
1703
- elif factor_count >= 1 or len(description) > 50:
1704
- return 'minimal'
1705
-
1706
- return 'none'
1707
-
1708
- def _check_consistent_naming(self, items: List[Dict]) -> bool:
1709
- """Check if items have consistent naming."""
1710
- if len(items) <= 1:
1711
- return True
1712
-
1713
- naming_patterns = []
1714
- for item in items:
1715
- name = item.get('name', '').lower()
1716
- if re.match(r'^[a-z][a-z0-9]*(_[a-z0-9]+)*$', name):
1717
- naming_patterns.append('snake_case')
1718
- elif re.match(r'^[a-z][a-zA-Z0-9]*$', name):
1719
- naming_patterns.append('camelCase')
1720
- elif re.match(r'^[a-z][a-z0-9]*(-[a-z0-9]+)*$', name):
1721
- naming_patterns.append('kebab-case')
1722
- else:
1723
- naming_patterns.append('mixed')
1724
-
1725
- unique_patterns = set(naming_patterns)
1726
- return len(unique_patterns) == 1 and 'mixed' not in unique_patterns
1727
-
1728
- def _check_auth_consistency(self, requests: List[Dict]) -> str:
1729
- """Check authentication consistency across requests."""
1730
- if not requests:
1731
- return 'none'
1732
-
1733
- auth_types = set(req.get('auth_type') or 'none' for req in requests)
1734
-
1735
- if len(auth_types) == 1:
1736
- return 'none' if 'none' in auth_types else 'consistent'
1737
-
1738
- return 'mixed'
1739
-
1740
- def _calculate_avg_documentation_quality(self, requests: List[Dict]) -> int:
1741
- """Calculate average documentation quality score."""
1742
- if not requests:
1743
- return 0
1744
-
1745
- quality_scores = {
1746
- 'excellent': 100,
1747
- 'good': 75,
1748
- 'minimal': 50,
1749
- 'none': 0
1750
- }
1751
-
1752
- scores = [quality_scores.get(
1753
- req.get('documentation_quality', 'none'), 0) for req in requests]
1754
- return round(sum(scores) / len(scores))
1755
-
1756
- def _calculate_avg_security_score(self, requests: List[Dict]) -> int:
1757
- """Calculate average security score."""
1758
- if not requests:
1759
- return 0
1760
-
1761
- scores = [req.get('security_score', 0) for req in requests]
1762
- return round(sum(scores) / len(scores))
1763
-
1764
- def _calculate_avg_performance_score(self, requests: List[Dict]) -> int:
1765
- """Calculate average performance score."""
1766
- if not requests:
1767
- return 0
1768
-
1769
- scores = [req.get('performance_score', 0) for req in requests]
1770
- return round(sum(scores) / len(scores))
1771
-
1772
- def _identify_collection_issues(self, collection_data: Dict) -> List[Dict]:
1773
- """Identify collection-level issues."""
1774
- issues = []
1775
-
1776
- if not collection_data.get('info', {}).get('description'):
1777
- issues.append({
1778
- 'type': 'warning',
1779
- 'severity': 'medium',
1780
- 'message': 'Collection lacks description',
1781
- 'location': 'Collection root',
1782
- 'suggestion': 'Add a description explaining the purpose of this collection'
1783
- })
1784
-
1785
- if not collection_data.get('auth'):
1786
- issues.append({
1787
- 'type': 'info',
1788
- 'severity': 'low',
1789
- 'message': 'Collection lacks default authentication',
1790
- 'location': 'Collection root',
1791
- 'suggestion': 'Consider setting up collection-level authentication'
1792
- })
1793
-
1794
- return issues
1795
-
1796
- def _identify_folder_issues(self, folder: Dict, requests: List[Dict]) -> List[Dict]:
1797
- """Identify folder-level issues."""
1798
- issues = []
1799
-
1800
- if not folder.get('description'):
1801
- issues.append({
1802
- 'type': 'warning',
1803
- 'severity': 'low',
1804
- 'message': 'Folder lacks description',
1805
- 'location': folder['name'],
1806
- 'suggestion': 'Add a description explaining the purpose of this folder'
1807
- })
1808
-
1809
- if not requests and (not folder.get('item') or len(folder['item']) == 0):
1810
- issues.append({
1811
- 'type': 'warning',
1812
- 'severity': 'medium',
1813
- 'message': 'Empty folder',
1814
- 'location': folder['name'],
1815
- 'suggestion': 'Consider removing empty folders or adding requests'
1816
- })
1817
-
1818
- return issues
1819
-
1820
- def _generate_request_issues(self, issues: List[Dict], item: Dict, analysis: Dict):
1821
- """Generate request-specific issues."""
1822
- if not analysis['has_description']:
1823
- issues.append({
1824
- 'type': 'warning',
1825
- 'severity': 'medium',
1826
- 'message': 'Request lacks description',
1827
- 'location': item['name'],
1828
- 'suggestion': 'Add a clear description explaining what this request does'
1829
- })
1830
-
1831
- if not analysis['has_auth'] and item['request']['method'] in ['POST', 'PUT', 'PATCH', 'DELETE']:
1832
- issues.append({
1833
- 'type': 'warning',
1834
- 'severity': 'high',
1835
- 'message': 'Sensitive operation without authentication',
1836
- 'location': item['name'],
1837
- 'suggestion': 'Add authentication for this request'
1838
- })
1839
-
1840
- if not analysis['has_tests']:
1841
- issues.append({
1842
- 'type': 'info',
1843
- 'severity': 'high',
1844
- 'message': 'Request lacks test scripts',
1845
- 'location': item['name'],
1846
- 'suggestion': 'Add test scripts to validate response'
1847
- })
1848
-
1849
- if analysis['has_hardcoded_url']:
1850
- issues.append({
1851
- 'type': 'warning',
1852
- 'severity': 'high',
1853
- 'message': 'Request contains hardcoded URL',
1854
- 'location': item['name'],
1855
- 'suggestion': 'Replace hardcoded URLs with environment variables'
1856
- })
1857
-
1858
- if analysis['has_security_issues']:
1859
- issues.append({
1860
- 'type': 'error',
1861
- 'severity': 'high',
1862
- 'message': 'Security vulnerabilities detected',
1863
- 'location': item['name'],
1864
- 'suggestion': 'Address security issues such as exposed credentials'
1865
- })
1866
-
1867
- def _calculate_quality_score(self, collection_data: Dict, folders: List[Dict], issues: List[Dict]) -> int:
1868
- """Calculate quality score (0-100)."""
1869
- score = 100
1870
-
1871
- # Deduct points for issues
1872
- for issue in issues:
1873
- severity = issue.get('severity', 'low')
1874
- if severity == 'high':
1875
- score -= 10
1876
- elif severity == 'medium':
1877
- score -= 5
1878
- elif severity == 'low':
1879
- score -= 2
1880
-
1881
- # Deduct points for folder and request issues
1882
- for folder in folders:
1883
- for issue in folder.get('issues', []):
1884
- severity = issue.get('severity', 'low')
1885
- if severity == 'high':
1886
- score -= 5
1887
- elif severity == 'medium':
1888
- score -= 3
1889
- elif severity == 'low':
1890
- score -= 1
1891
-
1892
- for request in folder.get('requests', []):
1893
- for issue in request.get('issues', []):
1894
- severity = issue.get('severity', 'low')
1895
- if severity == 'high':
1896
- score -= 3
1897
- elif severity == 'medium':
1898
- score -= 2
1899
- elif severity == 'low':
1900
- score -= 1
1901
-
1902
- return max(0, min(100, score))
1903
-
1904
- def _generate_recommendations(self, issues: List[Dict]) -> List[str]:
1905
- """Generate recommendations based on issues."""
1906
- recommendations = []
1907
- suggestion_counts = {}
1908
-
1909
- # Count similar suggestions
1910
- for issue in issues:
1911
- suggestion = issue.get('suggestion', '')
1912
- if suggestion:
1913
- suggestion_counts[suggestion] = suggestion_counts.get(
1914
- suggestion, 0) + 1
1915
-
1916
- # Generate recommendations from most common suggestions
1917
- sorted_suggestions = sorted(
1918
- suggestion_counts.items(), key=lambda x: x[1], reverse=True)[:10]
1919
-
1920
- for suggestion, count in sorted_suggestions:
1921
- if count > 1:
1922
- recommendations.append(f"{suggestion} ({count} instances)")
1923
- else:
1924
- recommendations.append(suggestion)
1925
-
1926
- return recommendations
1927
-
1928
- def _calculate_overall_security_score(self, folders: List[Dict]) -> int:
1929
- """Calculate overall security score."""
1930
- if not folders:
1931
- return 0
1932
-
1933
- scores = []
1934
- for folder in folders:
1935
- avg_score = folder.get('avg_security_score', 0)
1936
- if avg_score > 0:
1937
- scores.append(avg_score)
1938
-
1939
- return round(sum(scores) / len(scores)) if scores else 0
1940
-
1941
- def _calculate_overall_performance_score(self, folders: List[Dict]) -> int:
1942
- """Calculate overall performance score."""
1943
- if not folders:
1944
- return 0
1945
-
1946
- scores = []
1947
- for folder in folders:
1948
- avg_score = folder.get('avg_performance_score', 0)
1949
- if avg_score > 0:
1950
- scores.append(avg_score)
1951
-
1952
- return round(sum(scores) / len(scores)) if scores else 0
1953
-
1954
- def _calculate_overall_documentation_score(self, folders: List[Dict]) -> int:
1955
- """Calculate overall documentation score."""
1956
- if not folders:
1957
- return 0
1958
-
1959
- scores = []
1960
- for folder in folders:
1961
- avg_score = folder.get('avg_documentation_quality', 0)
1962
- if avg_score > 0:
1963
- scores.append(avg_score)
1964
-
1965
- return round(sum(scores) / len(scores)) if scores else 0
1966
-
1967
- def _generate_improvements(self, analysis: Dict) -> List[Dict]:
1968
- """Generate improvement suggestions with enhanced analysis."""
1969
- improvements = []
1970
-
1971
- # Collection-level improvements
1972
- if analysis['score'] < 80:
1973
- improvements.append({
1974
- 'id': 'collection-quality',
1975
- 'title': 'Improve Overall Collection Quality',
1976
- 'description': f"Collection quality score is {analysis['score']}/100. Focus on addressing high-priority issues.",
1977
- 'priority': 'high',
1978
- 'category': 'quality',
1979
- 'impact': 'high'
1980
- })
1981
-
1982
- if analysis['overall_security_score'] < 70:
1983
- improvements.append({
1984
- 'id': 'security-enhancement',
1985
- 'title': 'Enhance Security Practices',
1986
- 'description': f"Security score is {analysis['overall_security_score']}/100. Review authentication and data handling.",
1987
- 'priority': 'high',
1988
- 'category': 'security',
1989
- 'impact': 'high'
1990
- })
1991
-
1992
- if analysis['overall_documentation_score'] < 60:
1993
- improvements.append({
1994
- 'id': 'documentation-improvement',
1995
- 'title': 'Improve Documentation',
1996
- 'description': f"Documentation score is {analysis['overall_documentation_score']}/100. Add descriptions and examples.",
1997
- 'priority': 'medium',
1998
- 'category': 'documentation',
1999
- 'impact': 'medium'
2000
- })
2001
-
2002
- # Add specific improvements based on common issues
2003
- issue_counts = {}
2004
- for folder in analysis.get('folders', []):
2005
- for request in folder.get('requests', []):
2006
- for issue in request.get('issues', []):
2007
- issue_type = issue.get('message', '')
2008
- issue_counts[issue_type] = issue_counts.get(
2009
- issue_type, 0) + 1
2010
-
2011
- # Generate improvements for most common issues
2012
- if issue_counts.get('Request lacks test scripts', 0) > 3:
2013
- improvements.append({
2014
- 'id': 'add-test-scripts',
2015
- 'title': 'Add Test Scripts to Requests',
2016
- 'description': f"Found {issue_counts['Request lacks test scripts']} requests without test scripts.",
2017
- 'priority': 'medium',
2018
- 'category': 'testing',
2019
- 'impact': 'medium'
2020
- })
2021
-
2022
- if issue_counts.get('Request contains hardcoded URL', 0) > 2:
2023
- improvements.append({
2024
- 'id': 'use-environment-variables',
2025
- 'title': 'Use Environment Variables',
2026
- 'description': f"Found {issue_counts['Request contains hardcoded URL']} requests with hardcoded URLs.",
2027
- 'priority': 'high',
2028
- 'category': 'maintainability',
2029
- 'impact': 'high'
2030
- })
2031
-
2032
- return improvements
1535
+ script_content = str(exec_content)
1536
+ break
1537
+
1538
+ if script_content is None:
1539
+ return json.dumps({"success": False, "message": f"No {script_type} script found for request '{request_path}'"}, indent=2)
1540
+
1541
+ return json.dumps({
1542
+ "success": True,
1543
+ "script_type": script_type,
1544
+ "script_content": script_content,
1545
+ "request_path": request_path
1546
+ }, indent=2)
1547
+
1548
+ except Exception as e:
1549
+ stacktrace = format_exc()
1550
+ logger.error(f"Exception when getting request {script_type} script: {stacktrace}")
1551
+ raise ToolException(f"Unable to get {script_type} script for request '{request_path}': {str(e)}")