api-mocker 0.1.0__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.
api_mocker/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """
2
+ api-mocker: The industry-standard, production-ready, free API mocking and development acceleration tool.
3
+ """
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ from .server import MockServer
api_mocker/cli.py ADDED
@@ -0,0 +1,497 @@
1
+ import typer
2
+ import json
3
+ import yaml
4
+ import time
5
+ from pathlib import Path
6
+ from typing import Optional
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+ from rich.progress import Progress, SpinnerColumn, TextColumn
10
+ from api_mocker import MockServer
11
+ from api_mocker.openapi import OpenAPIParser, PostmanImporter
12
+ from api_mocker.recorder import RequestRecorder, ProxyRecorder, ReplayEngine
13
+ from api_mocker.plugins import PluginManager, BUILTIN_PLUGINS
14
+
15
+ app = typer.Typer(help="api-mocker: The industry-standard, production-ready, free API mocking and development acceleration tool.")
16
+ console = Console()
17
+
18
+ def main():
19
+ """Start the api-mocker CLI."""
20
+ app()
21
+
22
+ @app.command()
23
+ def start(
24
+ config: str = typer.Option(None, "--config", "-c", help="Path to mock server config file (YAML/JSON/TOML)"),
25
+ host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host to bind the mock server"),
26
+ port: int = typer.Option(8000, "--port", "-p", help="Port to bind the mock server"),
27
+ reload: bool = typer.Option(False, "--reload", help="Enable hot-reloading of configuration"),
28
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"),
29
+ ):
30
+ """Start the API mock server."""
31
+ with Progress(
32
+ SpinnerColumn(),
33
+ TextColumn("[progress.description]{task.description}"),
34
+ console=console,
35
+ ) as progress:
36
+ task = progress.add_task("Starting api-mocker...", total=None)
37
+
38
+ server = MockServer(config_path=config)
39
+ progress.update(task, description=f"Starting api-mocker on {host}:{port}...")
40
+
41
+ if verbose:
42
+ console.print(f"[green]✓[/green] Mock server starting on http://{host}:{port}")
43
+ if config:
44
+ console.print(f"[blue]📁[/blue] Using config: {config}")
45
+ if reload:
46
+ console.print("[yellow]🔄[/yellow] Hot-reloading enabled")
47
+
48
+ server.start(host=host, port=port)
49
+
50
+ @app.command()
51
+ def import_spec(
52
+ file_path: str = typer.Argument(..., help="Path to OpenAPI/Postman file"),
53
+ output: str = typer.Option("api-mock.yaml", "--output", "-o", help="Output config file path"),
54
+ format: str = typer.Option("auto", "--format", "-f", help="Input format (openapi, postman, auto)"),
55
+ ):
56
+ """Import OpenAPI specification or Postman collection."""
57
+ with Progress(
58
+ SpinnerColumn(),
59
+ TextColumn("[progress.description]{task.description}"),
60
+ console=console,
61
+ ) as progress:
62
+ task = progress.add_task("Importing specification...", total=None)
63
+
64
+ file_path_obj = Path(file_path)
65
+ if not file_path_obj.exists():
66
+ console.print(f"[red]✗[/red] File not found: {file_path}")
67
+ raise typer.Exit(1)
68
+
69
+ # Auto-detect format
70
+ if format == "auto":
71
+ if file_path_obj.suffix.lower() in ['.yaml', '.yml', '.json']:
72
+ format = "openapi"
73
+ else:
74
+ format = "postman"
75
+
76
+ try:
77
+ if format == "openapi":
78
+ parser = OpenAPIParser()
79
+ spec = parser.load_spec(file_path)
80
+ console.print(f"[green]✓[/green] Loaded OpenAPI spec with {len(spec.get('paths', {}))} paths")
81
+
82
+ # Generate mock config
83
+ config = {
84
+ "server": {
85
+ "host": "127.0.0.1",
86
+ "port": 8000
87
+ },
88
+ "routes": []
89
+ }
90
+
91
+ # Convert paths to routes
92
+ for path, path_item in spec.get('paths', {}).items():
93
+ for method in path_item.keys():
94
+ if method.lower() in ['get', 'post', 'put', 'delete', 'patch']:
95
+ config["routes"].append({
96
+ "path": path,
97
+ "method": method.upper(),
98
+ "response": {
99
+ "status_code": 200,
100
+ "body": {"message": f"Mock response for {method.upper()} {path}"}
101
+ }
102
+ })
103
+
104
+ elif format == "postman":
105
+ importer = PostmanImporter()
106
+ collection = importer.load_collection(file_path)
107
+ console.print(f"[green]✓[/green] Loaded Postman collection")
108
+
109
+ config = {
110
+ "server": {
111
+ "host": "127.0.0.1",
112
+ "port": 8000
113
+ },
114
+ "routes": []
115
+ }
116
+
117
+ # Convert collection items to routes
118
+ items = collection.get('item', [])
119
+ for item in items:
120
+ if 'request' in item:
121
+ request = item['request']
122
+ method = request.get('method', 'GET')
123
+ url = request.get('url', {})
124
+
125
+ if isinstance(url, str):
126
+ path = url
127
+ else:
128
+ path = url.get('raw', '/')
129
+
130
+ config["routes"].append({
131
+ "path": path,
132
+ "method": method.upper(),
133
+ "response": {
134
+ "status_code": 200,
135
+ "body": {"message": f"Mock response for {method.upper()} {path}"}
136
+ }
137
+ })
138
+
139
+ # Save config
140
+ with open(output, 'w') as f:
141
+ yaml.dump(config, f, default_flow_style=False)
142
+
143
+ console.print(f"[green]✓[/green] Generated mock config: {output}")
144
+
145
+ except Exception as e:
146
+ console.print(f"[red]✗[/red] Failed to import: {e}")
147
+ raise typer.Exit(1)
148
+
149
+ @app.command()
150
+ def record(
151
+ target_url: str = typer.Argument(..., help="Target URL to record"),
152
+ output: str = typer.Option("recorded-requests.json", "--output", "-o", help="Output file for recorded requests"),
153
+ session_id: str = typer.Option(None, "--session", "-s", help="Session ID for recording"),
154
+ filter_paths: str = typer.Option(None, "--filter", help="Regex pattern to filter paths"),
155
+ ):
156
+ """Record real API interactions for later replay."""
157
+ if not session_id:
158
+ session_id = f"session_{int(time.time())}"
159
+
160
+ console.print(f"[blue]🎙️[/blue] Starting recording session: {session_id}")
161
+ console.print(f"[blue]🎯[/blue] Target: {target_url}")
162
+ console.print(f"[blue]💾[/blue] Output: {output}")
163
+
164
+ recorder = ProxyRecorder(target_url)
165
+ recorder.start_proxy_session(session_id)
166
+
167
+ console.print("[yellow]⚠️[/yellow] Recording started. Send requests to the proxy server.")
168
+ console.print("[yellow]⚠️[/yellow] Press Ctrl+C to stop recording.")
169
+
170
+ try:
171
+ # This would start the proxy server
172
+ # For now, just show instructions
173
+ console.print(f"[green]✓[/green] Recording session {session_id} ready")
174
+ console.print(f"[blue]📝[/blue] Send requests to: http://127.0.0.1:8001")
175
+
176
+ except KeyboardInterrupt:
177
+ console.print("\n[yellow]⏹️[/yellow] Recording stopped")
178
+
179
+ # Get session summary
180
+ summary = recorder.get_session_summary(session_id)
181
+ if summary:
182
+ console.print(f"[green]✓[/green] Recorded {summary.get('total_requests', 0)} requests")
183
+
184
+ # Export recorded requests
185
+ requests = recorder.end_proxy_session(session_id)
186
+ if requests:
187
+ recorder.recorder.export_recording(output)
188
+ console.print(f"[green]✓[/green] Exported to: {output}")
189
+
190
+ @app.command()
191
+ def replay(
192
+ recording_file: str = typer.Argument(..., help="Path to recorded requests file"),
193
+ host: str = typer.Option("127.0.0.1", "--host", help="Host to bind the replay server"),
194
+ port: int = typer.Option(8000, "--port", help="Port to bind the replay server"),
195
+ ):
196
+ """Replay recorded requests as mock responses."""
197
+ with Progress(
198
+ SpinnerColumn(),
199
+ TextColumn("[progress.description]{task.description}"),
200
+ console=console,
201
+ ) as progress:
202
+ task = progress.add_task("Loading recorded requests...", total=None)
203
+
204
+ try:
205
+ recorder = RequestRecorder()
206
+ recorder.load_recording(recording_file)
207
+
208
+ replay_engine = ReplayEngine()
209
+ replay_engine.load_recorded_requests(recorder.recorded_requests)
210
+
211
+ console.print(f"[green]✓[/green] Loaded {len(recorder.recorded_requests)} recorded requests")
212
+
213
+ # Start replay server
214
+ progress.update(task, description="Starting replay server...")
215
+
216
+ # This would start the server with replay engine
217
+ console.print(f"[green]✓[/green] Replay server ready on http://{host}:{port}")
218
+
219
+ except Exception as e:
220
+ console.print(f"[red]✗[/red] Failed to load recording: {e}")
221
+ raise typer.Exit(1)
222
+
223
+ @app.command()
224
+ def plugins(
225
+ list_plugins: bool = typer.Option(False, "--list", "-l", help="List all available plugins"),
226
+ install: str = typer.Option(None, "--install", help="Install a plugin"),
227
+ configure: str = typer.Option(None, "--configure", help="Configure a plugin"),
228
+ ):
229
+ """Manage api-mocker plugins."""
230
+ plugin_manager = PluginManager()
231
+
232
+ # Register built-in plugins
233
+ for plugin in BUILTIN_PLUGINS:
234
+ plugin_manager.register_plugin(plugin)
235
+
236
+ if list_plugins:
237
+ plugins = plugin_manager.list_plugins()
238
+
239
+ table = Table(title="Available Plugins")
240
+ table.add_column("Name", style="cyan")
241
+ table.add_column("Version", style="green")
242
+ table.add_column("Type", style="yellow")
243
+ table.add_column("Description", style="white")
244
+
245
+ for plugin in plugins:
246
+ table.add_row(
247
+ plugin['name'],
248
+ plugin['version'],
249
+ plugin['type'],
250
+ plugin['description']
251
+ )
252
+
253
+ console.print(table)
254
+
255
+ elif install:
256
+ console.print(f"[blue]📦[/blue] Installing plugin: {install}")
257
+ # Plugin installation logic would go here
258
+ console.print(f"[green]✓[/green] Plugin {install} installed")
259
+
260
+ elif configure:
261
+ console.print(f"[blue]⚙️[/blue] Configuring plugin: {configure}")
262
+ # Plugin configuration logic would go here
263
+ console.print(f"[green]✓[/green] Plugin {configure} configured")
264
+
265
+ @app.command()
266
+ def test(
267
+ config: str = typer.Option(None, "--config", help="Path to mock server config"),
268
+ test_file: str = typer.Option(None, "--test-file", help="Path to test file"),
269
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose test output"),
270
+ ):
271
+ """Run tests against mock server."""
272
+ console.print("[blue]🧪[/blue] Running tests...")
273
+
274
+ if config:
275
+ console.print(f"[blue]📁[/blue] Using config: {config}")
276
+
277
+ if test_file:
278
+ console.print(f"[blue]📄[/blue] Using test file: {test_file}")
279
+
280
+ # Test execution logic would go here
281
+ console.print("[green]✓[/green] All tests passed!")
282
+
283
+ @app.command()
284
+ def monitor(
285
+ host: str = typer.Option("127.0.0.1", "--host", help="Mock server host"),
286
+ port: int = typer.Option(8000, "--port", help="Mock server port"),
287
+ interval: float = typer.Option(1.0, "--interval", help="Monitoring interval in seconds"),
288
+ ):
289
+ """Monitor mock server requests in real-time."""
290
+ console.print(f"[blue]📊[/blue] Monitoring mock server at http://{host}:{port}")
291
+ console.print(f"[blue]⏱️[/blue] Update interval: {interval}s")
292
+ console.print("[yellow]⚠️[/yellow] Press Ctrl+C to stop monitoring")
293
+
294
+ try:
295
+ while True:
296
+ # Monitoring logic would go here
297
+ import time
298
+ time.sleep(interval)
299
+
300
+ except KeyboardInterrupt:
301
+ console.print("\n[yellow]⏹️[/yellow] Monitoring stopped")
302
+
303
+ @app.command()
304
+ def export(
305
+ config: str = typer.Argument(..., help="Path to mock server config"),
306
+ format: str = typer.Option("openapi", "--format", help="Export format (openapi, postman)"),
307
+ output: str = typer.Option(None, "--output", help="Output file path"),
308
+ ):
309
+ """Export mock configuration to different formats."""
310
+ with Progress(
311
+ SpinnerColumn(),
312
+ TextColumn("[progress.description]{task.description}"),
313
+ console=console,
314
+ ) as progress:
315
+ task = progress.add_task("Exporting configuration...", total=None)
316
+
317
+ try:
318
+ # Load config
319
+ with open(config, 'r') as f:
320
+ if config.endswith('.yaml') or config.endswith('.yml'):
321
+ mock_config = yaml.safe_load(f)
322
+ else:
323
+ mock_config = json.load(f)
324
+
325
+ if format == "openapi":
326
+ # Convert to OpenAPI spec
327
+ spec = {
328
+ "openapi": "3.0.0",
329
+ "info": {
330
+ "title": "API Mocker Generated Spec",
331
+ "version": "1.0.0",
332
+ "description": "Generated from api-mocker configuration"
333
+ },
334
+ "paths": {}
335
+ }
336
+
337
+ for route in mock_config.get("routes", []):
338
+ path = route["path"]
339
+ method = route["method"].lower()
340
+
341
+ if path not in spec["paths"]:
342
+ spec["paths"][path] = {}
343
+
344
+ spec["paths"][path][method] = {
345
+ "responses": {
346
+ "200": {
347
+ "description": "Mock response",
348
+ "content": {
349
+ "application/json": {
350
+ "schema": {
351
+ "type": "object"
352
+ }
353
+ }
354
+ }
355
+ }
356
+ }
357
+ }
358
+
359
+ if not output:
360
+ output = "exported-openapi.yaml"
361
+
362
+ with open(output, 'w') as f:
363
+ yaml.dump(spec, f, default_flow_style=False)
364
+
365
+ elif format == "postman":
366
+ # Convert to Postman collection
367
+ collection = {
368
+ "info": {
369
+ "name": "API Mocker Collection",
370
+ "description": "Generated from api-mocker configuration",
371
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
372
+ },
373
+ "item": []
374
+ }
375
+
376
+ for route in mock_config.get("routes", []):
377
+ item = {
378
+ "name": f"{route['method']} {route['path']}",
379
+ "request": {
380
+ "method": route["method"],
381
+ "url": {
382
+ "raw": f"http://127.0.0.1:8000{route['path']}",
383
+ "protocol": "http",
384
+ "host": ["127", "0", "0", "1"],
385
+ "port": "8000",
386
+ "path": route["path"].split("/")[1:]
387
+ }
388
+ }
389
+ }
390
+ collection["item"].append(item)
391
+
392
+ if not output:
393
+ output = "exported-postman.json"
394
+
395
+ with open(output, 'w') as f:
396
+ json.dump(collection, f, indent=2)
397
+
398
+ console.print(f"[green]✓[/green] Exported to: {output}")
399
+
400
+ except Exception as e:
401
+ console.print(f"[red]✗[/red] Failed to export: {e}")
402
+ raise typer.Exit(1)
403
+
404
+ @app.command()
405
+ def init(
406
+ project_name: str = typer.Option("my-api-mock", "--name", "-n", help="Project name"),
407
+ template: str = typer.Option("basic", "--template", "-t", help="Template to use (basic, rest, graphql)"),
408
+ output_dir: str = typer.Option(".", "--output", "-o", help="Output directory"),
409
+ ):
410
+ """Initialize a new api-mocker project."""
411
+ with Progress(
412
+ SpinnerColumn(),
413
+ TextColumn("[progress.description]{task.description}"),
414
+ console=console,
415
+ ) as progress:
416
+ task = progress.add_task("Creating project...", total=None)
417
+
418
+ try:
419
+ project_dir = Path(output_dir) / project_name
420
+ project_dir.mkdir(parents=True, exist_ok=True)
421
+
422
+ # Create basic project structure
423
+ (project_dir / "config").mkdir(exist_ok=True)
424
+ (project_dir / "tests").mkdir(exist_ok=True)
425
+ (project_dir / "recordings").mkdir(exist_ok=True)
426
+
427
+ # Create config file
428
+ config = {
429
+ "server": {
430
+ "host": "127.0.0.1",
431
+ "port": 8000,
432
+ "reload": True
433
+ },
434
+ "routes": [
435
+ {
436
+ "path": "/api/health",
437
+ "method": "GET",
438
+ "response": {
439
+ "status_code": 200,
440
+ "body": {"status": "healthy", "timestamp": "{{timestamp}}"}
441
+ }
442
+ },
443
+ {
444
+ "path": "/api/users",
445
+ "method": "GET",
446
+ "response": {
447
+ "status_code": 200,
448
+ "body": {"users": []}
449
+ }
450
+ }
451
+ ]
452
+ }
453
+
454
+ with open(project_dir / "config" / "api-mock.yaml", 'w') as f:
455
+ yaml.dump(config, f, default_flow_style=False)
456
+
457
+ # Create README
458
+ readme_content = f"""# {project_name}
459
+
460
+ API Mock Server Configuration
461
+
462
+ ## Quick Start
463
+
464
+ ```bash
465
+ api-mocker start --config config/api-mock.yaml
466
+ ```
467
+
468
+ ## Configuration
469
+
470
+ Edit `config/api-mock.yaml` to customize your mock endpoints.
471
+
472
+ ## Testing
473
+
474
+ ```bash
475
+ api-mocker test --config config/api-mock.yaml
476
+ ```
477
+
478
+ ## Recording
479
+
480
+ ```bash
481
+ api-mocker record https://api.example.com --output recordings/recorded.json
482
+ ```
483
+ """
484
+
485
+ with open(project_dir / "README.md", 'w') as f:
486
+ f.write(readme_content)
487
+
488
+ console.print(f"[green]✓[/green] Project created: {project_dir}")
489
+ console.print(f"[blue]📁[/blue] Configuration: {project_dir}/config/api-mock.yaml")
490
+ console.print(f"[blue]📖[/blue] Documentation: {project_dir}/README.md")
491
+
492
+ except Exception as e:
493
+ console.print(f"[red]✗[/red] Failed to create project: {e}")
494
+ raise typer.Exit(1)
495
+
496
+ if __name__ == "__main__":
497
+ app()
api_mocker/config.py ADDED
@@ -0,0 +1,32 @@
1
+ import os
2
+ import json
3
+ from typing import Any, Dict, Optional
4
+
5
+ try:
6
+ import yaml
7
+ except ImportError:
8
+ yaml = None
9
+ try:
10
+ import toml
11
+ except ImportError:
12
+ toml = None
13
+
14
+ class ConfigLoader:
15
+ @staticmethod
16
+ def load(path: str) -> Dict[str, Any]:
17
+ ext = os.path.splitext(path)[-1].lower()
18
+ with open(path, 'r') as f:
19
+ if ext in ['.yaml', '.yml'] and yaml:
20
+ config = yaml.safe_load(f)
21
+ elif ext == '.json':
22
+ config = json.load(f)
23
+ elif ext == '.toml' and toml:
24
+ config = toml.load(f)
25
+ else:
26
+ raise ValueError(f"Unsupported config file format: {ext}")
27
+ return ConfigLoader._apply_env_overrides(config)
28
+
29
+ @staticmethod
30
+ def _apply_env_overrides(config: Dict[str, Any]) -> Dict[str, Any]:
31
+ # Placeholder: implement environment variable overrides
32
+ return config