mcp-security-framework 1.1.0__py3-none-any.whl → 1.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. mcp_security_framework/__init__.py +26 -15
  2. mcp_security_framework/cli/__init__.py +1 -1
  3. mcp_security_framework/cli/cert_cli.py +233 -197
  4. mcp_security_framework/cli/security_cli.py +324 -234
  5. mcp_security_framework/constants.py +21 -27
  6. mcp_security_framework/core/auth_manager.py +41 -22
  7. mcp_security_framework/core/cert_manager.py +210 -147
  8. mcp_security_framework/core/permission_manager.py +9 -9
  9. mcp_security_framework/core/rate_limiter.py +2 -2
  10. mcp_security_framework/core/security_manager.py +284 -229
  11. mcp_security_framework/examples/__init__.py +6 -0
  12. mcp_security_framework/examples/comprehensive_example.py +349 -279
  13. mcp_security_framework/examples/django_example.py +247 -206
  14. mcp_security_framework/examples/fastapi_example.py +315 -283
  15. mcp_security_framework/examples/flask_example.py +274 -203
  16. mcp_security_framework/examples/gateway_example.py +304 -237
  17. mcp_security_framework/examples/microservice_example.py +258 -189
  18. mcp_security_framework/examples/standalone_example.py +255 -230
  19. mcp_security_framework/examples/test_all_examples.py +151 -135
  20. mcp_security_framework/middleware/__init__.py +46 -55
  21. mcp_security_framework/middleware/auth_middleware.py +62 -63
  22. mcp_security_framework/middleware/fastapi_auth_middleware.py +119 -118
  23. mcp_security_framework/middleware/fastapi_middleware.py +156 -148
  24. mcp_security_framework/middleware/flask_auth_middleware.py +160 -147
  25. mcp_security_framework/middleware/flask_middleware.py +183 -157
  26. mcp_security_framework/middleware/mtls_middleware.py +106 -117
  27. mcp_security_framework/middleware/rate_limit_middleware.py +105 -101
  28. mcp_security_framework/middleware/security_middleware.py +109 -124
  29. mcp_security_framework/schemas/config.py +2 -1
  30. mcp_security_framework/schemas/models.py +18 -6
  31. mcp_security_framework/utils/cert_utils.py +14 -8
  32. mcp_security_framework/utils/datetime_compat.py +116 -0
  33. {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/METADATA +2 -1
  34. mcp_security_framework-1.1.1.dist-info/RECORD +84 -0
  35. tests/conftest.py +63 -66
  36. tests/test_cli/test_cert_cli.py +184 -146
  37. tests/test_cli/test_security_cli.py +274 -247
  38. tests/test_core/test_cert_manager.py +24 -10
  39. tests/test_core/test_security_manager.py +2 -2
  40. tests/test_examples/test_comprehensive_example.py +190 -137
  41. tests/test_examples/test_fastapi_example.py +124 -101
  42. tests/test_examples/test_flask_example.py +124 -101
  43. tests/test_examples/test_standalone_example.py +73 -80
  44. tests/test_integration/test_auth_flow.py +213 -197
  45. tests/test_integration/test_certificate_flow.py +180 -149
  46. tests/test_integration/test_fastapi_integration.py +108 -111
  47. tests/test_integration/test_flask_integration.py +141 -140
  48. tests/test_integration/test_standalone_integration.py +290 -259
  49. tests/test_middleware/test_fastapi_auth_middleware.py +195 -174
  50. tests/test_middleware/test_fastapi_middleware.py +147 -132
  51. tests/test_middleware/test_flask_auth_middleware.py +260 -202
  52. tests/test_middleware/test_flask_middleware.py +201 -179
  53. tests/test_middleware/test_security_middleware.py +145 -130
  54. tests/test_utils/test_datetime_compat.py +147 -0
  55. mcp_security_framework-1.1.0.dist-info/RECORD +0 -82
  56. {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/WHEEL +0 -0
  57. {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/entry_points.txt +0 -0
  58. {mcp_security_framework-1.1.0.dist-info → mcp_security_framework-1.1.1.dist-info}/top_level.txt +0 -0
@@ -14,25 +14,25 @@ Version: 1.0.0
14
14
  License: MIT
15
15
  """
16
16
 
17
- import os
18
- import sys
19
17
  import json
18
+ import os
20
19
  import shutil
21
20
  import subprocess
21
+ import sys
22
22
  import tempfile
23
- from pathlib import Path
24
- from typing import Dict, List, Tuple, Optional
25
- from datetime import datetime
26
23
  import traceback
24
+ from datetime import datetime
25
+ from pathlib import Path
26
+ from typing import Dict, List, Optional, Tuple
27
27
 
28
28
 
29
29
  class ExampleTester:
30
30
  """Test runner for all MCP Security Framework examples."""
31
-
31
+
32
32
  def __init__(self, test_dir: Optional[str] = None, install_deps: bool = True):
33
33
  """
34
34
  Initialize the example tester.
35
-
35
+
36
36
  Args:
37
37
  test_dir: Directory for test environment. If None, creates temporary directory.
38
38
  install_deps: Whether to install missing dependencies automatically.
@@ -42,79 +42,74 @@ class ExampleTester:
42
42
  self.results: Dict[str, Dict] = {}
43
43
  self.errors: List[str] = []
44
44
  self.warnings: List[str] = []
45
-
45
+
46
46
  # Example files to test
47
47
  self.examples = [
48
48
  "standalone_example.py",
49
- "fastapi_example.py",
49
+ "fastapi_example.py",
50
50
  "flask_example.py",
51
51
  "django_example.py",
52
52
  "gateway_example.py",
53
53
  "microservice_example.py",
54
- "comprehensive_example.py"
54
+ "comprehensive_example.py",
55
55
  ]
56
-
56
+
57
57
  # Required dependencies for examples
58
58
  self.required_deps = {
59
59
  "django_example.py": ["django"],
60
60
  "gateway_example.py": ["aiohttp"],
61
- "microservice_example.py": ["aiohttp"]
61
+ "microservice_example.py": ["aiohttp"],
62
62
  }
63
-
63
+
64
64
  print(f"🔧 Test environment: {self.test_dir}")
65
65
  if not self.install_deps:
66
66
  print("⚠️ Automatic dependency installation disabled")
67
-
67
+
68
68
  def setup_environment(self) -> bool:
69
69
  """
70
70
  Set up the test environment with all required files and dependencies.
71
-
71
+
72
72
  Returns:
73
73
  bool: True if setup was successful
74
74
  """
75
75
  print("\n📁 Setting up test environment...")
76
-
76
+
77
77
  try:
78
78
  # Create required directories
79
79
  self._create_directories()
80
-
80
+
81
81
  # Create configuration files
82
82
  self._create_config_files()
83
-
83
+
84
84
  # Create dummy certificates
85
85
  self._create_certificates()
86
-
86
+
87
87
  # Check dependencies
88
88
  self._check_dependencies()
89
-
89
+
90
90
  # Install missing dependencies if enabled
91
91
  if self.install_deps:
92
92
  if not self._install_missing_dependencies():
93
93
  print("❌ Failed to install missing dependencies")
94
94
  return False
95
-
95
+
96
96
  print("✅ Environment setup completed successfully")
97
97
  return True
98
-
98
+
99
99
  except Exception as e:
100
100
  self.errors.append(f"Environment setup failed: {str(e)}")
101
101
  print(f"❌ Environment setup failed: {str(e)}")
102
102
  return False
103
-
103
+
104
104
  def _create_directories(self):
105
105
  """Create required directories for examples."""
106
- directories = [
107
- "certs",
108
- "config",
109
- "logs",
110
- "keys"
111
- ]
112
-
106
+ directories = ["certs", "config", "logs", "keys"]
107
+
113
108
  for directory in directories:
114
109
  dir_path = os.path.join(self.test_dir, directory)
115
110
  os.makedirs(dir_path, exist_ok=True)
116
111
  print(f" 📂 Created directory: {directory}")
117
-
112
+
118
113
  def _create_config_files(self):
119
114
  """Create configuration files for examples."""
120
115
  # Create roles configuration
@@ -123,58 +118,58 @@ class ExampleTester:
123
118
  "admin": {
124
119
  "description": "Administrator role",
125
120
  "permissions": ["*"],
126
- "parent_roles": []
121
+ "parent_roles": [],
127
122
  },
128
123
  "user": {
129
- "description": "User role",
124
+ "description": "User role",
130
125
  "permissions": ["read", "write"],
131
- "parent_roles": []
126
+ "parent_roles": [],
132
127
  },
133
128
  "readonly": {
134
129
  "description": "Read Only role",
135
130
  "permissions": ["read"],
136
- "parent_roles": []
137
- }
131
+ "parent_roles": [],
132
+ },
138
133
  }
139
134
  }
