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 +7 -0
- api_mocker/cli.py +497 -0
- api_mocker/config.py +32 -0
- api_mocker/core.py +237 -0
- api_mocker/openapi.py +200 -0
- api_mocker/plugins.py +247 -0
- api_mocker/recorder.py +266 -0
- api_mocker/server.py +96 -0
- api_mocker-0.1.0.dist-info/METADATA +98 -0
- api_mocker-0.1.0.dist-info/RECORD +14 -0
- api_mocker-0.1.0.dist-info/WHEEL +5 -0
- api_mocker-0.1.0.dist-info/entry_points.txt +2 -0
- api_mocker-0.1.0.dist-info/licenses/LICENSE +21 -0
- api_mocker-0.1.0.dist-info/top_level.txt +1 -0
api_mocker/__init__.py
ADDED
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
|