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.
@@ -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'])