140
-
135
+
141
136
  roles_file = os.path.join(self.test_dir, "config", "roles.json")
142
- with open(roles_file, 'w') as f:
137
+ with open(roles_file, "w") as f:
143
138
  json.dump(roles_config, f, indent=2)
144
-
139
+
145
140
  print(f" 📄 Created roles configuration: config/roles.json")
146
-
141
+
147
142
  def _create_certificates(self):
148
143
  """Create dummy SSL certificates for testing."""
149
144
  certs_dir = os.path.join(self.test_dir, "certs")
150
-
145
+
151
146
  # Server certificate
152
147
  server_cert = os.path.join(certs_dir, "server.crt")
153
- with open(server_cert, 'w') as f:
148
+ with open(server_cert, "w") as f:
154
149
  f.write("-----BEGIN CERTIFICATE-----\n")
155
150
  f.write("DUMMY SERVER CERTIFICATE FOR TESTING\n")
156
151
  f.write("-----END CERTIFICATE-----\n")
157
-
152
+
158
153
  # Server private key
159
154
  server_key = os.path.join(certs_dir, "server.key")
160
- with open(server_key, 'w') as f:
155
+ with open(server_key, "w") as f:
161
156
  f.write("-----BEGIN PRIVATE KEY-----\n")
162
157
  f.write("DUMMY SERVER PRIVATE KEY FOR TESTING\n")
