alita-sdk 0.3.154__py3-none-any.whl → 0.3.156__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.
- alita_sdk/runtime/langchain/assistant.py +5 -3
- alita_sdk/tools/postman/__init__.py +1 -1
- alita_sdk/tools/postman/api_wrapper.py +651 -1199
- alita_sdk/tools/postman/postman_analysis.py +1133 -0
- {alita_sdk-0.3.154.dist-info → alita_sdk-0.3.156.dist-info}/METADATA +1 -1
- {alita_sdk-0.3.154.dist-info → alita_sdk-0.3.156.dist-info}/RECORD +9 -8
- {alita_sdk-0.3.154.dist-info → alita_sdk-0.3.156.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.154.dist-info → alita_sdk-0.3.156.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.154.dist-info → alita_sdk-0.3.156.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,15 @@
|
|
1
1
|
import json
|
2
2
|
import logging
|
3
3
|
import re
|
4
|
-
from typing import Any, Dict, List, Optional
|
4
|
+
from typing import Any, Dict, List, Optional
|
5
5
|
from traceback import format_exc
|
6
6
|
|
7
7
|
import requests
|
8
8
|
from langchain_core.tools import ToolException
|
9
|
-
from pydantic import Field,
|
9
|
+
from pydantic import Field, model_validator, create_model, SecretStr
|
10
10
|
|
11
11
|
from ..elitea_base import BaseToolApiWrapper
|
12
|
+
from .postman_analysis import PostmanAnalyzer
|
12
13
|
|
13
14
|
logger = logging.getLogger(__name__)
|
14
15
|
|
@@ -18,21 +19,17 @@ PostmanGetCollections = create_model(
|
|
18
19
|
)
|
19
20
|
|
20
21
|
PostmanGetCollection = create_model(
|
21
|
-
"PostmanGetCollection"
|
22
|
-
collection_id=(str, Field(
|
23
|
-
description="The ID of the collection to retrieve"))
|
22
|
+
"PostmanGetCollection"
|
24
23
|
)
|
25
24
|
|
26
25
|
PostmanGetFolder = create_model(
|
27
26
|
"PostmanGetFolder",
|
28
|
-
collection_id=(str, Field(description="The ID of the collection")),
|
29
27
|
folder_path=(str, Field(
|
30
28
|
description="The path to the folder (e.g., 'API/Users' for nested folders)"))
|
31
29
|
)
|
32
30
|
|
33
31
|
PostmanGetFolderRequests = create_model(
|
34
32
|
"PostmanGetFolderRequests",
|
35
|
-
collection_id=(str, Field(description="The ID of the collection")),
|
36
33
|
folder_path=(str, Field(description="The path to the folder")),
|
37
34
|
include_details=(bool, Field(
|
38
35
|
description="Include detailed request information", default=False))
|
@@ -40,8 +37,6 @@ PostmanGetFolderRequests = create_model(
|
|
40
37
|
|
41
38
|
PostmanSearchRequests = create_model(
|
42
39
|
"PostmanSearchRequests",
|
43
|
-
collection_id=(str, Field(
|
44
|
-
description="The ID of the collection to search in")),
|
45
40
|
query=(str, Field(description="The search query")),
|
46
41
|
search_in=(str, Field(
|
47
42
|
description="Where to search: name, url, description, all", default="all")),
|
@@ -51,20 +46,22 @@ PostmanSearchRequests = create_model(
|
|
51
46
|
|
52
47
|
PostmanAnalyzeCollection = create_model(
|
53
48
|
"PostmanAnalyzeCollection",
|
54
|
-
|
55
|
-
description="
|
49
|
+
include_improvements=(bool, Field(
|
50
|
+
description="Include improvement suggestions in the analysis", default=False))
|
56
51
|
)
|
57
52
|
|
58
53
|
PostmanAnalyzeFolder = create_model(
|
59
54
|
"PostmanAnalyzeFolder",
|
60
|
-
|
61
|
-
|
55
|
+
folder_path=(str, Field(description="The path to the folder to analyze")),
|
56
|
+
include_improvements=(bool, Field(
|
57
|
+
description="Include improvement suggestions in the analysis", default=False))
|
62
58
|
)
|
63
59
|
|
64
|
-
|
65
|
-
"
|
66
|
-
|
67
|
-
|
60
|
+
PostmanAnalyzeRequest = create_model(
|
61
|
+
"PostmanAnalyzeRequest",
|
62
|
+
request_path=(str, Field(description="The path to the request to analyze")),
|
63
|
+
include_improvements=(bool, Field(
|
64
|
+
description="Include improvement suggestions in the analysis", default=False))
|
68
65
|
)
|
69
66
|
|
70
67
|
PostmanCreateCollection = create_model(
|
@@ -78,36 +75,37 @@ PostmanCreateCollection = create_model(
|
|
78
75
|
description="Optional default authentication", default=None))
|
79
76
|
)
|
80
77
|
|
81
|
-
|
82
|
-
"
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
78
|
+
PostmanUpdateCollectionName = create_model(
|
79
|
+
"PostmanUpdateCollectionName",
|
80
|
+
name=(str, Field(description="New name for the collection"))
|
81
|
+
)
|
82
|
+
|
83
|
+
PostmanUpdateCollectionDescription = create_model(
|
84
|
+
"PostmanUpdateCollectionDescription",
|
85
|
+
description=(str, Field(description="New description for the collection"))
|
86
|
+
)
|
87
|
+
|
88
|
+
PostmanUpdateCollectionVariables = create_model(
|
89
|
+
"PostmanUpdateCollectionVariables",
|
90
|
+
variables=(List[Dict], Field(description="Updated collection variables"))
|
91
|
+
)
|
92
|
+
|
93
|
+
PostmanUpdateCollectionAuth = create_model(
|
94
|
+
"PostmanUpdateCollectionAuth",
|
95
|
+
auth=(Dict, Field(description="Updated authentication settings"))
|
93
96
|
)
|
94
97
|
|
95
98
|
PostmanDeleteCollection = create_model(
|
96
|
-
"PostmanDeleteCollection"
|
97
|
-
collection_id=(str, Field(
|
98
|
-
description="The ID of the collection to delete"))
|
99
|
+
"PostmanDeleteCollection"
|
99
100
|
)
|
100
101
|
|
101
102
|
PostmanDuplicateCollection = create_model(
|
102
103
|
"PostmanDuplicateCollection",
|
103
|
-
collection_id=(str, Field(
|
104
|
-
description="The ID of the collection to duplicate")),
|
105
104
|
new_name=(str, Field(description="Name for the new collection copy"))
|
106
105
|
)
|
107
106
|
|
108
107
|
PostmanCreateFolder = create_model(
|
109
108
|
"PostmanCreateFolder",
|
110
|
-
collection_id=(str, Field(description="The ID of the collection")),
|
111
109
|
name=(str, Field(description="Name of the new folder")),
|
112
110
|
description=(Optional[str], Field(
|
113
111
|
description="Optional description for the folder", default=None)),
|
@@ -119,7 +117,6 @@ PostmanCreateFolder = create_model(
|
|
119
117
|
|
120
118
|
PostmanUpdateFolder = create_model(
|
121
119
|
"PostmanUpdateFolder",
|
122
|
-
collection_id=(str, Field(description="The ID of the collection")),
|
123
120
|
folder_path=(str, Field(description="Path to the folder to update")),
|
124
121
|
name=(Optional[str], Field(
|
125
122
|
description="New name for the folder", default=None)),
|
@@ -131,13 +128,11 @@ PostmanUpdateFolder = create_model(
|
|
131
128
|
|
132
129
|
PostmanDeleteFolder = create_model(
|
133
130
|
"PostmanDeleteFolder",
|
134
|
-
collection_id=(str, Field(description="The ID of the collection")),
|
135
131
|
folder_path=(str, Field(description="Path to the folder to delete"))
|
136
132
|
)
|
137
133
|
|
138
134
|
PostmanMoveFolder = create_model(
|
139
135
|
"PostmanMoveFolder",
|
140
|
-
collection_id=(str, Field(description="The ID of the collection")),
|
141
136
|
source_path=(str, Field(description="Current path of the folder to move")),
|
142
137
|
target_path=(Optional[str], Field(
|
143
138
|
description="New parent folder path", default=None))
|
@@ -145,7 +140,6 @@ PostmanMoveFolder = create_model(
|
|
145
140
|
|
146
141
|
PostmanCreateRequest = create_model(
|
147
142
|
"PostmanCreateRequest",
|
148
|
-
collection_id=(str, Field(description="The ID of the collection")),
|
149
143
|
folder_path=(Optional[str], Field(
|
150
144
|
description="Path to the folder", default=None)),
|
151
145
|
name=(str, Field(description="Name of the new request")),
|
@@ -165,38 +159,67 @@ PostmanCreateRequest = create_model(
|
|
165
159
|
description="Optional pre-request script code", default=None))
|
166
160
|
)
|
167
161
|
|
168
|
-
|
169
|
-
"
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
162
|
+
PostmanUpdateRequestName = create_model(
|
163
|
+
"PostmanUpdateRequestName",
|
164
|
+
request_path=(str, Field(description="Path to the request (folder/requestName)")),
|
165
|
+
name=(str, Field(description="New name for the request"))
|
166
|
+
)
|
167
|
+
|
168
|
+
PostmanUpdateRequestMethod = create_model(
|
169
|
+
"PostmanUpdateRequestMethod",
|
170
|
+
request_path=(str, Field(description="Path to the request (folder/requestName)")),
|
171
|
+
method=(str, Field(description="HTTP method for the request"))
|
172
|
+
)
|
173
|
+
|
174
|
+
PostmanUpdateRequestUrl = create_model(
|
175
|
+
"PostmanUpdateRequestUrl",
|
176
|
+
request_path=(str, Field(description="Path to the request (folder/requestName)")),
|
177
|
+
url=(str, Field(description="URL for the request"))
|
178
|
+
)
|
179
|
+
|
180
|
+
PostmanUpdateRequestDescription = create_model(
|
181
|
+
"PostmanUpdateRequestDescription",
|
182
|
+
request_path=(str, Field(description="Path to the request (folder/requestName)")),
|
183
|
+
description=(str, Field(description="Description for the request"))
|
184
|
+
)
|
185
|
+
|
186
|
+
PostmanUpdateRequestHeaders = create_model(
|
187
|
+
"PostmanUpdateRequestHeaders",
|
188
|
+
request_path=(str, Field(description="Path to the request (folder/requestName)")),
|
189
|
+
headers=(List[Dict], Field(description="Request headers"))
|
190
|
+
)
|
191
|
+
|
192
|
+
PostmanUpdateRequestBody = create_model(
|
193
|
+
"PostmanUpdateRequestBody",
|
194
|
+
request_path=(str, Field(description="Path to the request (folder/requestName)")),
|
195
|
+
body=(Dict, Field(description="Request body"))
|
196
|
+
)
|
197
|
+
|
198
|
+
PostmanUpdateRequestAuth = create_model(
|
199
|
+
"PostmanUpdateRequestAuth",
|
200
|
+
request_path=(str, Field(description="Path to the request (folder/requestName)")),
|
201
|
+
auth=(Dict, Field(description="Request authentication"))
|
202
|
+
)
|
203
|
+
|
204
|
+
PostmanUpdateRequestTests = create_model(
|
205
|
+
"PostmanUpdateRequestTests",
|
206
|
+
request_path=(str, Field(description="Path to the request (folder/requestName)")),
|
207
|
+
tests=(str, Field(description="Test script code"))
|
208
|
+
)
|
209
|
+
|
210
|
+
PostmanUpdateRequestPreScript = create_model(
|
211
|
+
"PostmanUpdateRequestPreScript",
|
212
|
+
request_path=(str, Field(description="Path to the request (folder/requestName)")),
|
213
|
+
pre_request_script=(str, Field(description="Pre-request script code"))
|
189
214
|
)
|
190
215
|
|
191
216
|
PostmanDeleteRequest = create_model(
|
192
217
|
"PostmanDeleteRequest",
|
193
|
-
collection_id=(str, Field(description="The ID of the collection")),
|
194
218
|
request_path=(str, Field(description="Path to the request to delete"))
|
195
219
|
)
|
196
220
|
|
197
221
|
PostmanDuplicateRequest = create_model(
|
198
222
|
"PostmanDuplicateRequest",
|
199
|
-
collection_id=(str, Field(description="The ID of the collection")),
|
200
223
|
source_path=(str, Field(description="Path to the request to duplicate")),
|
201
224
|
new_name=(str, Field(description="Name for the duplicated request")),
|
202
225
|
target_path=(Optional[str], Field(
|
@@ -205,7 +228,6 @@ PostmanDuplicateRequest = create_model(
|
|
205
228
|
|
206
229
|
PostmanMoveRequest = create_model(
|
207
230
|
"PostmanMoveRequest",
|
208
|
-
collection_id=(str, Field(description="The ID of the collection")),
|
209
231
|
source_path=(str, Field(
|
210
232
|
description="Current path of the request to move")),
|
211
233
|
target_path=(Optional[str], Field(
|
@@ -221,7 +243,12 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
221
243
|
collection_id: Optional[str] = None
|
222
244
|
workspace_id: Optional[str] = None
|
223
245
|
timeout: int = 30
|
224
|
-
|
246
|
+
session: Any = None
|
247
|
+
analyzer: PostmanAnalyzer = None
|
248
|
+
|
249
|
+
model_config = {
|
250
|
+
"arbitrary_types_allowed": True
|
251
|
+
}
|
225
252
|
|
226
253
|
@model_validator(mode='before')
|
227
254
|
@classmethod
|
@@ -233,16 +260,14 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
233
260
|
"`requests` package not found, please run "
|
234
261
|
"`pip install requests`"
|
235
262
|
)
|
263
|
+
values["session"] = requests.Session()
|
264
|
+
values["session"].headers.update({
|
265
|
+
'X-API-Key': values.get('api_key'),
|
266
|
+
'Content-Type': 'application/json'
|
267
|
+
})
|
268
|
+
values["analyzer"] = PostmanAnalyzer()
|
236
269
|
return values
|
237
270
|
|
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
271
|
|
247
272
|
def _make_request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
|
248
273
|
"""Make HTTP request to Postman API."""
|
@@ -250,7 +275,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
250
275
|
|
251
276
|
try:
|
252
277
|
logger.info(f"Making {method.upper()} request to {url}")
|
253
|
-
response = self.
|
278
|
+
response = self.session.request(method, url, timeout=self.timeout, **kwargs)
|
254
279
|
response.raise_for_status()
|
255
280
|
|
256
281
|
if response.content:
|
@@ -318,11 +343,11 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
318
343
|
"ref": self.analyze_folder
|
319
344
|
},
|
320
345
|
{
|
321
|
-
"name": "
|
322
|
-
"mode": "
|
323
|
-
"description": "
|
324
|
-
"args_schema":
|
325
|
-
"ref": self.
|
346
|
+
"name": "analyze_request",
|
347
|
+
"mode": "analyze_request",
|
348
|
+
"description": "Analyze a specific request within a collection",
|
349
|
+
"args_schema": PostmanAnalyzeRequest,
|
350
|
+
"ref": self.analyze_request
|
326
351
|
},
|
327
352
|
{
|
328
353
|
"name": "create_collection",
|
@@ -332,19 +357,40 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
332
357
|
"ref": self.create_collection
|
333
358
|
},
|
334
359
|
{
|
335
|
-
"name": "
|
336
|
-
"mode": "
|
337
|
-
"description": "Update
|
338
|
-
"args_schema":
|
339
|
-
"ref": self.
|
360
|
+
"name": "update_collection_name",
|
361
|
+
"mode": "update_collection_name",
|
362
|
+
"description": "Update collection name",
|
363
|
+
"args_schema": PostmanUpdateCollectionName,
|
364
|
+
"ref": self.update_collection_name
|
340
365
|
},
|
341
366
|
{
|
342
|
-
"name": "
|
343
|
-
"mode": "
|
344
|
-
"description": "
|
345
|
-
"args_schema":
|
346
|
-
"ref": self.
|
367
|
+
"name": "update_collection_description",
|
368
|
+
"mode": "update_collection_description",
|
369
|
+
"description": "Update collection description",
|
370
|
+
"args_schema": PostmanUpdateCollectionDescription,
|
371
|
+
"ref": self.update_collection_description
|
347
372
|
},
|
373
|
+
{
|
374
|
+
"name": "update_collection_variables",
|
375
|
+
"mode": "update_collection_variables",
|
376
|
+
"description": "Update collection variables",
|
377
|
+
"args_schema": PostmanUpdateCollectionVariables,
|
378
|
+
"ref": self.update_collection_variables
|
379
|
+
},
|
380
|
+
{
|
381
|
+
"name": "update_collection_auth",
|
382
|
+
"mode": "update_collection_auth",
|
383
|
+
"description": "Update collection authentication settings",
|
384
|
+
"args_schema": PostmanUpdateCollectionAuth,
|
385
|
+
"ref": self.update_collection_auth
|
386
|
+
},
|
387
|
+
# {
|
388
|
+
# "name": "delete_collection",
|
389
|
+
# "mode": "delete_collection",
|
390
|
+
# "description": "Delete a collection permanently",
|
391
|
+
# "args_schema": PostmanDeleteCollection,
|
392
|
+
# "ref": self.delete_collection
|
393
|
+
# },
|
348
394
|
{
|
349
395
|
"name": "duplicate_collection",
|
350
396
|
"mode": "duplicate_collection",
|
@@ -367,13 +413,13 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
367
413
|
"args_schema": PostmanUpdateFolder,
|
368
414
|
"ref": self.update_folder
|
369
415
|
},
|
370
|
-
{
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
},
|
416
|
+
# {
|
417
|
+
# "name": "delete_folder",
|
418
|
+
# "mode": "delete_folder",
|
419
|
+
# "description": "Delete a folder and all its contents permanently",
|
420
|
+
# "args_schema": PostmanDeleteFolder,
|
421
|
+
# "ref": self.delete_folder
|
422
|
+
# },
|
377
423
|
{
|
378
424
|
"name": "move_folder",
|
379
425
|
"mode": "move_folder",
|
@@ -389,19 +435,75 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
389
435
|
"ref": self.create_request
|
390
436
|
},
|
391
437
|
{
|
392
|
-
"name": "
|
393
|
-
"mode": "
|
394
|
-
"description": "Update
|
395
|
-
"args_schema":
|
396
|
-
"ref": self.
|
438
|
+
"name": "update_request_name",
|
439
|
+
"mode": "update_request_name",
|
440
|
+
"description": "Update request name",
|
441
|
+
"args_schema": PostmanUpdateRequestName,
|
442
|
+
"ref": self.update_request_name
|
443
|
+
},
|
444
|
+
{
|
445
|
+
"name": "update_request_method",
|
446
|
+
"mode": "update_request_method",
|
447
|
+
"description": "Update request HTTP method",
|
448
|
+
"args_schema": PostmanUpdateRequestMethod,
|
449
|
+
"ref": self.update_request_method
|
450
|
+
},
|
451
|
+
{
|
452
|
+
"name": "update_request_url",
|
453
|
+
"mode": "update_request_url",
|
454
|
+
"description": "Update request URL",
|
455
|
+
"args_schema": PostmanUpdateRequestUrl,
|
456
|
+
"ref": self.update_request_url
|
457
|
+
},
|
458
|
+
{
|
459
|
+
"name": "update_request_description",
|
460
|
+
"mode": "update_request_description",
|
461
|
+
"description": "Update request description",
|
462
|
+
"args_schema": PostmanUpdateRequestDescription,
|
463
|
+
"ref": self.update_request_description
|
464
|
+
},
|
465
|
+
{
|
466
|
+
"name": "update_request_headers",
|
467
|
+
"mode": "update_request_headers",
|
468
|
+
"description": "Update request headers",
|
469
|
+
"args_schema": PostmanUpdateRequestHeaders,
|
470
|
+
"ref": self.update_request_headers
|
471
|
+
},
|
472
|
+
{
|
473
|
+
"name": "update_request_body",
|
474
|
+
"mode": "update_request_body",
|
475
|
+
"description": "Update request body",
|
476
|
+
"args_schema": PostmanUpdateRequestBody,
|
477
|
+
"ref": self.update_request_body
|
478
|
+
},
|
479
|
+
{
|
480
|
+
"name": "update_request_auth",
|
481
|
+
"mode": "update_request_auth",
|
482
|
+
"description": "Update request authentication",
|
483
|
+
"args_schema": PostmanUpdateRequestAuth,
|
484
|
+
"ref": self.update_request_auth
|
485
|
+
},
|
486
|
+
{
|
487
|
+
"name": "update_request_tests",
|
488
|
+
"mode": "update_request_tests",
|
489
|
+
"description": "Update request test scripts",
|
490
|
+
"args_schema": PostmanUpdateRequestTests,
|
491
|
+
"ref": self.update_request_tests
|
397
492
|
},
|
398
493
|
{
|
399
|
-
"name": "
|
400
|
-
"mode": "
|
401
|
-
"description": "
|
402
|
-
"args_schema":
|
403
|
-
"ref": self.
|
494
|
+
"name": "update_request_pre_script",
|
495
|
+
"mode": "update_request_pre_script",
|
496
|
+
"description": "Update request pre-request scripts",
|
497
|
+
"args_schema": PostmanUpdateRequestPreScript,
|
498
|
+
"ref": self.update_request_pre_script
|
404
499
|
},
|
500
|
+
# {
|
501
|
+
# "name": "delete_request",
|
502
|
+
# "mode": "delete_request",
|
503
|
+
# "description": "Delete an API request permanently",
|
504
|
+
# "args_schema": PostmanDeleteRequest,
|
505
|
+
# "ref": self.delete_request
|
506
|
+
# },
|
405
507
|
{
|
406
508
|
"name": "duplicate_request",
|
407
509
|
"mode": "duplicate_request",
|
@@ -432,25 +534,25 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
432
534
|
logger.error(f"Exception when getting collections: {stacktrace}")
|
433
535
|
raise ToolException(f"Unable to get collections: {str(e)}")
|
434
536
|
|
435
|
-
def get_collection(self,
|
537
|
+
def get_collection(self, **kwargs) -> str:
|
436
538
|
"""Get a specific collection by ID."""
|
437
539
|
try:
|
438
540
|
response = self._make_request(
|
439
|
-
'GET', f'/collections/{collection_id}')
|
541
|
+
'GET', f'/collections/{self.collection_id}')
|
440
542
|
return json.dumps(response, indent=2)
|
441
543
|
except Exception as e:
|
442
544
|
stacktrace = format_exc()
|
443
545
|
logger.error(
|
444
|
-
f"Exception when getting collection {collection_id}: {stacktrace}")
|
546
|
+
f"Exception when getting collection {self.collection_id}: {stacktrace}")
|
445
547
|
raise ToolException(
|
446
|
-
f"Unable to get collection {collection_id}: {str(e)}")
|
548
|
+
f"Unable to get collection {self.collection_id}: {str(e)}")
|
447
549
|
|
448
|
-
def get_folder(self,
|
550
|
+
def get_folder(self, folder_path: str, **kwargs) -> str:
|
449
551
|
"""Get folders from a collection by path."""
|
450
552
|
try:
|
451
553
|
collection = self._make_request(
|
452
|
-
'GET', f'/collections/{collection_id}')
|
453
|
-
folders = self.
|
554
|
+
'GET', f'/collections/{self.collection_id}')
|
555
|
+
folders = self.analyzer.find_folders_by_path(
|
454
556
|
collection['collection']['item'], folder_path)
|
455
557
|
return json.dumps(folders, indent=2)
|
456
558
|
except Exception as e:
|
@@ -458,21 +560,21 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
458
560
|
logger.error(
|
459
561
|
f"Exception when getting folder {folder_path}: {stacktrace}")
|
460
562
|
raise ToolException(
|
461
|
-
f"Unable to get folder {folder_path} from collection {collection_id}: {str(e)}")
|
563
|
+
f"Unable to get folder {folder_path} from collection {self.collection_id}: {str(e)}")
|
462
564
|
|
463
|
-
def get_folder_requests(self,
|
565
|
+
def get_folder_requests(self, folder_path: str, include_details: bool = False, **kwargs) -> str:
|
464
566
|
"""Get detailed information about all requests in a folder."""
|
465
567
|
try:
|
466
568
|
collection = self._make_request(
|
467
|
-
'GET', f'/collections/{collection_id}')
|
468
|
-
folders = self.
|
569
|
+
'GET', f'/collections/{self.collection_id}')
|
570
|
+
folders = self.analyzer.find_folders_by_path(
|
469
571
|
collection['collection']['item'], folder_path)
|
470
572
|
|
471
573
|
if not folders:
|
472
|
-
raise ToolException(f"Folder '{folder_path}' not found in collection '{collection_id}'.")
|
574
|
+
raise ToolException(f"Folder '{folder_path}' not found in collection '{self.collection_id}'.")
|
473
575
|
|
474
576
|
folder = folders[0]
|
475
|
-
requests = self.
|
577
|
+
requests = self.analyzer.extract_requests_from_items(
|
476
578
|
folder.get('item', []), include_details)
|
477
579
|
|
478
580
|
result = {
|
@@ -490,16 +592,16 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
490
592
|
raise ToolException(
|
491
593
|
f"Unable to get requests from folder {folder_path}: {str(e)}")
|
492
594
|
|
493
|
-
def search_requests(self,
|
595
|
+
def search_requests(self, query: str, search_in: str = "all", method: str = None, **kwargs) -> str:
|
494
596
|
"""Search for requests across the collection."""
|
495
597
|
try:
|
496
598
|
collection = self._make_request(
|
497
|
-
'GET', f'/collections/{collection_id}')
|
498
|
-
requests = self.
|
599
|
+
'GET', f'/collections/{self.collection_id}')
|
600
|
+
requests = self.analyzer.search_requests_in_items(
|
499
601
|
collection['collection']['item'], query, search_in, method)
|
500
602
|
|
501
603
|
result = {
|
502
|
-
"collection_id": collection_id,
|
604
|
+
"collection_id": self.collection_id,
|
503
605
|
"query": query,
|
504
606
|
"search_in": search_in,
|
505
607
|
"method_filter": method,
|
@@ -512,27 +614,33 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
512
614
|
stacktrace = format_exc()
|
513
615
|
logger.error(f"Exception when searching requests: {stacktrace}")
|
514
616
|
raise ToolException(
|
515
|
-
f"Unable to search requests in collection {collection_id}: {str(e)}")
|
617
|
+
f"Unable to search requests in collection {self.collection_id}: {str(e)}")
|
516
618
|
|
517
|
-
def analyze_collection(self,
|
619
|
+
def analyze_collection(self, include_improvements: bool = False, **kwargs) -> str:
|
518
620
|
"""Analyze a collection for API quality and best practices."""
|
519
621
|
try:
|
520
622
|
collection = self._make_request(
|
521
|
-
'GET', f'/collections/{collection_id}')
|
522
|
-
analysis = self.
|
623
|
+
'GET', f'/collections/{self.collection_id}')
|
624
|
+
analysis = self.analyzer.perform_collection_analysis(collection)
|
625
|
+
|
626
|
+
if include_improvements:
|
627
|
+
improvements = self.analyzer.generate_improvements(analysis)
|
628
|
+
analysis["improvements"] = improvements
|
629
|
+
analysis["improvement_count"] = len(improvements)
|
630
|
+
|
523
631
|
return json.dumps(analysis, indent=2)
|
524
632
|
except Exception as e:
|
525
633
|
stacktrace = format_exc()
|
526
634
|
logger.error(f"Exception when analyzing collection: {stacktrace}")
|
527
635
|
raise ToolException(
|
528
|
-
f"Unable to analyze collection {collection_id}: {str(e)}")
|
636
|
+
f"Unable to analyze collection {self.collection_id}: {str(e)}")
|
529
637
|
|
530
|
-
def analyze_folder(self,
|
638
|
+
def analyze_folder(self, folder_path: str, include_improvements: bool = False, **kwargs) -> str:
|
531
639
|
"""Analyze a specific folder within a collection."""
|
532
640
|
try:
|
533
641
|
collection = self._make_request(
|
534
|
-
'GET', f'/collections/{collection_id}')
|
535
|
-
folders = self.
|
642
|
+
'GET', f'/collections/{self.collection_id}')
|
643
|
+
folders = self.analyzer.find_folders_by_path(
|
536
644
|
collection['collection']['item'], folder_path)
|
537
645
|
|
538
646
|
if not folders:
|
@@ -540,7 +648,13 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
540
648
|
|
541
649
|
folder_analyses = []
|
542
650
|
for folder in folders:
|
543
|
-
analysis = self.
|
651
|
+
analysis = self.analyzer.perform_folder_analysis(folder, folder_path)
|
652
|
+
|
653
|
+
if include_improvements:
|
654
|
+
improvements = self.analyzer.generate_folder_improvements(analysis)
|
655
|
+
analysis["improvements"] = improvements
|
656
|
+
analysis["improvement_count"] = len(improvements)
|
657
|
+
|
544
658
|
folder_analyses.append(analysis)
|
545
659
|
|
546
660
|
return json.dumps(folder_analyses, indent=2)
|
@@ -550,28 +664,35 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
550
664
|
raise ToolException(
|
551
665
|
f"Unable to analyze folder {folder_path}: {str(e)}")
|
552
666
|
|
553
|
-
def
|
554
|
-
"""
|
667
|
+
def analyze_request(self, request_path: str, include_improvements: bool = False, **kwargs) -> str:
|
668
|
+
"""Analyze a specific request within a collection."""
|
555
669
|
try:
|
556
670
|
collection = self._make_request(
|
557
|
-
'GET', f'/collections/{collection_id}')
|
558
|
-
|
559
|
-
improvements = self._generate_improvements(analysis)
|
671
|
+
'GET', f'/collections/{self.collection_id}')
|
672
|
+
collection_data = collection["collection"]
|
560
673
|
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
"
|
566
|
-
}
|
674
|
+
# Find the request
|
675
|
+
request_item = self.analyzer.find_request_by_path(
|
676
|
+
collection_data["item"], request_path)
|
677
|
+
if not request_item:
|
678
|
+
raise ToolException(f"Request '{request_path}' not found")
|
567
679
|
|
568
|
-
|
680
|
+
# Perform request analysis
|
681
|
+
analysis = self.analyzer.perform_request_analysis(request_item)
|
682
|
+
analysis["request_path"] = request_path
|
683
|
+
analysis["collection_id"] = self.collection_id
|
684
|
+
|
685
|
+
if include_improvements:
|
686
|
+
improvements = self.analyzer.generate_request_improvements(analysis)
|
687
|
+
analysis["improvements"] = improvements
|
688
|
+
analysis["improvement_count"] = len(improvements)
|
689
|
+
|
690
|
+
return json.dumps(analysis, indent=2)
|
569
691
|
except Exception as e:
|
570
692
|
stacktrace = format_exc()
|
571
|
-
logger.error(
|
572
|
-
f"Exception when generating improvements: {stacktrace}")
|
693
|
+
logger.error(f"Exception when analyzing request: {stacktrace}")
|
573
694
|
raise ToolException(
|
574
|
-
f"Unable to
|
695
|
+
f"Unable to analyze request {request_path}: {str(e)}")
|
575
696
|
|
576
697
|
# =================================================================
|
577
698
|
# COLLECTION MANAGEMENT METHODS
|
@@ -608,52 +729,104 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
608
729
|
raise ToolException(
|
609
730
|
f"Unable to create collection '{name}': {str(e)}")
|
610
731
|
|
611
|
-
def
|
612
|
-
|
613
|
-
"""Update an existing collection."""
|
732
|
+
def update_collection_name(self, name: str, **kwargs) -> str:
|
733
|
+
"""Update collection name."""
|
614
734
|
try:
|
615
735
|
# Get current collection
|
616
736
|
current = self._make_request(
|
617
|
-
'GET', f'/collections/{collection_id}')
|
737
|
+
'GET', f'/collections/{self.collection_id}')
|
618
738
|
collection_data = current["collection"]
|
619
739
|
|
620
|
-
# Update
|
621
|
-
|
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
|
740
|
+
# Update name
|
741
|
+
collection_data["info"]["name"] = name
|
629
742
|
|
630
|
-
response = self._make_request('PUT', f'/collections/{collection_id}',
|
743
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
631
744
|
json={"collection": collection_data})
|
632
745
|
return json.dumps(response, indent=2)
|
633
746
|
except Exception as e:
|
634
747
|
stacktrace = format_exc()
|
635
|
-
logger.error(f"Exception when updating collection: {stacktrace}")
|
748
|
+
logger.error(f"Exception when updating collection name: {stacktrace}")
|
636
749
|
raise ToolException(
|
637
|
-
f"Unable to update collection {collection_id}: {str(e)}")
|
750
|
+
f"Unable to update collection {self.collection_id} name: {str(e)}")
|
638
751
|
|
639
|
-
def
|
752
|
+
def update_collection_description(self, description: str, **kwargs) -> str:
|
753
|
+
"""Update collection description."""
|
754
|
+
try:
|
755
|
+
# Get current collection
|
756
|
+
current = self._make_request(
|
757
|
+
'GET', f'/collections/{self.collection_id}')
|
758
|
+
collection_data = current["collection"]
|
759
|
+
|
760
|
+
# Update description
|
761
|
+
collection_data["info"]["description"] = description
|
762
|
+
|
763
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
764
|
+
json={"collection": collection_data})
|
765
|
+
return json.dumps(response, indent=2)
|
766
|
+
except Exception as e:
|
767
|
+
stacktrace = format_exc()
|
768
|
+
logger.error(f"Exception when updating collection description: {stacktrace}")
|
769
|
+
raise ToolException(
|
770
|
+
f"Unable to update collection {self.collection_id} description: {str(e)}")
|
771
|
+
|
772
|
+
def update_collection_variables(self, variables: List[Dict], **kwargs) -> str:
|
773
|
+
"""Update collection variables."""
|
774
|
+
try:
|
775
|
+
# Get current collection
|
776
|
+
current = self._make_request(
|
777
|
+
'GET', f'/collections/{self.collection_id}')
|
778
|
+
collection_data = current["collection"]
|
779
|
+
|
780
|
+
# Update variables
|
781
|
+
collection_data["variable"] = variables
|
782
|
+
|
783
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
784
|
+
json={"collection": collection_data})
|
785
|
+
return json.dumps(response, indent=2)
|
786
|
+
except Exception as e:
|
787
|
+
stacktrace = format_exc()
|
788
|
+
logger.error(f"Exception when updating collection variables: {stacktrace}")
|
789
|
+
raise ToolException(
|
790
|
+
f"Unable to update collection {self.collection_id} variables: {str(e)}")
|
791
|
+
|
792
|
+
def update_collection_auth(self, auth: Dict, **kwargs) -> str:
|
793
|
+
"""Update collection authentication settings."""
|
794
|
+
try:
|
795
|
+
# Get current collection
|
796
|
+
current = self._make_request(
|
797
|
+
'GET', f'/collections/{self.collection_id}')
|
798
|
+
collection_data = current["collection"]
|
799
|
+
|
800
|
+
# Update auth
|
801
|
+
collection_data["auth"] = auth
|
802
|
+
|
803
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
804
|
+
json={"collection": collection_data})
|
805
|
+
return json.dumps(response, indent=2)
|
806
|
+
except Exception as e:
|
807
|
+
stacktrace = format_exc()
|
808
|
+
logger.error(f"Exception when updating collection auth: {stacktrace}")
|
809
|
+
raise ToolException(
|
810
|
+
f"Unable to update collection {self.collection_id} auth: {str(e)}")
|
811
|
+
|
812
|
+
def delete_collection(self, **kwargs) -> str:
|
640
813
|
"""Delete a collection permanently."""
|
641
814
|
try:
|
642
815
|
response = self._make_request(
|
643
|
-
'DELETE', f'/collections/{collection_id}')
|
644
|
-
return json.dumps({"message": f"Collection {collection_id} deleted successfully"}, indent=2)
|
816
|
+
'DELETE', f'/collections/{self.collection_id}')
|
817
|
+
return json.dumps({"message": f"Collection {self.collection_id} deleted successfully"}, indent=2)
|
645
818
|
except Exception as e:
|
646
819
|
stacktrace = format_exc()
|
647
820
|
logger.error(f"Exception when deleting collection: {stacktrace}")
|
648
821
|
raise ToolException(
|
649
|
-
f"Unable to delete collection {collection_id}: {str(e)}")
|
822
|
+
f"Unable to delete collection {self.collection_id}: {str(e)}")
|
650
823
|
|
651
|
-
def duplicate_collection(self,
|
824
|
+
def duplicate_collection(self, new_name: str, **kwargs) -> str:
|
652
825
|
"""Create a copy of an existing collection."""
|
653
826
|
try:
|
654
827
|
# Get the original collection
|
655
828
|
original = self._make_request(
|
656
|
-
'GET', f'/collections/{collection_id}')
|
829
|
+
'GET', f'/collections/{self.collection_id}')
|
657
830
|
collection_data = original["collection"]
|
658
831
|
|
659
832
|
# Update the name and remove IDs to create a new collection
|
@@ -662,7 +835,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
662
835
|
del collection_data["info"]["_postman_id"]
|
663
836
|
|
664
837
|
# Remove item IDs recursively
|
665
|
-
self.
|
838
|
+
self.analyzer.remove_item_ids(collection_data.get("item", []))
|
666
839
|
|
667
840
|
response = self._make_request(
|
668
841
|
'POST', '/collections', json={"collection": collection_data})
|
@@ -672,19 +845,19 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
672
845
|
logger.error(
|
673
846
|
f"Exception when duplicating collection: {stacktrace}")
|
674
847
|
raise ToolException(
|
675
|
-
f"Unable to duplicate collection {collection_id}: {str(e)}")
|
848
|
+
f"Unable to duplicate collection {self.collection_id}: {str(e)}")
|
676
849
|
|
677
850
|
# =================================================================
|
678
851
|
# FOLDER MANAGEMENT METHODS
|
679
852
|
# =================================================================
|
680
853
|
|
681
|
-
def create_folder(self,
|
854
|
+
def create_folder(self, name: str, description: str = None,
|
682
855
|
parent_path: str = None, auth: Dict = None, **kwargs) -> str:
|
683
856
|
"""Create a new folder in a collection."""
|
684
857
|
try:
|
685
858
|
# Get current collection
|
686
859
|
collection = self._make_request(
|
687
|
-
'GET', f'/collections/{collection_id}')
|
860
|
+
'GET', f'/collections/{self.collection_id}')
|
688
861
|
collection_data = collection["collection"]
|
689
862
|
|
690
863
|
# Create folder item
|
@@ -700,7 +873,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
700
873
|
|
701
874
|
# Add folder to appropriate location
|
702
875
|
if parent_path:
|
703
|
-
parent_folders = self.
|
876
|
+
parent_folders = self.analyzer.find_folders_by_path(
|
704
877
|
collection_data["item"], parent_path)
|
705
878
|
if not parent_folders:
|
706
879
|
raise ToolException(
|
@@ -710,7 +883,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
710
883
|
collection_data["item"].append(folder_item)
|
711
884
|
|
712
885
|
# Update collection
|
713
|
-
response = self._make_request('PUT', f'/collections/{collection_id}',
|
886
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
714
887
|
json={"collection": collection_data})
|
715
888
|
return json.dumps({"message": f"Folder '{name}' created successfully"}, indent=2)
|
716
889
|
except Exception as e:
|
@@ -718,17 +891,17 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
718
891
|
logger.error(f"Exception when creating folder: {stacktrace}")
|
719
892
|
raise ToolException(f"Unable to create folder '{name}': {str(e)}")
|
720
893
|
|
721
|
-
def update_folder(self,
|
894
|
+
def update_folder(self, folder_path: str, name: str = None,
|
722
895
|
description: str = None, auth: Dict = None, **kwargs) -> str:
|
723
896
|
"""Update folder properties."""
|
724
897
|
try:
|
725
898
|
# Get current collection
|
726
899
|
collection = self._make_request(
|
727
|
-
'GET', f'/collections/{collection_id}')
|
900
|
+
'GET', f'/collections/{self.collection_id}')
|
728
901
|
collection_data = collection["collection"]
|
729
902
|
|
730
903
|
# Find the folder
|
731
|
-
folders = self.
|
904
|
+
folders = self.analyzer.find_folders_by_path(
|
732
905
|
collection_data["item"], folder_path)
|
733
906
|
if not folders:
|
734
907
|
raise ToolException(f"Folder '{folder_path}' not found")
|
@@ -744,7 +917,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
744
917
|
folder["auth"] = auth
|
745
918
|
|
746
919
|
# Update collection
|
747
|
-
response = self._make_request('PUT', f'/collections/{collection_id}',
|
920
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
748
921
|
json={"collection": collection_data})
|
749
922
|
return json.dumps({"message": f"Folder '{folder_path}' updated successfully"}, indent=2)
|
750
923
|
except Exception as e:
|
@@ -753,18 +926,18 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
753
926
|
raise ToolException(
|
754
927
|
f"Unable to update folder '{folder_path}': {str(e)}")
|
755
928
|
|
756
|
-
def delete_folder(self,
|
929
|
+
def delete_folder(self, folder_path: str, **kwargs) -> str:
|
757
930
|
"""Delete a folder and all its contents permanently."""
|
758
931
|
try:
|
759
932
|
# Get current collection
|
760
933
|
collection = self._make_request(
|
761
|
-
'GET', f'/collections/{collection_id}')
|
934
|
+
'GET', f'/collections/{self.collection_id}')
|
762
935
|
collection_data = collection["collection"]
|
763
936
|
|
764
937
|
# Find and remove the folder
|
765
|
-
if self.
|
938
|
+
if self.analyzer.remove_folder_by_path(collection_data["item"], folder_path):
|
766
939
|
# Update collection
|
767
|
-
response = self._make_request('PUT', f'/collections/{collection_id}',
|
940
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
768
941
|
json={"collection": collection_data})
|
769
942
|
return json.dumps({"message": f"Folder '{folder_path}' deleted successfully"}, indent=2)
|
770
943
|
else:
|
@@ -775,16 +948,16 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
775
948
|
raise ToolException(
|
776
949
|
f"Unable to delete folder '{folder_path}': {str(e)}")
|
777
950
|
|
778
|
-
def move_folder(self,
|
951
|
+
def move_folder(self, source_path: str, target_path: str = None, **kwargs) -> str:
|
779
952
|
"""Move a folder to a different location within the collection."""
|
780
953
|
try:
|
781
954
|
# Get current collection
|
782
955
|
collection = self._make_request(
|
783
|
-
'GET', f'/collections/{collection_id}')
|
956
|
+
'GET', f'/collections/{self.collection_id}')
|
784
957
|
collection_data = collection["collection"]
|
785
958
|
|
786
959
|
# Find source folder
|
787
|
-
source_folder = self.
|
960
|
+
source_folder = self.analyzer.find_folders_by_path(
|
788
961
|
collection_data["item"], source_path)
|
789
962
|
if not source_folder:
|
790
963
|
raise ToolException(f"Source folder '{source_path}' not found")
|
@@ -792,11 +965,11 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
792
965
|
folder_data = source_folder[0].copy()
|
793
966
|
|
794
967
|
# Remove from source location
|
795
|
-
self.
|
968
|
+
self.analyzer.remove_folder_by_path(collection_data["item"], source_path)
|
796
969
|
|
797
970
|
# Add to target location
|
798
971
|
if target_path:
|
799
|
-
target_folders = self.
|
972
|
+
target_folders = self.analyzer.find_folders_by_path(
|
800
973
|
collection_data["item"], target_path)
|
801
974
|
if not target_folders:
|
802
975
|
raise ToolException(
|
@@ -806,7 +979,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
806
979
|
collection_data["item"].append(folder_data)
|
807
980
|
|
808
981
|
# Update collection
|
809
|
-
response = self._make_request('PUT', f'/collections/{collection_id}',
|
982
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
810
983
|
json={"collection": collection_data})
|
811
984
|
return json.dumps({"message": f"Folder moved from '{source_path}' to '{target_path or 'root'}'"}, indent=2)
|
812
985
|
except Exception as e:
|
@@ -819,7 +992,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
819
992
|
# REQUEST MANAGEMENT METHODS
|
820
993
|
# =================================================================
|
821
994
|
|
822
|
-
def create_request(self,
|
995
|
+
def create_request(self, name: str, method: str, url: str,
|
823
996
|
folder_path: str = None, description: str = None, headers: List[Dict] = None,
|
824
997
|
body: Dict = None, auth: Dict = None, tests: str = None,
|
825
998
|
pre_request_script: str = None, **kwargs) -> str:
|
@@ -827,7 +1000,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
827
1000
|
try:
|
828
1001
|
# Get current collection
|
829
1002
|
collection = self._make_request(
|
830
|
-
'GET', f'/collections/{collection_id}')
|
1003
|
+
'GET', f'/collections/{self.collection_id}')
|
831
1004
|
collection_data = collection["collection"]
|
832
1005
|
|
833
1006
|
# Create request item
|
@@ -870,7 +1043,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
870
1043
|
|
871
1044
|
# Add request to appropriate location
|
872
1045
|
if folder_path:
|
873
|
-
folders = self.
|
1046
|
+
folders = self.analyzer.find_folders_by_path(
|
874
1047
|
collection_data["item"], folder_path)
|
875
1048
|
if not folders:
|
876
1049
|
raise ToolException(f"Folder '{folder_path}' not found")
|
@@ -879,7 +1052,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
879
1052
|
collection_data["item"].append(request_item)
|
880
1053
|
|
881
1054
|
# Update collection
|
882
|
-
response = self._make_request('PUT', f'/collections/{collection_id}',
|
1055
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
883
1056
|
json={"collection": collection_data})
|
884
1057
|
return json.dumps({"message": f"Request '{name}' created successfully"}, indent=2)
|
885
1058
|
except Exception as e:
|
@@ -887,92 +1060,283 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
887
1060
|
logger.error(f"Exception when creating request: {stacktrace}")
|
888
1061
|
raise ToolException(f"Unable to create request '{name}': {str(e)}")
|
889
1062
|
|
890
|
-
def
|
891
|
-
|
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."""
|
1063
|
+
def update_request_name(self, request_path: str, name: str, **kwargs) -> str:
|
1064
|
+
"""Update request name."""
|
895
1065
|
try:
|
896
1066
|
# Get current collection
|
897
1067
|
collection = self._make_request(
|
898
|
-
'GET', f'/collections/{collection_id}')
|
1068
|
+
'GET', f'/collections/{self.collection_id}')
|
899
1069
|
collection_data = collection["collection"]
|
900
1070
|
|
901
1071
|
# Find the request
|
902
|
-
request_item = self.
|
1072
|
+
request_item = self.analyzer.find_request_by_path(
|
903
1073
|
collection_data["item"], request_path)
|
904
1074
|
if not request_item:
|
905
1075
|
raise ToolException(f"Request '{request_path}' not found")
|
906
1076
|
|
907
|
-
# Update
|
908
|
-
|
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
|
1077
|
+
# Update name
|
1078
|
+
request_item["name"] = name
|
922
1079
|
|
923
|
-
# Update
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
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
|
-
})
|
1080
|
+
# Update collection
|
1081
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
1082
|
+
json={"collection": collection_data})
|
1083
|
+
return json.dumps({"message": f"Request '{request_path}' name updated successfully"}, indent=2)
|
1084
|
+
except Exception as e:
|
1085
|
+
stacktrace = format_exc()
|
1086
|
+
logger.error(f"Exception when updating request name: {stacktrace}")
|
1087
|
+
raise ToolException(
|
1088
|
+
f"Unable to update request '{request_path}' name: {str(e)}")
|
951
1089
|
|
952
|
-
|
1090
|
+
def update_request_method(self, request_path: str, method: str, **kwargs) -> str:
|
1091
|
+
"""Update request HTTP method."""
|
1092
|
+
try:
|
1093
|
+
# Get current collection
|
1094
|
+
collection = self._make_request(
|
1095
|
+
'GET', f'/collections/{self.collection_id}')
|
1096
|
+
collection_data = collection["collection"]
|
1097
|
+
|
1098
|
+
# Find the request
|
1099
|
+
request_item = self.analyzer.find_request_by_path(
|
1100
|
+
collection_data["item"], request_path)
|
1101
|
+
if not request_item:
|
1102
|
+
raise ToolException(f"Request '{request_path}' not found")
|
1103
|
+
|
1104
|
+
# Update method
|
1105
|
+
request_item["request"]["method"] = method.upper()
|
1106
|
+
|
1107
|
+
# Update collection
|
1108
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
1109
|
+
json={"collection": collection_data})
|
1110
|
+
return json.dumps({"message": f"Request '{request_path}' method updated successfully"}, indent=2)
|
1111
|
+
except Exception as e:
|
1112
|
+
stacktrace = format_exc()
|
1113
|
+
logger.error(f"Exception when updating request method: {stacktrace}")
|
1114
|
+
raise ToolException(
|
1115
|
+
f"Unable to update request '{request_path}' method: {str(e)}")
|
1116
|
+
|
1117
|
+
def update_request_url(self, request_path: str, url: str, **kwargs) -> str:
|
1118
|
+
"""Update request URL."""
|
1119
|
+
try:
|
1120
|
+
# Get current collection
|
1121
|
+
collection = self._make_request(
|
1122
|
+
'GET', f'/collections/{self.collection_id}')
|
1123
|
+
collection_data = collection["collection"]
|
1124
|
+
|
1125
|
+
# Find the request
|
1126
|
+
request_item = self.analyzer.find_request_by_path(
|
1127
|
+
collection_data["item"], request_path)
|
1128
|
+
if not request_item:
|
1129
|
+
raise ToolException(f"Request '{request_path}' not found")
|
1130
|
+
|
1131
|
+
# Update URL
|
1132
|
+
request_item["request"]["url"] = url
|
953
1133
|
|
954
1134
|
# Update collection
|
955
|
-
response = self._make_request('PUT', f'/collections/{collection_id}',
|
1135
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
956
1136
|
json={"collection": collection_data})
|
957
|
-
return json.dumps({"message": f"Request '{request_path}' updated successfully"}, indent=2)
|
1137
|
+
return json.dumps({"message": f"Request '{request_path}' URL updated successfully"}, indent=2)
|
958
1138
|
except Exception as e:
|
959
1139
|
stacktrace = format_exc()
|
960
|
-
logger.error(f"Exception when updating request: {stacktrace}")
|
1140
|
+
logger.error(f"Exception when updating request URL: {stacktrace}")
|
961
1141
|
raise ToolException(
|
962
|
-
f"Unable to update request '{request_path}': {str(e)}")
|
1142
|
+
f"Unable to update request '{request_path}' URL: {str(e)}")
|
1143
|
+
|
1144
|
+
def update_request_description(self, request_path: str, description: str, **kwargs) -> str:
|
1145
|
+
"""Update request description."""
|
1146
|
+
try:
|
1147
|
+
# Get current collection
|
1148
|
+
collection = self._make_request(
|
1149
|
+
'GET', f'/collections/{self.collection_id}')
|
1150
|
+
collection_data = collection["collection"]
|
1151
|
+
|
1152
|
+
# Find the request
|
1153
|
+
request_item = self.analyzer.find_request_by_path(
|
1154
|
+
collection_data["item"], request_path)
|
1155
|
+
if not request_item:
|
1156
|
+
raise ToolException(f"Request '{request_path}' not found")
|
1157
|
+
|
1158
|
+
# Update description
|
1159
|
+
request_item["request"]["description"] = description
|
963
1160
|
|
964
|
-
|
1161
|
+
# Update collection
|
1162
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
1163
|
+
json={"collection": collection_data})
|
1164
|
+
return json.dumps({"message": f"Request '{request_path}' description updated successfully"}, indent=2)
|
1165
|
+
except Exception as e:
|
1166
|
+
stacktrace = format_exc()
|
1167
|
+
logger.error(f"Exception when updating request description: {stacktrace}")
|
1168
|
+
raise ToolException(
|
1169
|
+
f"Unable to update request '{request_path}' description: {str(e)}")
|
1170
|
+
|
1171
|
+
def update_request_headers(self, request_path: str, headers: List[Dict], **kwargs) -> str:
|
1172
|
+
"""Update request headers."""
|
1173
|
+
try:
|
1174
|
+
# Get current collection
|
1175
|
+
collection = self._make_request(
|
1176
|
+
'GET', f'/collections/{self.collection_id}')
|
1177
|
+
collection_data = collection["collection"]
|
1178
|
+
|
1179
|
+
# Find the request
|
1180
|
+
request_item = self.analyzer.find_request_by_path(
|
1181
|
+
collection_data["item"], request_path)
|
1182
|
+
if not request_item:
|
1183
|
+
raise ToolException(f"Request '{request_path}' not found")
|
1184
|
+
|
1185
|
+
# Update headers
|
1186
|
+
request_item["request"]["header"] = headers
|
1187
|
+
|
1188
|
+
# Update collection
|
1189
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
1190
|
+
json={"collection": collection_data})
|
1191
|
+
return json.dumps({"message": f"Request '{request_path}' headers updated successfully"}, indent=2)
|
1192
|
+
except Exception as e:
|
1193
|
+
stacktrace = format_exc()
|
1194
|
+
logger.error(f"Exception when updating request headers: {stacktrace}")
|
1195
|
+
raise ToolException(
|
1196
|
+
f"Unable to update request '{request_path}' headers: {str(e)}")
|
1197
|
+
|
1198
|
+
def update_request_body(self, request_path: str, body: Dict, **kwargs) -> str:
|
1199
|
+
"""Update request body."""
|
1200
|
+
try:
|
1201
|
+
# Get current collection
|
1202
|
+
collection = self._make_request(
|
1203
|
+
'GET', f'/collections/{self.collection_id}')
|
1204
|
+
collection_data = collection["collection"]
|
1205
|
+
|
1206
|
+
# Find the request
|
1207
|
+
request_item = self.analyzer.find_request_by_path(
|
1208
|
+
collection_data["item"], request_path)
|
1209
|
+
if not request_item:
|
1210
|
+
raise ToolException(f"Request '{request_path}' not found")
|
1211
|
+
|
1212
|
+
# Update body
|
1213
|
+
request_item["request"]["body"] = body
|
1214
|
+
|
1215
|
+
# Update collection
|
1216
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
1217
|
+
json={"collection": collection_data})
|
1218
|
+
return json.dumps({"message": f"Request '{request_path}' body updated successfully"}, indent=2)
|
1219
|
+
except Exception as e:
|
1220
|
+
stacktrace = format_exc()
|
1221
|
+
logger.error(f"Exception when updating request body: {stacktrace}")
|
1222
|
+
raise ToolException(
|
1223
|
+
f"Unable to update request '{request_path}' body: {str(e)}")
|
1224
|
+
|
1225
|
+
def update_request_auth(self, request_path: str, auth: Dict, **kwargs) -> str:
|
1226
|
+
"""Update request authentication."""
|
1227
|
+
try:
|
1228
|
+
# Get current collection
|
1229
|
+
collection = self._make_request(
|
1230
|
+
'GET', f'/collections/{self.collection_id}')
|
1231
|
+
collection_data = collection["collection"]
|
1232
|
+
|
1233
|
+
# Find the request
|
1234
|
+
request_item = self.analyzer.find_request_by_path(
|
1235
|
+
collection_data["item"], request_path)
|
1236
|
+
if not request_item:
|
1237
|
+
raise ToolException(f"Request '{request_path}' not found")
|
1238
|
+
|
1239
|
+
# Update auth
|
1240
|
+
request_item["request"]["auth"] = auth
|
1241
|
+
|
1242
|
+
# Update collection
|
1243
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
1244
|
+
json={"collection": collection_data})
|
1245
|
+
return json.dumps({"message": f"Request '{request_path}' auth updated successfully"}, indent=2)
|
1246
|
+
except Exception as e:
|
1247
|
+
stacktrace = format_exc()
|
1248
|
+
logger.error(f"Exception when updating request auth: {stacktrace}")
|
1249
|
+
raise ToolException(
|
1250
|
+
f"Unable to update request '{request_path}' auth: {str(e)}")
|
1251
|
+
|
1252
|
+
def update_request_tests(self, request_path: str, tests: str, **kwargs) -> str:
|
1253
|
+
"""Update request test scripts."""
|
1254
|
+
try:
|
1255
|
+
# Get current collection
|
1256
|
+
collection = self._make_request(
|
1257
|
+
'GET', f'/collections/{self.collection_id}')
|
1258
|
+
collection_data = collection["collection"]
|
1259
|
+
|
1260
|
+
# Find the request
|
1261
|
+
request_item = self.analyzer.find_request_by_path(
|
1262
|
+
collection_data["item"], request_path)
|
1263
|
+
if not request_item:
|
1264
|
+
raise ToolException(f"Request '{request_path}' not found")
|
1265
|
+
|
1266
|
+
# Update test events
|
1267
|
+
events = request_item.get("event", [])
|
1268
|
+
# Remove existing test events
|
1269
|
+
events = [e for e in events if e.get("listen") != "test"]
|
1270
|
+
if tests:
|
1271
|
+
events.append({
|
1272
|
+
"listen": "test",
|
1273
|
+
"script": {
|
1274
|
+
"exec": tests.split('\n'),
|
1275
|
+
"type": "text/javascript"
|
1276
|
+
}
|
1277
|
+
})
|
1278
|
+
request_item["event"] = events
|
1279
|
+
|
1280
|
+
# Update collection
|
1281
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
1282
|
+
json={"collection": collection_data})
|
1283
|
+
return json.dumps({"message": f"Request '{request_path}' tests updated successfully"}, indent=2)
|
1284
|
+
except Exception as e:
|
1285
|
+
stacktrace = format_exc()
|
1286
|
+
logger.error(f"Exception when updating request tests: {stacktrace}")
|
1287
|
+
raise ToolException(
|
1288
|
+
f"Unable to update request '{request_path}' tests: {str(e)}")
|
1289
|
+
|
1290
|
+
def update_request_pre_script(self, request_path: str, pre_request_script: str, **kwargs) -> str:
|
1291
|
+
"""Update request pre-request scripts."""
|
1292
|
+
try:
|
1293
|
+
# Get current collection
|
1294
|
+
collection = self._make_request(
|
1295
|
+
'GET', f'/collections/{self.collection_id}')
|
1296
|
+
collection_data = collection["collection"]
|
1297
|
+
|
1298
|
+
# Find the request
|
1299
|
+
request_item = self.analyzer.find_request_by_path(
|
1300
|
+
collection_data["item"], request_path)
|
1301
|
+
if not request_item:
|
1302
|
+
raise ToolException(f"Request '{request_path}' not found")
|
1303
|
+
|
1304
|
+
# Update prerequest events
|
1305
|
+
events = request_item.get("event", [])
|
1306
|
+
# Remove existing prerequest events
|
1307
|
+
events = [e for e in events if e.get("listen") != "prerequest"]
|
1308
|
+
if pre_request_script:
|
1309
|
+
events.append({
|
1310
|
+
"listen": "prerequest",
|
1311
|
+
"script": {
|
1312
|
+
"exec": pre_request_script.split('\n'),
|
1313
|
+
"type": "text/javascript"
|
1314
|
+
}
|
1315
|
+
})
|
1316
|
+
request_item["event"] = events
|
1317
|
+
|
1318
|
+
# Update collection
|
1319
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
1320
|
+
json={"collection": collection_data})
|
1321
|
+
return json.dumps({"message": f"Request '{request_path}' pre-script updated successfully"}, indent=2)
|
1322
|
+
except Exception as e:
|
1323
|
+
stacktrace = format_exc()
|
1324
|
+
logger.error(f"Exception when updating request pre-script: {stacktrace}")
|
1325
|
+
raise ToolException(
|
1326
|
+
f"Unable to update request '{request_path}' pre-script: {str(e)}")
|
1327
|
+
|
1328
|
+
def delete_request(self, request_path: str, **kwargs) -> str:
|
965
1329
|
"""Delete an API request permanently."""
|
966
1330
|
try:
|
967
1331
|
# Get current collection
|
968
1332
|
collection = self._make_request(
|
969
|
-
'GET', f'/collections/{collection_id}')
|
1333
|
+
'GET', f'/collections/{self.collection_id}')
|
970
1334
|
collection_data = collection["collection"]
|
971
1335
|
|
972
1336
|
# Find and remove the request
|
973
|
-
if self.
|
1337
|
+
if self.analyzer.remove_request_by_path(collection_data["item"], request_path):
|
974
1338
|
# Update collection
|
975
|
-
response = self._make_request('PUT', f'/collections/{collection_id}',
|
1339
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
976
1340
|
json={"collection": collection_data})
|
977
1341
|
return json.dumps({"message": f"Request '{request_path}' deleted successfully"}, indent=2)
|
978
1342
|
else:
|
@@ -983,17 +1347,17 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
983
1347
|
raise ToolException(
|
984
1348
|
f"Unable to delete request '{request_path}': {str(e)}")
|
985
1349
|
|
986
|
-
def duplicate_request(self,
|
1350
|
+
def duplicate_request(self, source_path: str, new_name: str,
|
987
1351
|
target_path: str = None, **kwargs) -> str:
|
988
1352
|
"""Create a copy of an existing API request."""
|
989
1353
|
try:
|
990
1354
|
# Get current collection
|
991
1355
|
collection = self._make_request(
|
992
|
-
'GET', f'/collections/{collection_id}')
|
1356
|
+
'GET', f'/collections/{self.collection_id}')
|
993
1357
|
collection_data = collection["collection"]
|
994
1358
|
|
995
1359
|
# Find source request
|
996
|
-
source_request = self.
|
1360
|
+
source_request = self.analyzer.find_request_by_path(
|
997
1361
|
collection_data["item"], source_path)
|
998
1362
|
if not source_request:
|
999
1363
|
raise ToolException(
|
@@ -1009,7 +1373,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
1009
1373
|
|
1010
1374
|
# Add to target location
|
1011
1375
|
if target_path:
|
1012
|
-
folders = self.
|
1376
|
+
folders = self.analyzer.find_folders_by_path(
|
1013
1377
|
collection_data["item"], target_path)
|
1014
1378
|
if not folders:
|
1015
1379
|
raise ToolException(
|
@@ -1019,14 +1383,14 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
1019
1383
|
# Add to same location as source
|
1020
1384
|
source_folder_path = "/".join(source_path.split("/")[:-1])
|
1021
1385
|
if source_folder_path:
|
1022
|
-
folders = self.
|
1386
|
+
folders = self.analyzer.find_folders_by_path(
|
1023
1387
|
collection_data["item"], source_folder_path)
|
1024
1388
|
folders[0]["item"].append(request_copy)
|
1025
1389
|
else:
|
1026
1390
|
collection_data["item"].append(request_copy)
|
1027
1391
|
|
1028
1392
|
# Update collection
|
1029
|
-
response = self._make_request('PUT', f'/collections/{collection_id}',
|
1393
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
1030
1394
|
json={"collection": collection_data})
|
1031
1395
|
return json.dumps({"message": f"Request duplicated as '{new_name}'"}, indent=2)
|
1032
1396
|
except Exception as e:
|
@@ -1035,16 +1399,16 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
1035
1399
|
raise ToolException(
|
1036
1400
|
f"Unable to duplicate request '{source_path}': {str(e)}")
|
1037
1401
|
|
1038
|
-
def move_request(self,
|
1402
|
+
def move_request(self, source_path: str, target_path: str = None, **kwargs) -> str:
|
1039
1403
|
"""Move an API request to a different folder."""
|
1040
1404
|
try:
|
1041
1405
|
# Get current collection
|
1042
1406
|
collection = self._make_request(
|
1043
|
-
'GET', f'/collections/{collection_id}')
|
1407
|
+
'GET', f'/collections/{self.collection_id}')
|
1044
1408
|
collection_data = collection["collection"]
|
1045
1409
|
|
1046
1410
|
# Find source request
|
1047
|
-
source_request = self.
|
1411
|
+
source_request = self.analyzer.find_request_by_path(
|
1048
1412
|
collection_data["item"], source_path)
|
1049
1413
|
if not source_request:
|
1050
1414
|
raise ToolException(
|
@@ -1053,11 +1417,11 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
1053
1417
|
request_data = json.loads(json.dumps(source_request)) # Deep copy
|
1054
1418
|
|
1055
1419
|
# Remove from source location
|
1056
|
-
self.
|
1420
|
+
self.analyzer.remove_request_by_path(collection_data["item"], source_path)
|
1057
1421
|
|
1058
1422
|
# Add to target location
|
1059
1423
|
if target_path:
|
1060
|
-
folders = self.
|
1424
|
+
folders = self.analyzer.find_folders_by_path(
|
1061
1425
|
collection_data["item"], target_path)
|
1062
1426
|
if not folders:
|
1063
1427
|
raise ToolException(
|
@@ -1067,7 +1431,7 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
1067
1431
|
collection_data["item"].append(request_data)
|
1068
1432
|
|
1069
1433
|
# Update collection
|
1070
|
-
response = self._make_request('PUT', f'/collections/{collection_id}',
|
1434
|
+
response = self._make_request('PUT', f'/collections/{self.collection_id}',
|
1071
1435
|
json={"collection": collection_data})
|
1072
1436
|
return json.dumps({"message": f"Request moved from '{source_path}' to '{target_path or 'root'}'"}, indent=2)
|
1073
1437
|
except Exception as e:
|
@@ -1075,958 +1439,46 @@ class PostmanApiWrapper(BaseToolApiWrapper):
|
|
1075
1439
|
logger.error(f"Exception when moving request: {stacktrace}")
|
1076
1440
|
raise ToolException(
|
1077
1441
|
f"Unable to move request '{source_path}': {str(e)}")
|
1078
|
-
|
1442
|
+
|
1079
1443
|
# =================================================================
|
1080
1444
|
# HELPER METHODS
|
1081
1445
|
# =================================================================
|
1082
1446
|
|
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
1447
|
|
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
1448
|
|
1123
|
-
|
1124
|
-
|
1125
|
-
|
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))
|
1449
|
+
def _make_request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
|
1450
|
+
"""Make HTTP request to Postman API."""
|
1451
|
+
url = f"{self.base_url.rstrip('/')}{endpoint}"
|
1138
1452
|
|
1139
|
-
|
1453
|
+
try:
|
1454
|
+
logger.info(f"Making {method.upper()} request to {url}")
|
1455
|
+
response = self.session.request(method, url, timeout=self.timeout, **kwargs)
|
1456
|
+
response.raise_for_status()
|
1140
1457
|
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
1458
|
+
if response.content:
|
1459
|
+
return response.json()
|
1460
|
+
return {}
|
1461
|
+
|
1462
|
+
except requests.exceptions.RequestException as e:
|
1463
|
+
logger.error(f"Request failed: {e}")
|
1464
|
+
raise ToolException(f"Postman API request failed: {str(e)}")
|
1465
|
+
except json.JSONDecodeError as e:
|
1466
|
+
logger.error(f"Failed to decode JSON response: {e}")
|
1467
|
+
raise ToolException(
|
1468
|
+
f"Invalid JSON response from Postman API: {str(e)}")
|
1145
1469
|
|
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
|
1226
|
-
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
1470
|
|
1292
|
-
# =================================================================
|
1293
|
-
# ANALYSIS HELPER METHODS
|
1294
|
-
# =================================================================
|
1295
1471
|
|
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
1472
|
|
1322
|
-
|
1323
|
-
|
1324
|
-
|
1325
|
-
|
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 = []
|
1473
|
+
|
1474
|
+
# =================================================================
|
1475
|
+
# HELPER METHODS
|
1476
|
+
# =================================================================
|
1357
1477
|
|
1358
1478
|
for item in items:
|
1359
1479
|
if item.get('request'): # This is a request
|
1360
|
-
analysis = self.
|
1480
|
+
analysis = self.analyzer.perform_request_analysis(item)
|
1361
1481
|
requests.append(analysis)
|
1362
1482
|
|
1363
1483
|
return requests
|
1364
1484
|
|
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
|