alita-sdk 0.3.155__py3-none-any.whl → 0.3.157__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alita_sdk/tools/postman/__init__.py +1 -1
- alita_sdk/tools/postman/api_wrapper.py +762 -1243
- alita_sdk/tools/postman/postman_analysis.py +1133 -0
- {alita_sdk-0.3.155.dist-info → alita_sdk-0.3.157.dist-info}/METADATA +1 -1
- {alita_sdk-0.3.155.dist-info → alita_sdk-0.3.157.dist-info}/RECORD +8 -7
- {alita_sdk-0.3.155.dist-info → alita_sdk-0.3.157.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.155.dist-info → alita_sdk-0.3.157.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.155.dist-info → alita_sdk-0.3.157.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1133 @@
|
|
1
|
+
"""
|
2
|
+
Postman collection analysis utilities.
|
3
|
+
|
4
|
+
This module contains all the analysis logic for Postman collections, folders, and requests
|
5
|
+
that is separate from the API interaction logic.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
import re
|
10
|
+
from typing import Any, Dict, List, Optional
|
11
|
+
|
12
|
+
|
13
|
+
class PostmanAnalyzer:
|
14
|
+
"""Analyzer for Postman collections, folders, and requests."""
|
15
|
+
|
16
|
+
def perform_collection_analysis(self, collection: Dict) -> Dict:
|
17
|
+
"""Perform comprehensive analysis of a collection."""
|
18
|
+
collection_data = collection['collection']
|
19
|
+
folders = self.analyze_folders(collection_data.get('item', []))
|
20
|
+
total_requests = self.count_requests(collection_data.get('item', []))
|
21
|
+
issues = self.identify_collection_issues(collection_data)
|
22
|
+
score = self.calculate_quality_score(collection_data, folders, issues)
|
23
|
+
recommendations = self.generate_recommendations(issues)
|
24
|
+
|
25
|
+
return {
|
26
|
+
"collection_id": collection_data['info'].get('_postman_id', ''),
|
27
|
+
"collection_name": collection_data['info'].get('name', ''),
|
28
|
+
"total_requests": total_requests,
|
29
|
+
"folders": folders,
|
30
|
+
"issues": issues,
|
31
|
+
"recommendations": recommendations,
|
32
|
+
"score": score,
|
33
|
+
"overall_security_score": self.calculate_overall_security_score(folders),
|
34
|
+
"overall_performance_score": self.calculate_overall_performance_score(folders),
|
35
|
+
"overall_documentation_score": self.calculate_overall_documentation_score(folders)
|
36
|
+
}
|
37
|
+
|
38
|
+
def analyze_folders(self, items: List[Dict], base_path: str = "") -> List[Dict]:
|
39
|
+
"""Analyze all folders in a collection."""
|
40
|
+
folders = []
|
41
|
+
|
42
|
+
for item in items:
|
43
|
+
if item.get('item') is not None: # This is a folder
|
44
|
+
folder_path = f"{base_path}/{item['name']}" if base_path else item['name']
|
45
|
+
analysis = self.perform_folder_analysis(item, folder_path)
|
46
|
+
folders.append(analysis)
|
47
|
+
|
48
|
+
# Recursively analyze subfolders
|
49
|
+
subfolders = self.analyze_folders(item['item'], folder_path)
|
50
|
+
folders.extend(subfolders)
|
51
|
+
|
52
|
+
return folders
|
53
|
+
|
54
|
+
def perform_folder_analysis(self, folder: Dict, path: str) -> Dict:
|
55
|
+
"""Perform analysis of a specific folder."""
|
56
|
+
requests = self.analyze_requests(folder.get('item', []))
|
57
|
+
request_count = self.count_requests(folder.get('item', []))
|
58
|
+
issues = self.identify_folder_issues(folder, requests)
|
59
|
+
|
60
|
+
return {
|
61
|
+
"name": folder['name'],
|
62
|
+
"path": path,
|
63
|
+
"request_count": request_count,
|
64
|
+
"requests": requests,
|
65
|
+
"issues": issues,
|
66
|
+
"has_consistent_naming": self.check_consistent_naming(folder.get('item', [])),
|
67
|
+
"has_proper_structure": bool(folder.get('description') and folder.get('item')),
|
68
|
+
"auth_consistency": self.check_auth_consistency(requests),
|
69
|
+
"avg_documentation_quality": self.calculate_avg_documentation_quality(requests),
|
70
|
+
"avg_security_score": self.calculate_avg_security_score(requests),
|
71
|
+
"avg_performance_score": self.calculate_avg_performance_score(requests)
|
72
|
+
}
|
73
|
+
|
74
|
+
def analyze_requests(self, items: List[Dict]) -> List[Dict]:
|
75
|
+
"""Analyze requests within a folder."""
|
76
|
+
requests = []
|
77
|
+
|
78
|
+
for item in items:
|
79
|
+
if item.get('request'): # This is a request
|
80
|
+
analysis = self.perform_request_analysis(item)
|
81
|
+
requests.append(analysis)
|
82
|
+
|
83
|
+
return requests
|
84
|
+
|
85
|
+
def perform_request_analysis(self, item: Dict) -> Dict:
|
86
|
+
"""Perform comprehensive analysis of a specific request."""
|
87
|
+
request = item['request']
|
88
|
+
issues = []
|
89
|
+
|
90
|
+
# Basic checks
|
91
|
+
has_auth = bool(request.get('auth') or self.has_auth_in_headers(request))
|
92
|
+
has_description = bool(item.get('description') or request.get('description'))
|
93
|
+
has_tests = bool([e for e in item.get('event', []) if e.get('listen') == 'test'])
|
94
|
+
has_examples = bool(item.get('response', []))
|
95
|
+
|
96
|
+
# Enhanced analysis
|
97
|
+
url = request.get('url', '')
|
98
|
+
if isinstance(url, dict):
|
99
|
+
url = url.get('raw', '')
|
100
|
+
|
101
|
+
has_hardcoded_url = self.detect_hardcoded_url(url)
|
102
|
+
has_hardcoded_data = self.detect_hardcoded_data(request)
|
103
|
+
has_proper_headers = self.validate_headers(request)
|
104
|
+
has_variables = self.detect_variable_usage(request)
|
105
|
+
has_error_handling = self.detect_error_handling(item)
|
106
|
+
follows_naming_convention = self.validate_naming_convention(item['name'])
|
107
|
+
has_security_issues = self.detect_security_issues(request)
|
108
|
+
has_performance_issues = self.detect_performance_issues(request)
|
109
|
+
|
110
|
+
# Calculate scores
|
111
|
+
security_score = self.calculate_security_score(request, has_auth, has_security_issues)
|
112
|
+
performance_score = self.calculate_performance_score(request, has_performance_issues)
|
113
|
+
|
114
|
+
# Generate issues
|
115
|
+
self.generate_request_issues(issues, item, {
|
116
|
+
'has_description': has_description,
|
117
|
+
'has_auth': has_auth,
|
118
|
+
'has_tests': has_tests,
|
119
|
+
'has_hardcoded_url': has_hardcoded_url,
|
120
|
+
'has_hardcoded_data': has_hardcoded_data,
|
121
|
+
'has_proper_headers': has_proper_headers,
|
122
|
+
'has_security_issues': has_security_issues,
|
123
|
+
'follows_naming_convention': follows_naming_convention
|
124
|
+
})
|
125
|
+
|
126
|
+
return {
|
127
|
+
"name": item['name'],
|
128
|
+
"method": request.get('method'),
|
129
|
+
"url": url,
|
130
|
+
"has_auth": has_auth,
|
131
|
+
"has_description": has_description,
|
132
|
+
"has_tests": has_tests,
|
133
|
+
"has_examples": has_examples,
|
134
|
+
"issues": issues,
|
135
|
+
"has_hardcoded_url": has_hardcoded_url,
|
136
|
+
"has_hardcoded_data": has_hardcoded_data,
|
137
|
+
"has_proper_headers": has_proper_headers,
|
138
|
+
"has_variables": has_variables,
|
139
|
+
"has_error_handling": has_error_handling,
|
140
|
+
"follows_naming_convention": follows_naming_convention,
|
141
|
+
"has_security_issues": has_security_issues,
|
142
|
+
"has_performance_issues": has_performance_issues,
|
143
|
+
"auth_type": request.get('auth', {}).get('type'),
|
144
|
+
"response_examples": len(item.get('response', [])),
|
145
|
+
"test_coverage": self.assess_test_coverage(item),
|
146
|
+
"documentation_quality": self.assess_documentation_quality(item),
|
147
|
+
"security_score": security_score,
|
148
|
+
"performance_score": performance_score
|
149
|
+
}
|
150
|
+
|
151
|
+
def extract_requests_from_items(self, items: List[Dict], include_details: bool = False) -> List[Dict]:
|
152
|
+
"""Extract requests from items recursively."""
|
153
|
+
requests = []
|
154
|
+
|
155
|
+
for item in items:
|
156
|
+
if item.get('request'):
|
157
|
+
# This is a request
|
158
|
+
request_data = {
|
159
|
+
"name": item.get('name'),
|
160
|
+
"method": item['request'].get('method'),
|
161
|
+
"url": item['request'].get('url')
|
162
|
+
}
|
163
|
+
|
164
|
+
if include_details:
|
165
|
+
request_data.update({
|
166
|
+
"description": item.get('description'),
|
167
|
+
"headers": item['request'].get('header', []),
|
168
|
+
"body": item['request'].get('body'),
|
169
|
+
"auth": item['request'].get('auth'),
|
170
|
+
"tests": [e for e in item.get('event', []) if e.get('listen') == 'test'],
|
171
|
+
"pre_request_scripts": [e for e in item.get('event', []) if e.get('listen') == 'prerequest']
|
172
|
+
})
|
173
|
+
|
174
|
+
requests.append(request_data)
|
175
|
+
elif item.get('item'):
|
176
|
+
# This is a folder, recurse
|
177
|
+
requests.extend(self.extract_requests_from_items(
|
178
|
+
item['item'], include_details))
|
179
|
+
|
180
|
+
return requests
|
181
|
+
|
182
|
+
def search_requests_in_items(self, items: List[Dict], query: str, search_in: str, method: str = None) -> List[Dict]:
|
183
|
+
"""Search for requests in items recursively."""
|
184
|
+
results = []
|
185
|
+
query_lower = query.lower()
|
186
|
+
|
187
|
+
for item in items:
|
188
|
+
if item.get('request'):
|
189
|
+
# This is a request
|
190
|
+
request = item['request']
|
191
|
+
matches = False
|
192
|
+
|
193
|
+
# Check method filter first
|
194
|
+
if method and request.get('method', '').upper() != method.upper():
|
195
|
+
continue
|
196
|
+
|
197
|
+
# Check search criteria
|
198
|
+
if search_in == 'all' or search_in == 'name':
|
199
|
+
if query_lower in item.get('name', '').lower():
|
200
|
+
matches = True
|
201
|
+
|
202
|
+
if search_in == 'all' or search_in == 'url':
|
203
|
+
url = request.get('url', '')
|
204
|
+
if isinstance(url, dict):
|
205
|
+
url = url.get('raw', '')
|
206
|
+
if query_lower in url.lower():
|
207
|
+
matches = True
|
208
|
+
|
209
|
+
if search_in == 'all' or search_in == 'description':
|
210
|
+
description = item.get('description', '') or request.get('description', '')
|
211
|
+
if query_lower in description.lower():
|
212
|
+
matches = True
|
213
|
+
|
214
|
+
if matches:
|
215
|
+
results.append({
|
216
|
+
"name": item.get('name'),
|
217
|
+
"method": request.get('method'),
|
218
|
+
"url": request.get('url'),
|
219
|
+
"description": item.get('description') or request.get('description'),
|
220
|
+
"path": self.get_item_path(items, item)
|
221
|
+
})
|
222
|
+
|
223
|
+
elif item.get('item'):
|
224
|
+
# This is a folder, recurse
|
225
|
+
results.extend(self.search_requests_in_items(
|
226
|
+
item['item'], query, search_in, method))
|
227
|
+
|
228
|
+
return results
|
229
|
+
|
230
|
+
def get_item_path(self, root_items: List[Dict], target_item: Dict, current_path: str = "") -> str:
|
231
|
+
"""Get the path of an item within the collection structure."""
|
232
|
+
for item in root_items:
|
233
|
+
item_path = f"{current_path}/{item['name']}" if current_path else item['name']
|
234
|
+
|
235
|
+
if item == target_item:
|
236
|
+
return item_path
|
237
|
+
|
238
|
+
if item.get('item'):
|
239
|
+
result = self.get_item_path(
|
240
|
+
item['item'], target_item, item_path)
|
241
|
+
if result:
|
242
|
+
return result
|
243
|
+
|
244
|
+
return ""
|
245
|
+
|
246
|
+
def get_script_content(self, events: List[Dict], script_type: str) -> str:
|
247
|
+
"""Get script content from event list."""
|
248
|
+
for event in events:
|
249
|
+
if event.get("listen") == script_type and event.get("script"):
|
250
|
+
script_exec = event["script"].get("exec", [])
|
251
|
+
if isinstance(script_exec, list):
|
252
|
+
return "\n".join(script_exec)
|
253
|
+
return str(script_exec)
|
254
|
+
return ""
|
255
|
+
|
256
|
+
# =================================================================
|
257
|
+
# UTILITY METHODS
|
258
|
+
# =================================================================
|
259
|
+
|
260
|
+
def count_requests(self, items: List[Dict]) -> int:
|
261
|
+
"""Count total requests in items."""
|
262
|
+
count = 0
|
263
|
+
for item in items:
|
264
|
+
if item.get('request'):
|
265
|
+
count += 1
|
266
|
+
elif item.get('item'):
|
267
|
+
count += self.count_requests(item['item'])
|
268
|
+
return count
|
269
|
+
|
270
|
+
def has_auth_in_headers(self, request: Dict) -> bool:
|
271
|
+
"""Check if request has authentication in headers."""
|
272
|
+
headers = request.get('header', [])
|
273
|
+
auth_headers = ['authorization', 'x-api-key', 'x-auth-token']
|
274
|
+
return any(h.get('key', '').lower() in auth_headers for h in headers)
|
275
|
+
|
276
|
+
def detect_hardcoded_url(self, url: str) -> bool:
|
277
|
+
"""Detect hardcoded URLs that should use variables."""
|
278
|
+
hardcoded_patterns = [
|
279
|
+
r'^https?://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', # IP addresses
|
280
|
+
r'^https?://localhost', # localhost
|
281
|
+
r'^https?://[a-zA-Z0-9.-]+\.(com|org|net|io|dev)', # Direct domains
|
282
|
+
r'api\.example\.com', # Example domains
|
283
|
+
r'staging\.|dev\.|test\.' # Environment-specific
|
284
|
+
]
|
285
|
+
return any(re.search(pattern, url) for pattern in hardcoded_patterns) and '{{' not in url
|
286
|
+
|
287
|
+
def detect_hardcoded_data(self, request: Dict) -> bool:
|
288
|
+
"""Detect hardcoded data in request body and headers."""
|
289
|
+
# Check headers
|
290
|
+
headers = request.get('header', [])
|
291
|
+
has_hardcoded_headers = any(
|
292
|
+
('token' in h.get('key', '').lower() or
|
293
|
+
'key' in h.get('key', '').lower() or
|
294
|
+
'secret' in h.get('key', '').lower()) and
|
295
|
+
'{{' not in h.get('value', '')
|
296
|
+
for h in headers
|
297
|
+
)
|
298
|
+
|
299
|
+
# Check body
|
300
|
+
has_hardcoded_body = False
|
301
|
+
body = request.get('body', {})
|
302
|
+
if body.get('raw'):
|
303
|
+
try:
|
304
|
+
body_data = json.loads(body['raw'])
|
305
|
+
has_hardcoded_body = self.contains_hardcoded_values(body_data)
|
306
|
+
except json.JSONDecodeError:
|
307
|
+
# If not JSON, check for common patterns
|
308
|
+
has_hardcoded_body = re.search(
|
309
|
+
r'("api_key"|"token"|"password"):\s*"[^{]', body['raw']) is not None
|
310
|
+
|
311
|
+
return has_hardcoded_headers or has_hardcoded_body
|
312
|
+
|
313
|
+
def contains_hardcoded_values(self, obj: Any) -> bool:
|
314
|
+
"""Check if object contains hardcoded values that should be variables."""
|
315
|
+
if not isinstance(obj, dict):
|
316
|
+
return False
|
317
|
+
|
318
|
+
for key, value in obj.items():
|
319
|
+
if isinstance(value, str):
|
320
|
+
# Check for sensitive keys
|
321
|
+
if key.lower() in ['token', 'key', 'secret', 'password', 'api_key', 'client_id', 'client_secret']:
|
322
|
+
if '{{' not in value:
|
323
|
+
return True
|
324
|
+
# Check for email patterns, URLs
|
325
|
+
if re.search(r'@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', value) or value.startswith('http'):
|
326
|
+
if '{{' not in value:
|
327
|
+
return True
|
328
|
+
elif isinstance(value, dict):
|
329
|
+
if self.contains_hardcoded_values(value):
|
330
|
+
return True
|
331
|
+
|
332
|
+
return False
|
333
|
+
|
334
|
+
def validate_headers(self, request: Dict) -> bool:
|
335
|
+
"""Validate request headers."""
|
336
|
+
headers = request.get('header', [])
|
337
|
+
header_names = [h.get('key', '').lower() for h in headers]
|
338
|
+
method = request.get('method', '').upper()
|
339
|
+
|
340
|
+
# Check for essential headers
|
341
|
+
if method in ['POST', 'PUT', 'PATCH'] and request.get('body'):
|
342
|
+
if 'content-type' not in header_names:
|
343
|
+
return False
|
344
|
+
|
345
|
+
if method in ['GET', 'POST', 'PUT', 'PATCH']:
|
346
|
+
if 'accept' not in header_names:
|
347
|
+
return False
|
348
|
+
|
349
|
+
return True
|
350
|
+
|
351
|
+
def detect_variable_usage(self, request: Dict) -> bool:
|
352
|
+
"""Detect variable usage in request."""
|
353
|
+
url = request.get('url', '')
|
354
|
+
if isinstance(url, dict):
|
355
|
+
url = url.get('raw', '')
|
356
|
+
|
357
|
+
has_url_variables = '{{' in url
|
358
|
+
has_header_variables = any('{{' in h.get('value', '') for h in request.get('header', []))
|
359
|
+
|
360
|
+
has_body_variables = False
|
361
|
+
body = request.get('body', {})
|
362
|
+
if body.get('raw'):
|
363
|
+
has_body_variables = '{{' in body['raw']
|
364
|
+
|
365
|
+
return has_url_variables or has_header_variables or has_body_variables
|
366
|
+
|
367
|
+
def detect_error_handling(self, item: Dict) -> bool:
|
368
|
+
"""Detect error handling in tests."""
|
369
|
+
test_scripts = [e for e in item.get('event', []) if e.get('listen') == 'test']
|
370
|
+
|
371
|
+
for script in test_scripts:
|
372
|
+
script_code = '\n'.join(script.get('script', {}).get('exec', []))
|
373
|
+
if ('4' in script_code or '5' in script_code or
|
374
|
+
'error' in script_code.lower() or 'fail' in script_code.lower()):
|
375
|
+
return True
|
376
|
+
|
377
|
+
return False
|
378
|
+
|
379
|
+
def validate_naming_convention(self, name: str) -> bool:
|
380
|
+
"""Validate naming convention."""
|
381
|
+
has_consistent_case = re.match(r'^[a-zA-Z][a-zA-Z0-9\s\-_]*$', name) is not None
|
382
|
+
has_descriptive_name = len(name) > 3 and 'test' not in name.lower() and 'temp' not in name.lower()
|
383
|
+
return has_consistent_case and has_descriptive_name
|
384
|
+
|
385
|
+
def detect_security_issues(self, request: Dict) -> bool:
|
386
|
+
"""Detect security issues."""
|
387
|
+
url = request.get('url', '')
|
388
|
+
if isinstance(url, dict):
|
389
|
+
url = url.get('raw', '')
|
390
|
+
|
391
|
+
# Check for exposed credentials in URL
|
392
|
+
if re.search(r'[?&](token|key|password|secret)=([^&\s]+)', url):
|
393
|
+
return True
|
394
|
+
|
395
|
+
# Check for weak authentication
|
396
|
+
auth = request.get('auth', {})
|
397
|
+
if auth.get('type') == 'basic' and not url.startswith('https'):
|
398
|
+
return True
|
399
|
+
|
400
|
+
# Check headers for exposed credentials
|
401
|
+
headers = request.get('header', [])
|
402
|
+
return any('secret' in h.get('key', '').lower() or 'password' in h.get('key', '').lower()
|
403
|
+
for h in headers)
|
404
|
+
|
405
|
+
def detect_performance_issues(self, request: Dict) -> bool:
|
406
|
+
"""Detect performance issues."""
|
407
|
+
# Large request body
|
408
|
+
body = request.get('body', {})
|
409
|
+
if body.get('raw') and len(body['raw']) > 10000:
|
410
|
+
return True
|
411
|
+
|
412
|
+
# Too many headers
|
413
|
+
if len(request.get('header', [])) > 20:
|
414
|
+
return True
|
415
|
+
|
416
|
+
# Too many query parameters
|
417
|
+
url = request.get('url', '')
|
418
|
+
if isinstance(url, dict):
|
419
|
+
url = url.get('raw', '')
|
420
|
+
|
421
|
+
query_params = url.split('?')[1] if '?' in url else ''
|
422
|
+
if query_params and len(query_params.split('&')) > 15:
|
423
|
+
return True
|
424
|
+
|
425
|
+
return False
|
426
|
+
|
427
|
+
# =================================================================
|
428
|
+
# SCORING METHODS
|
429
|
+
# =================================================================
|
430
|
+
|
431
|
+
def calculate_security_score(self, request: Dict, has_auth: bool, has_security_issues: bool) -> int:
|
432
|
+
"""Calculate security score."""
|
433
|
+
score = 100
|
434
|
+
method = request.get('method', '').upper()
|
435
|
+
|
436
|
+
if not has_auth and method in ['POST', 'PUT', 'PATCH', 'DELETE']:
|
437
|
+
score -= 40
|
438
|
+
|
439
|
+
if has_security_issues:
|
440
|
+
score -= 30
|
441
|
+
|
442
|
+
url = request.get('url', '')
|
443
|
+
if isinstance(url, dict):
|
444
|
+
url = url.get('raw', '')
|
445
|
+
|
446
|
+
if url.startswith('http://'):
|
447
|
+
score -= 20
|
448
|
+
|
449
|
+
auth = request.get('auth', {})
|
450
|
+
if auth.get('type') == 'basic':
|
451
|
+
score -= 10
|
452
|
+
|
453
|
+
return max(0, score)
|
454
|
+
|
455
|
+
def calculate_performance_score(self, request: Dict, has_performance_issues: bool) -> int:
|
456
|
+
"""Calculate performance score."""
|
457
|
+
score = 100
|
458
|
+
|
459
|
+
if has_performance_issues:
|
460
|
+
score -= 50
|
461
|
+
|
462
|
+
headers = request.get('header', [])
|
463
|
+
header_names = [h.get('key', '').lower() for h in headers]
|
464
|
+
|
465
|
+
if 'cache-control' not in header_names:
|
466
|
+
score -= 10
|
467
|
+
|
468
|
+
if 'accept-encoding' not in header_names:
|
469
|
+
score -= 10
|
470
|
+
|
471
|
+
return max(0, score)
|
472
|
+
|
473
|
+
def assess_test_coverage(self, item: Dict) -> str:
|
474
|
+
"""Assess test coverage."""
|
475
|
+
test_scripts = [e for e in item.get('event', []) if e.get('listen') == 'test']
|
476
|
+
|
477
|
+
if not test_scripts:
|
478
|
+
return 'none'
|
479
|
+
|
480
|
+
all_test_code = '\n'.join([
|
481
|
+
'\n'.join(script.get('script', {}).get('exec', []))
|
482
|
+
for script in test_scripts
|
483
|
+
])
|
484
|
+
|
485
|
+
checks = [
|
486
|
+
'pm.response.code' in all_test_code or 'status' in all_test_code,
|
487
|
+
'responseTime' in all_test_code,
|
488
|
+
'pm.response.json' in all_test_code or 'body' in all_test_code,
|
489
|
+
'4' in all_test_code or '5' in all_test_code
|
490
|
+
]
|
491
|
+
|
492
|
+
check_count = sum(checks)
|
493
|
+
|
494
|
+
if check_count >= 3:
|
495
|
+
return 'comprehensive'
|
496
|
+
elif check_count >= 1:
|
497
|
+
return 'basic'
|
498
|
+
|
499
|
+
return 'none'
|
500
|
+
|
501
|
+
def assess_documentation_quality(self, item: Dict) -> str:
|
502
|
+
"""Assess documentation quality."""
|
503
|
+
description = item.get('description', '') or item.get('request', {}).get('description', '')
|
504
|
+
|
505
|
+
if not description:
|
506
|
+
return 'none'
|
507
|
+
|
508
|
+
description_lower = description.lower()
|
509
|
+
quality_factors = [
|
510
|
+
'parameter' in description_lower,
|
511
|
+
'response' in description_lower,
|
512
|
+
'example' in description_lower,
|
513
|
+
'auth' in description_lower,
|
514
|
+
'error' in description_lower
|
515
|
+
]
|
516
|
+
|
517
|
+
factor_count = sum(quality_factors)
|
518
|
+
|
519
|
+
if factor_count >= 4:
|
520
|
+
return 'excellent'
|
521
|
+
elif factor_count >= 2:
|
522
|
+
return 'good'
|
523
|
+
elif factor_count >= 1 or len(description) > 50:
|
524
|
+
return 'minimal'
|
525
|
+
|
526
|
+
return 'none'
|
527
|
+
|
528
|
+
def check_consistent_naming(self, items: List[Dict]) -> bool:
|
529
|
+
"""Check if items have consistent naming."""
|
530
|
+
if len(items) <= 1:
|
531
|
+
return True
|
532
|
+
|
533
|
+
naming_patterns = []
|
534
|
+
for item in items:
|
535
|
+
name = item.get('name', '').lower()
|
536
|
+
if re.match(r'^[a-z][a-z0-9]*(_[a-z0-9]+)*$', name):
|
537
|
+
naming_patterns.append('snake_case')
|
538
|
+
elif re.match(r'^[a-z][a-zA-Z0-9]*$', name):
|
539
|
+
naming_patterns.append('camelCase')
|
540
|
+
elif re.match(r'^[a-z][a-z0-9]*(-[a-z0-9]+)*$', name):
|
541
|
+
naming_patterns.append('kebab-case')
|
542
|
+
else:
|
543
|
+
naming_patterns.append('mixed')
|
544
|
+
|
545
|
+
unique_patterns = set(naming_patterns)
|
546
|
+
return len(unique_patterns) == 1 and 'mixed' not in unique_patterns
|
547
|
+
|
548
|
+
def check_auth_consistency(self, requests: List[Dict]) -> str:
|
549
|
+
"""Check authentication consistency across requests."""
|
550
|
+
if not requests:
|
551
|
+
return 'none'
|
552
|
+
|
553
|
+
auth_types = set(req.get('auth_type') or 'none' for req in requests)
|
554
|
+
|
555
|
+
if len(auth_types) == 1:
|
556
|
+
return 'none' if 'none' in auth_types else 'consistent'
|
557
|
+
|
558
|
+
return 'mixed'
|
559
|
+
|
560
|
+
def calculate_avg_documentation_quality(self, requests: List[Dict]) -> int:
|
561
|
+
"""Calculate average documentation quality score."""
|
562
|
+
if not requests:
|
563
|
+
return 0
|
564
|
+
|
565
|
+
quality_scores = {
|
566
|
+
'excellent': 100,
|
567
|
+
'good': 75,
|
568
|
+
'minimal': 50,
|
569
|
+
'none': 0
|
570
|
+
}
|
571
|
+
|
572
|
+
scores = [quality_scores.get(req.get('documentation_quality', 'none'), 0) for req in requests]
|
573
|
+
return round(sum(scores) / len(scores))
|
574
|
+
|
575
|
+
def calculate_avg_security_score(self, requests: List[Dict]) -> int:
|
576
|
+
"""Calculate average security score."""
|
577
|
+
if not requests:
|
578
|
+
return 0
|
579
|
+
|
580
|
+
scores = [req.get('security_score', 0) for req in requests]
|
581
|
+
return round(sum(scores) / len(scores))
|
582
|
+
|
583
|
+
def calculate_avg_performance_score(self, requests: List[Dict]) -> int:
|
584
|
+
"""Calculate average performance score."""
|
585
|
+
if not requests:
|
586
|
+
return 0
|
587
|
+
|
588
|
+
scores = [req.get('performance_score', 0) for req in requests]
|
589
|
+
return round(sum(scores) / len(scores))
|
590
|
+
|
591
|
+
def calculate_quality_score(self, collection_data: Dict, folders: List[Dict], issues: List[Dict]) -> int:
|
592
|
+
"""Calculate quality score (0-100)."""
|
593
|
+
score = 100
|
594
|
+
|
595
|
+
# Deduct points for issues
|
596
|
+
for issue in issues:
|
597
|
+
severity = issue.get('severity', 'low')
|
598
|
+
if severity == 'high':
|
599
|
+
score -= 10
|
600
|
+
elif severity == 'medium':
|
601
|
+
score -= 5
|
602
|
+
elif severity == 'low':
|
603
|
+
score -= 2
|
604
|
+
|
605
|
+
# Deduct points for folder and request issues
|
606
|
+
for folder in folders:
|
607
|
+
for issue in folder.get('issues', []):
|
608
|
+
severity = issue.get('severity', 'low')
|
609
|
+
if severity == 'high':
|
610
|
+
score -= 5
|
611
|
+
elif severity == 'medium':
|
612
|
+
score -= 3
|
613
|
+
elif severity == 'low':
|
614
|
+
score -= 1
|
615
|
+
|
616
|
+
for request in folder.get('requests', []):
|
617
|
+
for issue in request.get('issues', []):
|
618
|
+
severity = issue.get('severity', 'low')
|
619
|
+
if severity == 'high':
|
620
|
+
score -= 3
|
621
|
+
elif severity == 'medium':
|
622
|
+
score -= 2
|
623
|
+
elif severity == 'low':
|
624
|
+
score -= 1
|
625
|
+
|
626
|
+
return max(0, min(100, score))
|
627
|
+
|
628
|
+
def calculate_overall_security_score(self, folders: List[Dict]) -> int:
|
629
|
+
"""Calculate overall security score."""
|
630
|
+
if not folders:
|
631
|
+
return 0
|
632
|
+
|
633
|
+
scores = []
|
634
|
+
for folder in folders:
|
635
|
+
avg_score = folder.get('avg_security_score', 0)
|
636
|
+
if avg_score > 0:
|
637
|
+
scores.append(avg_score)
|
638
|
+
|
639
|
+
return round(sum(scores) / len(scores)) if scores else 0
|
640
|
+
|
641
|
+
def calculate_overall_performance_score(self, folders: List[Dict]) -> int:
|
642
|
+
"""Calculate overall performance score."""
|
643
|
+
if not folders:
|
644
|
+
return 0
|
645
|
+
|
646
|
+
scores = []
|
647
|
+
for folder in folders:
|
648
|
+
avg_score = folder.get('avg_performance_score', 0)
|
649
|
+
if avg_score > 0:
|
650
|
+
scores.append(avg_score)
|
651
|
+
|
652
|
+
return round(sum(scores) / len(scores)) if scores else 0
|
653
|
+
|
654
|
+
def calculate_overall_documentation_score(self, folders: List[Dict]) -> int:
|
655
|
+
"""Calculate overall documentation score."""
|
656
|
+
if not folders:
|
657
|
+
return 0
|
658
|
+
|
659
|
+
scores = []
|
660
|
+
for folder in folders:
|
661
|
+
avg_score = folder.get('avg_documentation_quality', 0)
|
662
|
+
if avg_score > 0:
|
663
|
+
scores.append(avg_score)
|
664
|
+
|
665
|
+
return round(sum(scores) / len(scores)) if scores else 0
|
666
|
+
|
667
|
+
# =================================================================
|
668
|
+
# ISSUE IDENTIFICATION METHODS
|
669
|
+
# =================================================================
|
670
|
+
|
671
|
+
def identify_collection_issues(self, collection_data: Dict) -> List[Dict]:
|
672
|
+
"""Identify collection-level issues."""
|
673
|
+
issues = []
|
674
|
+
|
675
|
+
if not collection_data.get('info', {}).get('description'):
|
676
|
+
issues.append({
|
677
|
+
'type': 'warning',
|
678
|
+
'severity': 'medium',
|
679
|
+
'message': 'Collection lacks description',
|
680
|
+
'location': 'Collection root',
|
681
|
+
'suggestion': 'Add a description explaining the purpose of this collection'
|
682
|
+
})
|
683
|
+
|
684
|
+
if not collection_data.get('auth'):
|
685
|
+
issues.append({
|
686
|
+
'type': 'info',
|
687
|
+
'severity': 'low',
|
688
|
+
'message': 'Collection lacks default authentication',
|
689
|
+
'location': 'Collection root',
|
690
|
+
'suggestion': 'Consider setting up collection-level authentication'
|
691
|
+
})
|
692
|
+
|
693
|
+
return issues
|
694
|
+
|
695
|
+
def identify_folder_issues(self, folder: Dict, requests: List[Dict]) -> List[Dict]:
|
696
|
+
"""Identify folder-level issues."""
|
697
|
+
issues = []
|
698
|
+
|
699
|
+
if not folder.get('description'):
|
700
|
+
issues.append({
|
701
|
+
'type': 'warning',
|
702
|
+
'severity': 'low',
|
703
|
+
'message': 'Folder lacks description',
|
704
|
+
'location': folder['name'],
|
705
|
+
'suggestion': 'Add a description explaining the purpose of this folder'
|
706
|
+
})
|
707
|
+
|
708
|
+
if not requests and (not folder.get('item') or len(folder['item']) == 0):
|
709
|
+
issues.append({
|
710
|
+
'type': 'warning',
|
711
|
+
'severity': 'medium',
|
712
|
+
'message': 'Empty folder',
|
713
|
+
'location': folder['name'],
|
714
|
+
'suggestion': 'Consider removing empty folders or adding requests'
|
715
|
+
})
|
716
|
+
|
717
|
+
return issues
|
718
|
+
|
719
|
+
def generate_request_issues(self, issues: List[Dict], item: Dict, analysis: Dict):
|
720
|
+
"""Generate request-specific issues."""
|
721
|
+
if not analysis['has_description']:
|
722
|
+
issues.append({
|
723
|
+
'type': 'warning',
|
724
|
+
'severity': 'medium',
|
725
|
+
'message': 'Request lacks description',
|
726
|
+
'location': item['name'],
|
727
|
+
'suggestion': 'Add a clear description explaining what this request does'
|
728
|
+
})
|
729
|
+
|
730
|
+
if not analysis['has_auth'] and item['request']['method'] in ['POST', 'PUT', 'PATCH', 'DELETE']:
|
731
|
+
issues.append({
|
732
|
+
'type': 'warning',
|
733
|
+
'severity': 'high',
|
734
|
+
'message': 'Sensitive operation without authentication',
|
735
|
+
'location': item['name'],
|
736
|
+
'suggestion': 'Add authentication for this request'
|
737
|
+
})
|
738
|
+
|
739
|
+
if not analysis['has_tests']:
|
740
|
+
issues.append({
|
741
|
+
'type': 'info',
|
742
|
+
'severity': 'high',
|
743
|
+
'message': 'Request lacks test scripts',
|
744
|
+
'location': item['name'],
|
745
|
+
'suggestion': 'Add test scripts to validate response'
|
746
|
+
})
|
747
|
+
|
748
|
+
if analysis['has_hardcoded_url']:
|
749
|
+
issues.append({
|
750
|
+
'type': 'warning',
|
751
|
+
'severity': 'high',
|
752
|
+
'message': 'Request contains hardcoded URL',
|
753
|
+
'location': item['name'],
|
754
|
+
'suggestion': 'Replace hardcoded URLs with environment variables'
|
755
|
+
})
|
756
|
+
|
757
|
+
if analysis['has_security_issues']:
|
758
|
+
issues.append({
|
759
|
+
'type': 'error',
|
760
|
+
'severity': 'high',
|
761
|
+
'message': 'Security vulnerabilities detected',
|
762
|
+
'location': item['name'],
|
763
|
+
'suggestion': 'Address security issues such as exposed credentials'
|
764
|
+
})
|
765
|
+
|
766
|
+
# =================================================================
|
767
|
+
# IMPROVEMENT GENERATION METHODS
|
768
|
+
# =================================================================
|
769
|
+
|
770
|
+
def generate_recommendations(self, issues: List[Dict]) -> List[str]:
|
771
|
+
"""Generate recommendations based on issues."""
|
772
|
+
recommendations = []
|
773
|
+
suggestion_counts = {}
|
774
|
+
|
775
|
+
# Count similar suggestions
|
776
|
+
for issue in issues:
|
777
|
+
suggestion = issue.get('suggestion', '')
|
778
|
+
if suggestion:
|
779
|
+
suggestion_counts[suggestion] = suggestion_counts.get(suggestion, 0) + 1
|
780
|
+
|
781
|
+
# Generate recommendations from most common suggestions
|
782
|
+
sorted_suggestions = sorted(
|
783
|
+
suggestion_counts.items(), key=lambda x: x[1], reverse=True)[:10]
|
784
|
+
|
785
|
+
for suggestion, count in sorted_suggestions:
|
786
|
+
if count > 1:
|
787
|
+
recommendations.append(f"{suggestion} ({count} instances)")
|
788
|
+
else:
|
789
|
+
recommendations.append(suggestion)
|
790
|
+
|
791
|
+
return recommendations
|
792
|
+
|
793
|
+
def generate_improvements(self, analysis: Dict) -> List[Dict]:
|
794
|
+
"""Generate improvement suggestions with enhanced analysis."""
|
795
|
+
improvements = []
|
796
|
+
|
797
|
+
# Collection-level improvements
|
798
|
+
if analysis['score'] < 80:
|
799
|
+
improvements.append({
|
800
|
+
'id': 'collection-quality',
|
801
|
+
'title': 'Improve Overall Collection Quality',
|
802
|
+
'description': f"Collection quality score is {analysis['score']}/100. Focus on addressing high-priority issues.",
|
803
|
+
'priority': 'high',
|
804
|
+
'category': 'quality',
|
805
|
+
'impact': 'high'
|
806
|
+
})
|
807
|
+
|
808
|
+
if analysis['overall_security_score'] < 70:
|
809
|
+
improvements.append({
|
810
|
+
'id': 'security-enhancement',
|
811
|
+
'title': 'Enhance Security Practices',
|
812
|
+
'description': f"Security score is {analysis['overall_security_score']}/100. Review authentication and data handling.",
|
813
|
+
'priority': 'high',
|
814
|
+
'category': 'security',
|
815
|
+
'impact': 'high'
|
816
|
+
})
|
817
|
+
|
818
|
+
if analysis['overall_documentation_score'] < 60:
|
819
|
+
improvements.append({
|
820
|
+
'id': 'documentation-improvement',
|
821
|
+
'title': 'Improve Documentation',
|
822
|
+
'description': f"Documentation score is {analysis['overall_documentation_score']}/100. Add descriptions and examples.",
|
823
|
+
'priority': 'medium',
|
824
|
+
'category': 'documentation',
|
825
|
+
'impact': 'medium'
|
826
|
+
})
|
827
|
+
|
828
|
+
# Add specific improvements based on common issues
|
829
|
+
issue_counts = {}
|
830
|
+
for folder in analysis.get('folders', []):
|
831
|
+
for request in folder.get('requests', []):
|
832
|
+
for issue in request.get('issues', []):
|
833
|
+
issue_type = issue.get('message', '')
|
834
|
+
issue_counts[issue_type] = issue_counts.get(issue_type, 0) + 1
|
835
|
+
|
836
|
+
# Generate improvements for most common issues
|
837
|
+
if issue_counts.get('Request lacks test scripts', 0) > 3:
|
838
|
+
improvements.append({
|
839
|
+
'id': 'add-test-scripts',
|
840
|
+
'title': 'Add Test Scripts to Requests',
|
841
|
+
'description': f"Found {issue_counts['Request lacks test scripts']} requests without test scripts.",
|
842
|
+
'priority': 'medium',
|
843
|
+
'category': 'testing',
|
844
|
+
'impact': 'medium'
|
845
|
+
})
|
846
|
+
|
847
|
+
if issue_counts.get('Request contains hardcoded URL', 0) > 2:
|
848
|
+
improvements.append({
|
849
|
+
'id': 'use-environment-variables',
|
850
|
+
'title': 'Use Environment Variables',
|
851
|
+
'description': f"Found {issue_counts['Request contains hardcoded URL']} requests with hardcoded URLs.",
|
852
|
+
'priority': 'high',
|
853
|
+
'category': 'maintainability',
|
854
|
+
'impact': 'high'
|
855
|
+
})
|
856
|
+
|
857
|
+
return improvements
|
858
|
+
|
859
|
+
def generate_folder_improvements(self, analysis: Dict) -> List[Dict]:
|
860
|
+
"""Generate improvement suggestions for a specific folder."""
|
861
|
+
improvements = []
|
862
|
+
|
863
|
+
# Folder-level improvements
|
864
|
+
if analysis.get('avg_security_score', 0) < 70:
|
865
|
+
improvements.append({
|
866
|
+
'id': 'folder-security',
|
867
|
+
'title': 'Improve Folder Security',
|
868
|
+
'description': f"Folder security score is {analysis.get('avg_security_score', 0)}/100. Review authentication and data handling.",
|
869
|
+
'priority': 'high',
|
870
|
+
'category': 'security',
|
871
|
+
'impact': 'high'
|
872
|
+
})
|
873
|
+
|
874
|
+
if analysis.get('avg_documentation_quality', 0) < 60:
|
875
|
+
improvements.append({
|
876
|
+
'id': 'folder-documentation',
|
877
|
+
'title': 'Improve Folder Documentation',
|
878
|
+
'description': f"Documentation quality is {analysis.get('avg_documentation_quality', 0)}/100. Add descriptions and examples.",
|
879
|
+
'priority': 'medium',
|
880
|
+
'category': 'documentation',
|
881
|
+
'impact': 'medium'
|
882
|
+
})
|
883
|
+
|
884
|
+
if not analysis.get('has_consistent_naming', True):
|
885
|
+
improvements.append({
|
886
|
+
'id': 'folder-naming-consistency',
|
887
|
+
'title': 'Improve Naming Consistency',
|
888
|
+
'description': "Folder contains inconsistent naming patterns. Consider standardizing request names.",
|
889
|
+
'priority': 'low',
|
890
|
+
'category': 'organization',
|
891
|
+
'impact': 'low'
|
892
|
+
})
|
893
|
+
|
894
|
+
if not analysis.get('auth_consistency', True):
|
895
|
+
improvements.append({
|
896
|
+
'id': 'folder-auth-consistency',
|
897
|
+
'title': 'Standardize Authentication',
|
898
|
+
'description': "Inconsistent authentication methods across requests in this folder.",
|
899
|
+
'priority': 'medium',
|
900
|
+
'category': 'security',
|
901
|
+
'impact': 'medium'
|
902
|
+
})
|
903
|
+
|
904
|
+
# Count specific issues in requests
|
905
|
+
issue_counts = {}
|
906
|
+
for request in analysis.get('requests', []):
|
907
|
+
for issue in request.get('issues', []):
|
908
|
+
issue_type = issue.get('message', '')
|
909
|
+
issue_counts[issue_type] = issue_counts.get(issue_type, 0) + 1
|
910
|
+
|
911
|
+
# Generate improvements for common issues in this folder
|
912
|
+
if issue_counts.get('Request lacks test scripts', 0) > 0:
|
913
|
+
improvements.append({
|
914
|
+
'id': 'folder-add-tests',
|
915
|
+
'title': 'Add Test Scripts',
|
916
|
+
'description': f"Found {issue_counts['Request lacks test scripts']} requests in this folder without test scripts.",
|
917
|
+
'priority': 'medium',
|
918
|
+
'category': 'testing',
|
919
|
+
'impact': 'medium'
|
920
|
+
})
|
921
|
+
|
922
|
+
return improvements
|
923
|
+
|
924
|
+
def generate_request_improvements(self, analysis: Dict) -> List[Dict]:
|
925
|
+
"""Generate improvement suggestions for a specific request."""
|
926
|
+
improvements = []
|
927
|
+
|
928
|
+
# Request-level improvements based on analysis
|
929
|
+
if analysis.get('security_score', 100) < 70:
|
930
|
+
improvements.append({
|
931
|
+
'id': 'request-security',
|
932
|
+
'title': 'Improve Request Security',
|
933
|
+
'description': f"Security score is {analysis.get('security_score', 0)}/100. Review authentication and data handling.",
|
934
|
+
'priority': 'high',
|
935
|
+
'category': 'security',
|
936
|
+
'impact': 'high'
|
937
|
+
})
|
938
|
+
|
939
|
+
if analysis.get('performance_score', 100) < 70:
|
940
|
+
improvements.append({
|
941
|
+
'id': 'request-performance',
|
942
|
+
'title': 'Optimize Request Performance',
|
943
|
+
'description': f"Performance score is {analysis.get('performance_score', 0)}/100. Review request structure and size.",
|
944
|
+
'priority': 'medium',
|
945
|
+
'category': 'performance',
|
946
|
+
'impact': 'medium'
|
947
|
+
})
|
948
|
+
|
949
|
+
if not analysis.get('has_description', True):
|
950
|
+
improvements.append({
|
951
|
+
'id': 'request-add-description',
|
952
|
+
'title': 'Add Request Description',
|
953
|
+
'description': "Request lacks a description. Add documentation to explain its purpose.",
|
954
|
+
'priority': 'low',
|
955
|
+
'category': 'documentation',
|
956
|
+
'impact': 'low'
|
957
|
+
})
|
958
|
+
|
959
|
+
if not analysis.get('has_auth', True):
|
960
|
+
improvements.append({
|
961
|
+
'id': 'request-add-auth',
|
962
|
+
'title': 'Add Authentication',
|
963
|
+
'description': "Request lacks authentication. Consider adding appropriate auth method.",
|
964
|
+
'priority': 'high',
|
965
|
+
'category': 'security',
|
966
|
+
'impact': 'high'
|
967
|
+
})
|
968
|
+
|
969
|
+
if not analysis.get('has_tests', True):
|
970
|
+
improvements.append({
|
971
|
+
'id': 'request-add-tests',
|
972
|
+
'title': 'Add Test Scripts',
|
973
|
+
'description': "Request lacks test scripts. Add tests to validate responses.",
|
974
|
+
'priority': 'medium',
|
975
|
+
'category': 'testing',
|
976
|
+
'impact': 'medium'
|
977
|
+
})
|
978
|
+
|
979
|
+
if analysis.get('has_hardcoded_url', False):
|
980
|
+
improvements.append({
|
981
|
+
'id': 'request-use-variables',
|
982
|
+
'title': 'Use Environment Variables',
|
983
|
+
'description': "Request contains hardcoded URLs. Use environment variables for better maintainability.",
|
984
|
+
'priority': 'high',
|
985
|
+
'category': 'maintainability',
|
986
|
+
'impact': 'high'
|
987
|
+
})
|
988
|
+
|
989
|
+
if analysis.get('has_hardcoded_data', False):
|
990
|
+
improvements.append({
|
991
|
+
'id': 'request-parameterize-data',
|
992
|
+
'title': 'Parameterize Request Data',
|
993
|
+
'description': "Request contains hardcoded data. Consider using variables or dynamic values.",
|
994
|
+
'priority': 'medium',
|
995
|
+
'category': 'maintainability',
|
996
|
+
'impact': 'medium'
|
997
|
+
})
|
998
|
+
|
999
|
+
if not analysis.get('has_proper_headers', True):
|
1000
|
+
improvements.append({
|
1001
|
+
'id': 'request-fix-headers',
|
1002
|
+
'title': 'Fix Request Headers',
|
1003
|
+
'description': "Request headers may be missing or incorrect. Review and add appropriate headers.",
|
1004
|
+
'priority': 'medium',
|
1005
|
+
'category': 'correctness',
|
1006
|
+
'impact': 'medium'
|
1007
|
+
})
|
1008
|
+
|
1009
|
+
if not analysis.get('follows_naming_convention', True):
|
1010
|
+
improvements.append({
|
1011
|
+
'id': 'request-naming-convention',
|
1012
|
+
'title': 'Follow Naming Convention',
|
1013
|
+
'description': "Request name doesn't follow standard conventions. Consider renaming for consistency.",
|
1014
|
+
'priority': 'low',
|
1015
|
+
'category': 'organization',
|
1016
|
+
'impact': 'low'
|
1017
|
+
})
|
1018
|
+
|
1019
|
+
return improvements
|
1020
|
+
|
1021
|
+
# =================================================================
|
1022
|
+
# FINDER METHODS
|
1023
|
+
# =================================================================
|
1024
|
+
|
1025
|
+
def find_folders_by_path(self, items: List[Dict], path: str) -> List[Dict]:
|
1026
|
+
"""Find folders by path (supports nested paths like 'API/Users')."""
|
1027
|
+
path_parts = [part.strip() for part in path.split('/') if part.strip()]
|
1028
|
+
if not path_parts:
|
1029
|
+
return items
|
1030
|
+
|
1031
|
+
results = []
|
1032
|
+
|
1033
|
+
def find_in_items(current_items: List[Dict], current_path: List[str], depth: int = 0):
|
1034
|
+
if depth >= len(current_path):
|
1035
|
+
results.extend(current_items)
|
1036
|
+
return
|
1037
|
+
|
1038
|
+
target_name = current_path[depth]
|
1039
|
+
for item in current_items:
|
1040
|
+
if (item.get('name', '').lower() == target_name.lower() or
|
1041
|
+
target_name.lower() in item.get('name', '').lower()) and item.get('item'):
|
1042
|
+
if depth == len(current_path) - 1:
|
1043
|
+
# This is the target folder
|
1044
|
+
results.append(item)
|
1045
|
+
else:
|
1046
|
+
# Continue searching in subfolders
|
1047
|
+
find_in_items(item['item'], current_path, depth + 1)
|
1048
|
+
|
1049
|
+
find_in_items(items, path_parts)
|
1050
|
+
return results
|
1051
|
+
|
1052
|
+
def find_request_by_path(self, items: List[Dict], request_path: str) -> Optional[Dict]:
|
1053
|
+
"""Find a request by its path."""
|
1054
|
+
path_parts = [part.strip() for part in request_path.split('/') if part.strip()]
|
1055
|
+
if not path_parts:
|
1056
|
+
return None
|
1057
|
+
|
1058
|
+
current_items = items
|
1059
|
+
|
1060
|
+
# Navigate through folders to the request
|
1061
|
+
for i, part in enumerate(path_parts):
|
1062
|
+
found = False
|
1063
|
+
for item in current_items:
|
1064
|
+
if item.get('name', '').lower() == part.lower():
|
1065
|
+
if i == len(path_parts) - 1:
|
1066
|
+
# This should be the request
|
1067
|
+
if item.get('request'):
|
1068
|
+
return item
|
1069
|
+
else:
|
1070
|
+
return None
|
1071
|
+
else:
|
1072
|
+
# This should be a folder
|
1073
|
+
if item.get('item'):
|
1074
|
+
current_items = item['item']
|
1075
|
+
found = True
|
1076
|
+
break
|
1077
|
+
else:
|
1078
|
+
return None
|
1079
|
+
|
1080
|
+
if not found:
|
1081
|
+
return None
|
1082
|
+
|
1083
|
+
return None
|
1084
|
+
|
1085
|
+
def remove_folder_by_path(self, items: List[Dict], folder_path: str) -> bool:
|
1086
|
+
"""Remove a folder by its path."""
|
1087
|
+
path_parts = [part.strip() for part in folder_path.split('/') if part.strip()]
|
1088
|
+
if not path_parts:
|
1089
|
+
return False
|
1090
|
+
|
1091
|
+
if len(path_parts) == 1:
|
1092
|
+
# Remove from current level
|
1093
|
+
for i, item in enumerate(items):
|
1094
|
+
if item.get('name', '').lower() == path_parts[0].lower() and item.get('item') is not None:
|
1095
|
+
del items[i]
|
1096
|
+
return True
|
1097
|
+
return False
|
1098
|
+
else:
|
1099
|
+
# Navigate to parent folder
|
1100
|
+
parent_path = '/'.join(path_parts[:-1])
|
1101
|
+
parent_folders = self.find_folders_by_path(items, parent_path)
|
1102
|
+
if parent_folders:
|
1103
|
+
return self.remove_folder_by_path(parent_folders[0]['item'], path_parts[-1])
|
1104
|
+
return False
|
1105
|
+
|
1106
|
+
def remove_request_by_path(self, items: List[Dict], request_path: str) -> bool:
|
1107
|
+
"""Remove a request by its path."""
|
1108
|
+
path_parts = [part.strip() for part in request_path.split('/') if part.strip()]
|
1109
|
+
if not path_parts:
|
1110
|
+
return False
|
1111
|
+
|
1112
|
+
if len(path_parts) == 1:
|
1113
|
+
# Remove from current level
|
1114
|
+
for i, item in enumerate(items):
|
1115
|
+
if item.get('name', '').lower() == path_parts[0].lower() and item.get('request'):
|
1116
|
+
del items[i]
|
1117
|
+
return True
|
1118
|
+
return False
|
1119
|
+
else:
|
1120
|
+
# Navigate to parent folder
|
1121
|
+
parent_path = '/'.join(path_parts[:-1])
|
1122
|
+
parent_folders = self.find_folders_by_path(items, parent_path)
|
1123
|
+
if parent_folders:
|
1124
|
+
return self.remove_request_by_path(parent_folders[0]['item'], path_parts[-1])
|
1125
|
+
return False
|
1126
|
+
|
1127
|
+
def remove_item_ids(self, items: List[Dict]):
|
1128
|
+
"""Remove IDs from items recursively for duplication."""
|
1129
|
+
for item in items:
|
1130
|
+
if 'id' in item:
|
1131
|
+
del item['id']
|
1132
|
+
if item.get('item'):
|
1133
|
+
self.remove_item_ids(item['item'])
|