163
158
  f.write("-----END PRIVATE KEY-----\n")
164
-
159
+
165
160
  # CA certificate
166
161
  ca_cert = os.path.join(certs_dir, "ca.crt")
167
- with open(ca_cert, 'w') as f:
162
+ with open(ca_cert, "w") as f:
168
163
  f.write("-----BEGIN CERTIFICATE-----\n")
169
164
  f.write("DUMMY CA CERTIFICATE FOR TESTING\n")
170
165
  f.write("-----END CERTIFICATE-----\n")
171
-
166
+
172
167
  print(f" 🔐 Created dummy SSL certificates")
173
-
168
+
174
169
  def _check_dependencies(self):
175
170
  """Check if required dependencies are available."""
176
171
  print("\n🔍 Checking dependencies...")
177
-
172
+
178
173
  for example, deps in self.required_deps.items():
179
174
  missing_deps = []
180
175
  for dep in deps:
@@ -182,22 +177,24 @@ class ExampleTester:
182
177
  __import__(dep)
183
178
  except ImportError:
184
179
  missing_deps.append(dep)
185
-
180
+
186
181
  if missing_deps:
187
- self.warnings.append(f"Missing dependencies for {example}: {', '.join(missing_deps)}")
182
+ self.warnings.append(
183
+ f"Missing dependencies for {example}: {', '.join(missing_deps)}"
184
+ )
188
185
  print(f" ⚠️ {example}: Missing {', '.join(missing_deps)}")
189
186
  else:
190
187
  print(f" ✅ {example}: All dependencies available")
191
-
188
+
192
189
  def _install_missing_dependencies(self) -> bool:
193
190
  """
194
191
  Install missing dependencies for examples.
195
-
192
+
196
193
  Returns:
197
194
  bool: True if installation was successful
198
195
  """
199
196
  print("\n📦 Installing missing dependencies...")
200
-
197
+
201
198
  # Collect all missing dependencies
202
199
  all_missing_deps = set()
203
200
  for example, deps in self.required_deps.items():
@@ -206,13 +203,13 @@ class ExampleTester:
206
203
  __import__(dep)
207
204
  except ImportError:
208
205
  all_missing_deps.add(dep)
209
-
206
+
210
207
  if not all_missing_deps:
211
208
  print(" ✅ All dependencies are already available")
212
209
  return True
213
-
210
+
214
211
  print(f" 📥 Installing: {', '.join(all_missing_deps)}")
215
-
212
+
216
213
  try:
217
214
  # Install missing dependencies
218
215
  for dep in all_missing_deps:
@@ -221,16 +218,16 @@ class ExampleTester:
221
218
  [sys.executable, "-m", "pip", "install", "--user", dep],
222
219
  capture_output=True,
223
220
  text=True,
224
- timeout=120
221
+ timeout=120,
225
222
  )
226
-
223
+
227
224
  if result.returncode == 0:
228
225
  print(f" ✅ {dep} installed successfully")
229
226
  else:
230
227
  print(f" ❌ Failed to install {dep}: {result.stderr}")
231
228
  self.errors.append(f"Failed to install {dep}: {result.stderr}")
232
229
  return False
233
-
230
+
234
231
  # Verify installation by importing in a new process
235
232
  print("\n🔍 Verifying installations...")
236
233
  for dep in all_missing_deps:
@@ -241,23 +238,23 @@ class ExampleTester:
241
238
  [sys.executable, "-c", test_script],
242
239
  capture_output=True,
243
240
  text=True,
244
- timeout=10
241
+ timeout=10,
245
242
  )
246
-
243
+
247
244
  if result.returncode == 0:
248
245
  print(f" ✅ {dep} is now available")
249
246
  else:
250
247
  print(f" ❌ {dep} is still not available after installation")
251
248
  self.errors.append(f"{dep} installation verification failed")
252
249
  return False
253
-
250
+
254
251
  except Exception as e:
255
252
  print(f" ❌ {dep} verification failed: {str(e)}")
256
253
  self.errors.append(f"{dep} verification failed: {str(e)}")
257
254
  return False
258
-
255
+
259
256
  return True
260
-
257
+
261
258
  except subprocess.TimeoutExpired:
262
259
  self.errors.append("Dependency installation timed out")
263
260
  print(" ❌ Installation timed out")
@@ -266,19 +263,19 @@ class ExampleTester:
266
263
  self.errors.append(f"Dependency installation failed: {str(e)}")
267
264
  print(f" ❌ Installation failed: {str(e)}")
268
265
  return False
269
-
266
+
270
267
  def run_example(self, example_file: str) -> Dict:
271
268
  """
272
269
  Run a single example and capture results.
273
-
270
+
274
271
  Args:
275
272
  example_file: Name of the example file to run
276
-
273
+
277
274
  Returns:
278
275
  Dict: Test results for the example
279
276
  """
280
277
  print(f"\n🚀 Running {example_file}...")
281
-
278
+
282
279
  result = {
283
280
  "file": example_file,
284
281
  "status": "unknown",
@@ -286,36 +283,36 @@ class ExampleTester:
286
283
  "end_time": None,
287
284
  "error": None,
288
285
  "output": "",
289
- "warnings": []
286
+ "warnings": [],
290
287
  }
291
-
288
+
292
289
  try:
293
290
  # Change to test directory
294
291
  original_cwd = os.getcwd()
295
292
  os.chdir(self.test_dir)
296
-
293
+
297
294
  # Get the full path to the example file
298
295
  examples_dir = Path(__file__).parent
299
296
  example_path = examples_dir / example_file
300
-
297
+
301
298
  if not example_path.exists():
302
299
  result["status"] = "error"
303
300
  result["error"] = f"Example file not found: {example_path}"
304
301
  return result
305
-
302
+
306
303
  # Run the example
307
304
  start_time = datetime.now()
308
305
  process = subprocess.run(
309
306
  [sys.executable, str(example_path)],
310
307
  capture_output=True,
311
308
  text=True,
312
- timeout=30 # 30 second timeout
309
+ timeout=30, # 30 second timeout
313
310
  )
314
311
  end_time = datetime.now()
315
-
312
+
316
313
  result["end_time"] = end_time
317
314
  result["output"] = process.stdout + process.stderr
318
-
315
+
319
316
  # Analyze results
320
317
  if process.returncode == 0:
321
318
  result["status"] = "success"
@@ -323,11 +320,11 @@ class ExampleTester:
323
320
  else:
324
321
  # Check if the example actually worked despite test failures
325
322
  output_lower = result["output"].lower()
326
-
323
+
327
324
  # Check for successful initialization and basic functionality
328
325
  success_indicators = [
329
326
  "security manager initialized successfully",
330
- "api key authentication successful",
327
+ "api key authentication successful",
331
328
  "rate limiting test completed",
332
329
  "middleware setup",
333
330
  "fastapi example",
@@ -335,35 +332,46 @@ class ExampleTester:
335
332
  "django example",
336
333
  "gateway example",
337
334
  "microservice example",
338
- "standalone example"
335
+ "standalone example",
339
336
  ]
340
-
341
- has_success_indicators = any(indicator in output_lower for indicator in success_indicators)
342
-
337
+
338
+ has_success_indicators = any(
339
+ indicator in output_lower for indicator in success_indicators
340
+ )
341
+
343
342
  # Check if it's just a test assertion failure (not a real error)
344
- is_test_failure = "assertionerror" in output_lower or "test assertion failed" in output_lower
345
-
343
+ is_test_failure = (
344
+ "assertionerror" in output_lower
345
+ or "test assertion failed" in output_lower
346
+ )
347
+
346
348
  if has_success_indicators and is_test_failure:
347
349
  result["status"] = "success"
348
- result["warnings"].append("Test assertion failed but core functionality works")
349
- print(f" {example_file}: Success (core functionality works, test assertions failed)")
350
+ result["warnings"].append(
351
+ "Test assertion failed but core functionality works"
352
+ )
353
+ print(
354
+ f" ✅ {example_file}: Success (core functionality works, test assertions failed)"
355
+ )
350
356
  else:
351
357
  result["status"] = "error"
352
358
  result["error"] = f"Exit code: {process.returncode}"
353
- print(f" ❌ {example_file}: Failed (exit code: {process.returncode})")
354
-
359
+ print(
360
+ f" ❌ {example_file}: Failed (exit code: {process.returncode})"
361
+ )
362
+
355
363
  # Check for specific patterns in output
356
364
  if "AssertionError" in result["output"]:
357
365
  result["warnings"].append("Test assertion failed")
358
366
  print(f" ⚠️ {example_file}: Test assertion failed")
359
-
367
+
360
368
  if "ModuleNotFoundError" in result["output"]:
361
369
  result["warnings"].append("Missing dependencies")
362
370
  print(f" ⚠️ {example_file}: Missing dependencies")
363
-
371
+
364
372
  # Restore original directory
365
373
  os.chdir(original_cwd)
366
-
374
+
367
375
  except subprocess.TimeoutExpired:
368
376
  result["status"] = "timeout"
369
377
  result["error"] = "Test timed out after 30 seconds"
@@ -372,28 +380,28 @@ class ExampleTester:
372
380
  result["status"] = "error"
373
381
  result["error"] = str(e)
374
382
  print(f" ❌ {example_file}: Exception - {str(e)}")
375
-
383
+
376
384
  return result
377
-
385
+
378
386
  def run_all_examples(self) -> Dict:
379
387
  """
380
388
  Run all examples and collect results.
381
-
389
+
382
390
  Returns:
383
391
  Dict: Summary of all test results
384
392
  """
385
393
  print("\n🎯 Starting test run for all examples...")
386
-
394
+
387
395
  for example in self.examples:
388
396
  result = self.run_example(example)
389
397
  self.results[example] = result
390
-
398
+
391
399
  return self._generate_summary()
392
-
400
+
393
401
  def _generate_summary(self) -> Dict:
394
402
  """
395
403
  Generate a comprehensive test summary.
396
-
404
+
397
405
  Returns:
398
406
  Dict: Test summary with statistics
399
407
  """
@@ -401,7 +409,7 @@ class ExampleTester:
401
409
  successful = sum(1 for r in self.results.values() if r["status"] == "success")
402
410
  failed = sum(1 for r in self.results.values() if r["status"] == "error")
403
411
  timeout = sum(1 for r in self.results.values() if r["status"] == "timeout")
404
-
412
+
405
413
  summary = {
406
414
  "total_examples": total,
407
415
  "successful": successful,
@@ -410,22 +418,22 @@ class ExampleTester:
410
418
  "success_rate": (successful / total * 100) if total > 0 else 0,
411
419
  "results": self.results,
412
420
  "errors": self.errors,
413
- "warnings": self.warnings
421
+ "warnings": self.warnings,
414
422
  }
415
-
423
+
416
424
  return summary
417
-
425
+
418
426
  def print_report(self, summary: Dict):
419
427
  """
420
428
  Print a comprehensive test report.
421
-
429
+
422
430
  Args:
423
431
  summary: Test summary dictionary
424
432
  """
425
- print("\n" + "="*80)
433
+ print("\n" + "=" * 80)
426
434
  print("📊 MCP SECURITY FRAMEWORK EXAMPLES TEST REPORT")
427
- print("="*80)
428
-
435
+ print("=" * 80)
436
+
429
437
  # Overall statistics
430
438
  print(f"\n📈 OVERALL STATISTICS:")
431
439
  print(f" Total examples tested: {summary['total_examples']}")
@@ -433,53 +441,61 @@ class ExampleTester:
433
441
  print(f" Failed: {summary['failed']} ❌")
434
442
  print(f" Timeout: {summary['timeout']} ⏰")
435
443
  print(f" Success rate: {summary['success_rate']:.1f}%")
436
-
444
+
437
445
  # Detailed results
438
446
  print(f"\n📋 DETAILED RESULTS:")
439
447
  for example, result in summary["results"].items():
440
448
  status_icon = {
441
449
  "success": "✅",
442
- "error": "❌",
450
+ "error": "❌",
443
451
  "timeout": "⏰",
444
- "unknown": "❓"
452
+ "unknown": "❓",
445
453
  }.get(result["status"], "❓")
446
-
454
+
447
455
  print(f" {status_icon} {example}")
448
-
456
+
449
457
  if result["status"] == "error" and result["error"]:
450
458
  print(f" Error: {result['error']}")
451
-
459
+
452
460
  if result["warnings"]:
453
461
  for warning in result["warnings"]:
454
462
  print(f" ⚠️ {warning}")
455
-
463
+
456
464
  # Errors and warnings
457
465
  if summary["errors"]:
458
466
  print(f"\n❌ ERRORS:")
459
467
  for error in summary["errors"]:
460
468
  print(f" • {error}")
461
-
469
+
462
470
  if summary["warnings"]:
463
471
  print(f"\n⚠️ WARNINGS:")
464
472
  for warning in summary["warnings"]:
465
473
  print(f" • {warning}")
466
-
474
+
467
475
  # Recommendations
468
476
  print(f"\n💡 RECOMMENDATIONS:")
469
477
  if summary["failed"] > 0:
470
- print(f" • {summary['failed']} examples failed - check dependencies and configuration")
471
-
478
+ print(
479
+ f" • {summary['failed']} examples failed - check dependencies and configuration"
480
+ )
481
+
472
482
  if summary["timeout"] > 0:
473
- print(f" • {summary['timeout']} examples timed out - consider increasing timeout")
474
-
483
+ print(
484
+ f" • {summary['timeout']} examples timed out - consider increasing timeout"
485
+ )
486
+
475
487
  if summary["success_rate"] < 50:
476
- print(f" • Low success rate ({summary['success_rate']:.1f}%) - review setup and dependencies")
488
+ print(
489
+ f" • Low success rate ({summary['success_rate']:.1f}%) - review setup and dependencies"
490
+ )
477
491
  elif summary["success_rate"] >= 80:
478
- print(f" • Excellent success rate ({summary['success_rate']:.1f}%) - examples are working well")
479
-
492
+ print(
493
+ f" • Excellent success rate ({summary['success_rate']:.1f}%) - examples are working well"
494
+ )
495
+
480
496
  print(f"\n🔧 Test environment: {self.test_dir}")
481
- print("="*80)
482
-
497
+ print("=" * 80)
498
+
483
499
  def cleanup(self):
484
500
  """Clean up the test environment."""
485
501
  if self.test_dir and os.path.exists(self.test_dir):
@@ -494,12 +510,12 @@ def main():
494
510
  """Main function to run all example tests."""
495
511
  print("🚀 MCP Security Framework Examples Test Runner")
496
512
  print("=" * 60)
497
-
513
+
498
514
  # Parse command line arguments
499
515
  keep_env = "--keep-env" in sys.argv
500
516
  test_dir = None
501
517
  install_deps = True
502
-
518
+
503
519
  if "--test-dir" in sys.argv:
504
520
  try:
505
521
  idx = sys.argv.index("--test-dir")
@@ -507,30 +523,30 @@ def main():
507
523
  except (IndexError, ValueError):
508
524
  print("❌ Invalid --test-dir argument")
509
525
  sys.exit(1)
510
-
526
+
511
527
  if "--no-install-deps" in sys.argv:
512
528
  install_deps = False
513
529
  print("⚠️ Skipping automatic dependency installation.")
514
-
530
+
515
531
  # Create tester
516
532
  tester = ExampleTester(test_dir, install_deps)
517
-
533
+
518
534
  try:
519
535
  # Setup environment
520
536
  if not tester.setup_environment():
521
537
  print("❌ Failed to setup test environment")
522
538
  sys.exit(1)
523
-
539
+
524
540
  # Run all examples
525
541
  summary = tester.run_all_examples()
526
-
542
+
527
543
  # Print report
528
544
  tester.print_report(summary)
529
-
545
+
530
546
  # Cleanup
531
547
  if not keep_env:
532
548
  tester.cleanup()
533
-
549
+
534
550
  # Exit with appropriate code
535
551
  if summary["failed"] > 0:
536
552
  print(f"\n❌ Test run completed with {summary['failed']} failures")
@@ -538,7 +554,7 @@ def main():
538
554
  else:
539
555
  print(f"\n✅ Test run completed successfully!")
540
556
  sys.exit(0)
541
-
557
+
542
558
  except KeyboardInterrupt:
543
559
  print("\n⏹️ Test run interrupted by user")
544
560
  if not keep_env: