api-mocker 0.3.0__py3-none-any.whl → 0.4.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/cli.py +128 -1
- api_mocker/mock_responses.py +622 -0
- {api_mocker-0.3.0.dist-info → api_mocker-0.4.0.dist-info}/METADATA +2 -2
- {api_mocker-0.3.0.dist-info → api_mocker-0.4.0.dist-info}/RECORD +8 -7
- {api_mocker-0.3.0.dist-info → api_mocker-0.4.0.dist-info}/WHEEL +0 -0
- {api_mocker-0.3.0.dist-info → api_mocker-0.4.0.dist-info}/entry_points.txt +0 -0
- {api_mocker-0.3.0.dist-info → api_mocker-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {api_mocker-0.3.0.dist-info → api_mocker-0.4.0.dist-info}/top_level.txt +0 -0
api_mocker/cli.py
CHANGED
|
@@ -16,7 +16,8 @@ from api_mocker.dashboard import DashboardManager
|
|
|
16
16
|
from api_mocker.advanced import AdvancedFeatures, RateLimitConfig, CacheConfig, AuthConfig
|
|
17
17
|
from api_mocker.scenarios import scenario_manager, Scenario, ScenarioCondition, ScenarioResponse, ScenarioType
|
|
18
18
|
from api_mocker.smart_matching import smart_matcher, ResponseRule, MatchCondition, MatchType
|
|
19
|
-
from api_mocker.enhanced_analytics import EnhancedAnalytics
|
|
19
|
+
from api_mocker.enhanced_analytics import EnhancedAnalytics
|
|
20
|
+
from api_mocker.mock_responses import MockSet, MockAPIResponse, ResponseType, HTTPMethod, create_user_response, create_error_response, create_delayed_response
|
|
20
21
|
|
|
21
22
|
app = typer.Typer(help="api-mocker: The industry-standard, production-ready, free API mocking and development acceleration tool.")
|
|
22
23
|
console = Console()
|
|
@@ -1306,5 +1307,131 @@ def enhanced_analytics(
|
|
|
1306
1307
|
raise typer.Exit(1)
|
|
1307
1308
|
|
|
1308
1309
|
|
|
1310
|
+
@app.command()
|
|
1311
|
+
def mock_responses(
|
|
1312
|
+
action: str = typer.Argument(..., help="Mock response action (create, list, find, test, export, import)"),
|
|
1313
|
+
name: str = typer.Option(None, "--name", "-n", help="Response name"),
|
|
1314
|
+
path: str = typer.Option(None, "--path", "-p", help="Response path"),
|
|
1315
|
+
method: str = typer.Option("GET", "--method", "-m", help="HTTP method"),
|
|
1316
|
+
status_code: int = typer.Option(200, "--status", "-s", help="Status code"),
|
|
1317
|
+
response_type: str = typer.Option("static", "--type", "-t", help="Response type (static, dynamic, templated, conditional, delayed, error)"),
|
|
1318
|
+
file: str = typer.Option(None, "--file", "-f", help="Configuration file"),
|
|
1319
|
+
output: str = typer.Option(None, "--output", "-o", help="Output file")
|
|
1320
|
+
):
|
|
1321
|
+
"""Manage mock API responses with advanced features."""
|
|
1322
|
+
|
|
1323
|
+
if action == "create":
|
|
1324
|
+
if not name or not path:
|
|
1325
|
+
console.print("❌ Name and path are required for creating responses")
|
|
1326
|
+
raise typer.Exit(1)
|
|
1327
|
+
|
|
1328
|
+
# Create mock response based on type
|
|
1329
|
+
if response_type == "static":
|
|
1330
|
+
response = MockAPIResponse(
|
|
1331
|
+
path=path,
|
|
1332
|
+
method=HTTPMethod(method),
|
|
1333
|
+
status_code=status_code,
|
|
1334
|
+
name=name,
|
|
1335
|
+
response_type=ResponseType.STATIC,
|
|
1336
|
+
body={"message": "Static response"}
|
|
1337
|
+
)
|
|
1338
|
+
elif response_type == "templated":
|
|
1339
|
+
response = MockAPIResponse(
|
|
1340
|
+
path=path,
|
|
1341
|
+
method=HTTPMethod(method),
|
|
1342
|
+
status_code=status_code,
|
|
1343
|
+
name=name,
|
|
1344
|
+
response_type=ResponseType.TEMPLATED,
|
|
1345
|
+
template_vars={"id": "123", "name": "John Doe"},
|
|
1346
|
+
body={"id": "{{id}}", "name": "{{name}}"}
|
|
1347
|
+
)
|
|
1348
|
+
elif response_type == "delayed":
|
|
1349
|
+
response = MockAPIResponse(
|
|
1350
|
+
path=path,
|
|
1351
|
+
method=HTTPMethod(method),
|
|
1352
|
+
status_code=status_code,
|
|
1353
|
+
name=name,
|
|
1354
|
+
response_type=ResponseType.DELAYED,
|
|
1355
|
+
delay_ms=1000,
|
|
1356
|
+
body={"message": "Delayed response"}
|
|
1357
|
+
)
|
|
1358
|
+
elif response_type == "error":
|
|
1359
|
+
response = MockAPIResponse(
|
|
1360
|
+
path=path,
|
|
1361
|
+
method=HTTPMethod(method),
|
|
1362
|
+
status_code=500,
|
|
1363
|
+
name=name,
|
|
1364
|
+
response_type=ResponseType.ERROR,
|
|
1365
|
+
error_probability=1.0,
|
|
1366
|
+
body={"error": "Simulated error"}
|
|
1367
|
+
)
|
|
1368
|
+
else:
|
|
1369
|
+
response = create_user_response("123", "John Doe")
|
|
1370
|
+
response.name = name
|
|
1371
|
+
response.path = path
|
|
1372
|
+
response.method = HTTPMethod(method)
|
|
1373
|
+
response.status_code = status_code
|
|
1374
|
+
|
|
1375
|
+
console.print(f"✅ Created mock response: {name}")
|
|
1376
|
+
|
|
1377
|
+
elif action == "list":
|
|
1378
|
+
# This would typically load from a file or database
|
|
1379
|
+
console.print("📋 Available mock responses:")
|
|
1380
|
+
console.print(" (Use 'create' to add responses)")
|
|
1381
|
+
|
|
1382
|
+
elif action == "find":
|
|
1383
|
+
if not path:
|
|
1384
|
+
console.print("❌ Path is required for finding responses")
|
|
1385
|
+
raise typer.Exit(1)
|
|
1386
|
+
|
|
1387
|
+
# Simulate finding responses
|
|
1388
|
+
console.print(f"🔍 Searching for responses matching: {path}")
|
|
1389
|
+
console.print(" (Use 'create' to add responses first)")
|
|
1390
|
+
|
|
1391
|
+
elif action == "test":
|
|
1392
|
+
if not path:
|
|
1393
|
+
console.print("❌ Path is required for testing responses")
|
|
1394
|
+
raise typer.Exit(1)
|
|
1395
|
+
|
|
1396
|
+
# Create a test response and test it
|
|
1397
|
+
test_response = create_user_response("123", "John Doe")
|
|
1398
|
+
test_response.path = path
|
|
1399
|
+
test_response.method = HTTPMethod(method)
|
|
1400
|
+
|
|
1401
|
+
result = test_response.generate_response()
|
|
1402
|
+
console.print(f"🧪 Test response for {path}:")
|
|
1403
|
+
console.print(f" Status: {result['status_code']}")
|
|
1404
|
+
console.print(f" Body: {result['body']}")
|
|
1405
|
+
|
|
1406
|
+
elif action == "export":
|
|
1407
|
+
if not output:
|
|
1408
|
+
output = f"mock_responses_{int(time.time())}.yaml"
|
|
1409
|
+
|
|
1410
|
+
# Create a sample mock set and export it
|
|
1411
|
+
mock_set = MockSet("sample_mocks")
|
|
1412
|
+
mock_set.add_response(create_user_response("123", "John Doe"))
|
|
1413
|
+
mock_set.add_response(create_error_response(404, "Not found"))
|
|
1414
|
+
mock_set.add_response(create_delayed_response(1000))
|
|
1415
|
+
|
|
1416
|
+
mock_set.save_to_file(output)
|
|
1417
|
+
console.print(f"✅ Mock responses exported to {output}")
|
|
1418
|
+
|
|
1419
|
+
elif action == "import":
|
|
1420
|
+
if not file:
|
|
1421
|
+
console.print("❌ File is required for importing responses")
|
|
1422
|
+
raise typer.Exit(1)
|
|
1423
|
+
|
|
1424
|
+
try:
|
|
1425
|
+
mock_set = MockSet.load_from_file(file)
|
|
1426
|
+
console.print(f"✅ Imported {len(mock_set.responses)} responses from {file}")
|
|
1427
|
+
except Exception as e:
|
|
1428
|
+
console.print(f"❌ Error importing from {file}: {e}")
|
|
1429
|
+
raise typer.Exit(1)
|
|
1430
|
+
|
|
1431
|
+
else:
|
|
1432
|
+
console.print(f"❌ Unknown action: {action}")
|
|
1433
|
+
raise typer.Exit(1)
|
|
1434
|
+
|
|
1435
|
+
|
|
1309
1436
|
if __name__ == "__main__":
|
|
1310
1437
|
app()
|
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mock API Response Management System
|
|
3
|
+
|
|
4
|
+
This module provides comprehensive functionality for creating and managing mock API responses
|
|
5
|
+
with support for pytest integration, automated testing, and efficient response management.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Dict, List, Optional, Union, Callable
|
|
12
|
+
from enum import Enum
|
|
13
|
+
import pytest
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ResponseType(Enum):
|
|
19
|
+
"""Types of mock responses"""
|
|
20
|
+
STATIC = "static"
|
|
21
|
+
DYNAMIC = "dynamic"
|
|
22
|
+
TEMPLATED = "templated"
|
|
23
|
+
CONDITIONAL = "conditional"
|
|
24
|
+
DELAYED = "delayed"
|
|
25
|
+
ERROR = "error"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class HTTPMethod(Enum):
|
|
29
|
+
"""HTTP methods supported by mock responses"""
|
|
30
|
+
GET = "GET"
|
|
31
|
+
POST = "POST"
|
|
32
|
+
PUT = "PUT"
|
|
33
|
+
DELETE = "DELETE"
|
|
34
|
+
PATCH = "PATCH"
|
|
35
|
+
HEAD = "HEAD"
|
|
36
|
+
OPTIONS = "OPTIONS"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class MockAPIResponse:
|
|
41
|
+
"""
|
|
42
|
+
Core class for creating and managing mock API responses.
|
|
43
|
+
|
|
44
|
+
This class provides comprehensive functionality for defining mock responses
|
|
45
|
+
with support for static data, dynamic generation, templating, and conditional logic.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
# Basic response properties
|
|
49
|
+
path: str
|
|
50
|
+
method: HTTPMethod = HTTPMethod.GET
|
|
51
|
+
status_code: int = 200
|
|
52
|
+
headers: Dict[str, str] = field(default_factory=dict)
|
|
53
|
+
body: Any = None
|
|
54
|
+
|
|
55
|
+
# Response type and behavior
|
|
56
|
+
response_type: ResponseType = ResponseType.STATIC
|
|
57
|
+
delay_ms: int = 0
|
|
58
|
+
error_probability: float = 0.0
|
|
59
|
+
|
|
60
|
+
# Conditional logic
|
|
61
|
+
conditions: List[Dict[str, Any]] = field(default_factory=list)
|
|
62
|
+
priority: int = 0
|
|
63
|
+
|
|
64
|
+
# Dynamic response properties
|
|
65
|
+
template_vars: Dict[str, Any] = field(default_factory=dict)
|
|
66
|
+
generator_func: Optional[Callable] = None
|
|
67
|
+
cache_ttl: int = 300 # 5 minutes default
|
|
68
|
+
|
|
69
|
+
# Metadata
|
|
70
|
+
name: Optional[str] = None
|
|
71
|
+
description: Optional[str] = None
|
|
72
|
+
tags: List[str] = field(default_factory=list)
|
|
73
|
+
created_at: float = field(default_factory=time.time)
|
|
74
|
+
updated_at: float = field(default_factory=time.time)
|
|
75
|
+
|
|
76
|
+
def __post_init__(self):
|
|
77
|
+
"""Initialize response after creation"""
|
|
78
|
+
if self.name is None:
|
|
79
|
+
# Clean up path for name generation - remove leading slash and replace with single underscore
|
|
80
|
+
clean_path = self.path.lstrip('/').replace('/', '_').replace('{', '').replace('}', '')
|
|
81
|
+
self.name = f"{self.method.value}_{clean_path}"
|
|
82
|
+
|
|
83
|
+
# Set default headers if not provided
|
|
84
|
+
if not self.headers:
|
|
85
|
+
self.headers = {
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
"X-Mock-Response": "true"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
def matches_request(self, request_path: str, request_method: str,
|
|
91
|
+
request_headers: Dict[str, str] = None,
|
|
92
|
+
**kwargs) -> bool:
|
|
93
|
+
"""
|
|
94
|
+
Check if this response matches the given request.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
request_path: The request path
|
|
98
|
+
request_method: The HTTP method
|
|
99
|
+
request_headers: Request headers
|
|
100
|
+
request_body: Request body
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
bool: True if response matches request
|
|
104
|
+
"""
|
|
105
|
+
# Basic path and method matching
|
|
106
|
+
if not self._path_matches(request_path) or self.method.value != request_method:
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
# Check conditions if any
|
|
110
|
+
if self.conditions:
|
|
111
|
+
request_body = kwargs.get('body')
|
|
112
|
+
return self._check_conditions(request_headers, request_body)
|
|
113
|
+
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
def _path_matches(self, request_path: str) -> bool:
|
|
117
|
+
"""Check if the request path matches this response's path"""
|
|
118
|
+
# Exact match
|
|
119
|
+
if self.path == request_path:
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
# Pattern matching with wildcards
|
|
123
|
+
if '*' in self.path:
|
|
124
|
+
return self._wildcard_match(request_path)
|
|
125
|
+
|
|
126
|
+
# Parameter matching (e.g., /users/{id})
|
|
127
|
+
if '{' in self.path:
|
|
128
|
+
return self._parameter_match(request_path)
|
|
129
|
+
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
def _wildcard_match(self, request_path: str) -> bool:
|
|
133
|
+
"""Match paths with wildcards"""
|
|
134
|
+
import re
|
|
135
|
+
pattern = self.path.replace('*', '.*')
|
|
136
|
+
return bool(re.match(pattern, request_path))
|
|
137
|
+
|
|
138
|
+
def _parameter_match(self, request_path: str) -> bool:
|
|
139
|
+
"""Match paths with parameters"""
|
|
140
|
+
import re
|
|
141
|
+
# Convert /users/{id} to regex pattern
|
|
142
|
+
pattern = re.sub(r'\{[^}]+\}', r'[^/]+', self.path)
|
|
143
|
+
# Add end anchor to prevent partial matches
|
|
144
|
+
pattern = pattern + '$'
|
|
145
|
+
return bool(re.match(pattern, request_path))
|
|
146
|
+
|
|
147
|
+
def _check_conditions(self, headers: Dict[str, str] = None,
|
|
148
|
+
body: Any = None) -> bool:
|
|
149
|
+
"""Check if request meets all conditions"""
|
|
150
|
+
if not headers:
|
|
151
|
+
headers = {}
|
|
152
|
+
|
|
153
|
+
for condition in self.conditions:
|
|
154
|
+
if not self._evaluate_condition(condition, headers, body):
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
def _evaluate_condition(self, condition: Dict[str, Any],
|
|
160
|
+
headers: Dict[str, str], body: Any) -> bool:
|
|
161
|
+
"""Evaluate a single condition"""
|
|
162
|
+
condition_type = condition.get('type', 'header')
|
|
163
|
+
|
|
164
|
+
if condition_type == 'header':
|
|
165
|
+
header_name = condition.get('name')
|
|
166
|
+
expected_value = condition.get('value')
|
|
167
|
+
return headers.get(header_name) == expected_value
|
|
168
|
+
|
|
169
|
+
elif condition_type == 'body':
|
|
170
|
+
field_path = condition.get('field')
|
|
171
|
+
expected_value = condition.get('value')
|
|
172
|
+
actual_value = self._get_nested_value(body, field_path)
|
|
173
|
+
return actual_value == expected_value
|
|
174
|
+
|
|
175
|
+
elif condition_type == 'custom':
|
|
176
|
+
func = condition.get('function')
|
|
177
|
+
return func(headers, body) if callable(func) else False
|
|
178
|
+
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
def _get_nested_value(self, obj: Any, path: str) -> Any:
|
|
182
|
+
"""Get nested value from object using dot notation"""
|
|
183
|
+
if not path:
|
|
184
|
+
return obj
|
|
185
|
+
|
|
186
|
+
keys = path.split('.')
|
|
187
|
+
current = obj
|
|
188
|
+
|
|
189
|
+
for key in keys:
|
|
190
|
+
if isinstance(current, dict):
|
|
191
|
+
current = current.get(key)
|
|
192
|
+
elif isinstance(current, list) and key.isdigit():
|
|
193
|
+
current = current[int(key)]
|
|
194
|
+
else:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
return current
|
|
198
|
+
|
|
199
|
+
def generate_response(self, request_context: Dict[str, Any] = None) -> Dict[str, Any]:
|
|
200
|
+
"""
|
|
201
|
+
Generate the actual response based on type and context.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
request_context: Additional context for response generation
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Dict containing status_code, headers, and body
|
|
208
|
+
"""
|
|
209
|
+
# Check for errors
|
|
210
|
+
if self.error_probability > 0 and self._should_return_error():
|
|
211
|
+
return self._generate_error_response()
|
|
212
|
+
|
|
213
|
+
# Apply delay if specified
|
|
214
|
+
if self.delay_ms > 0:
|
|
215
|
+
time.sleep(self.delay_ms / 1000.0)
|
|
216
|
+
|
|
217
|
+
# Generate response based on type
|
|
218
|
+
if self.response_type == ResponseType.STATIC:
|
|
219
|
+
body = self.body
|
|
220
|
+
elif self.response_type == ResponseType.DYNAMIC:
|
|
221
|
+
body = self._generate_dynamic_response(request_context)
|
|
222
|
+
elif self.response_type == ResponseType.TEMPLATED:
|
|
223
|
+
body = self._generate_templated_response(request_context)
|
|
224
|
+
else:
|
|
225
|
+
body = self.body
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
'status_code': self.status_code,
|
|
229
|
+
'headers': self.headers.copy(),
|
|
230
|
+
'body': body
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
def _should_return_error(self) -> bool:
|
|
234
|
+
"""Determine if an error should be returned based on probability"""
|
|
235
|
+
import random
|
|
236
|
+
return random.random() < self.error_probability
|
|
237
|
+
|
|
238
|
+
def _generate_error_response(self) -> Dict[str, Any]:
|
|
239
|
+
"""Generate an error response"""
|
|
240
|
+
return {
|
|
241
|
+
'status_code': 500,
|
|
242
|
+
'headers': {
|
|
243
|
+
'Content-Type': 'application/json',
|
|
244
|
+
'X-Mock-Error': 'true'
|
|
245
|
+
},
|
|
246
|
+
'body': {
|
|
247
|
+
'error': 'Internal Server Error',
|
|
248
|
+
'message': 'Mock error response',
|
|
249
|
+
'timestamp': time.time()
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
def _generate_dynamic_response(self, context: Dict[str, Any] = None) -> Any:
|
|
254
|
+
"""Generate dynamic response using generator function"""
|
|
255
|
+
if self.generator_func:
|
|
256
|
+
return self.generator_func(context or {})
|
|
257
|
+
return self.body
|
|
258
|
+
|
|
259
|
+
def _generate_templated_response(self, context: Dict[str, Any] = None) -> Any:
|
|
260
|
+
"""Generate templated response with variable substitution"""
|
|
261
|
+
if isinstance(self.body, dict):
|
|
262
|
+
# Handle dictionary body with template variables
|
|
263
|
+
result = {}
|
|
264
|
+
vars_dict = {**self.template_vars, **(context or {})}
|
|
265
|
+
|
|
266
|
+
for key, value in self.body.items():
|
|
267
|
+
if isinstance(value, str):
|
|
268
|
+
# Replace template variables in string values
|
|
269
|
+
for var_key, var_value in vars_dict.items():
|
|
270
|
+
value = value.replace(f'{{{{{var_key}}}}}', str(var_value))
|
|
271
|
+
result[key] = value
|
|
272
|
+
return result
|
|
273
|
+
elif isinstance(self.body, str):
|
|
274
|
+
template = self.body
|
|
275
|
+
vars_dict = {**self.template_vars, **(context or {})}
|
|
276
|
+
|
|
277
|
+
for key, value in vars_dict.items():
|
|
278
|
+
template = template.replace(f'{{{{{key}}}}}', str(value))
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
return json.loads(template)
|
|
282
|
+
except json.JSONDecodeError:
|
|
283
|
+
return template
|
|
284
|
+
|
|
285
|
+
return self.body
|
|
286
|
+
|
|
287
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
288
|
+
"""Convert response to dictionary for serialization"""
|
|
289
|
+
return {
|
|
290
|
+
'name': self.name,
|
|
291
|
+
'path': self.path,
|
|
292
|
+
'method': self.method.value,
|
|
293
|
+
'status_code': self.status_code,
|
|
294
|
+
'headers': self.headers,
|
|
295
|
+
'body': self.body,
|
|
296
|
+
'response_type': self.response_type.value,
|
|
297
|
+
'delay_ms': self.delay_ms,
|
|
298
|
+
'error_probability': self.error_probability,
|
|
299
|
+
'conditions': self.conditions,
|
|
300
|
+
'priority': self.priority,
|
|
301
|
+
'template_vars': self.template_vars,
|
|
302
|
+
'description': self.description,
|
|
303
|
+
'tags': self.tags,
|
|
304
|
+
'created_at': self.created_at,
|
|
305
|
+
'updated_at': self.updated_at
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
@classmethod
|
|
309
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'MockAPIResponse':
|
|
310
|
+
"""Create response from dictionary"""
|
|
311
|
+
data = data.copy()
|
|
312
|
+
data['method'] = HTTPMethod(data['method'])
|
|
313
|
+
data['response_type'] = ResponseType(data['response_type'])
|
|
314
|
+
return cls(**data)
|
|
315
|
+
|
|
316
|
+
def update(self, **kwargs) -> None:
|
|
317
|
+
"""Update response properties"""
|
|
318
|
+
for key, value in kwargs.items():
|
|
319
|
+
if hasattr(self, key):
|
|
320
|
+
setattr(self, key, value)
|
|
321
|
+
self.updated_at = time.time()
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@dataclass
|
|
325
|
+
class MockSet:
|
|
326
|
+
"""
|
|
327
|
+
Efficient collection for managing multiple mock responses.
|
|
328
|
+
|
|
329
|
+
Provides fast lookup, filtering, and management capabilities for large
|
|
330
|
+
collections of mock responses.
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
name: str
|
|
334
|
+
responses: List[MockAPIResponse] = field(default_factory=list)
|
|
335
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
336
|
+
|
|
337
|
+
def __post_init__(self):
|
|
338
|
+
"""Initialize the mock set"""
|
|
339
|
+
self._build_index()
|
|
340
|
+
|
|
341
|
+
def _build_index(self) -> None:
|
|
342
|
+
"""Build internal indexes for fast lookup"""
|
|
343
|
+
self._path_index = {}
|
|
344
|
+
self._method_index = {}
|
|
345
|
+
self._tag_index = {}
|
|
346
|
+
self._name_index = {}
|
|
347
|
+
|
|
348
|
+
for response in self.responses:
|
|
349
|
+
# Index by path
|
|
350
|
+
if response.path not in self._path_index:
|
|
351
|
+
self._path_index[response.path] = []
|
|
352
|
+
self._path_index[response.path].append(response)
|
|
353
|
+
|
|
354
|
+
# Index by method
|
|
355
|
+
method_key = response.method.value
|
|
356
|
+
if method_key not in self._method_index:
|
|
357
|
+
self._method_index[method_key] = []
|
|
358
|
+
self._method_index[method_key].append(response)
|
|
359
|
+
|
|
360
|
+
# Index by tags
|
|
361
|
+
for tag in response.tags:
|
|
362
|
+
if tag not in self._tag_index:
|
|
363
|
+
self._tag_index[tag] = []
|
|
364
|
+
self._tag_index[tag].append(response)
|
|
365
|
+
|
|
366
|
+
# Index by name
|
|
367
|
+
self._name_index[response.name] = response
|
|
368
|
+
|
|
369
|
+
def add_response(self, response: MockAPIResponse) -> None:
|
|
370
|
+
"""Add a response to the set"""
|
|
371
|
+
self.responses.append(response)
|
|
372
|
+
self._build_index()
|
|
373
|
+
|
|
374
|
+
def remove_response(self, response_name: str) -> bool:
|
|
375
|
+
"""Remove a response by name"""
|
|
376
|
+
if response_name in self._name_index:
|
|
377
|
+
response = self._name_index[response_name]
|
|
378
|
+
self.responses.remove(response)
|
|
379
|
+
self._build_index()
|
|
380
|
+
return True
|
|
381
|
+
return False
|
|
382
|
+
|
|
383
|
+
def find_matching_response(self, path: str, method: str,
|
|
384
|
+
headers: Dict[str, str] = None,
|
|
385
|
+
body: Any = None) -> Optional[MockAPIResponse]:
|
|
386
|
+
"""
|
|
387
|
+
Find the best matching response for a request.
|
|
388
|
+
|
|
389
|
+
Returns the highest priority response that matches the request.
|
|
390
|
+
"""
|
|
391
|
+
matching_responses = []
|
|
392
|
+
|
|
393
|
+
# Find all responses that match the request
|
|
394
|
+
for response in self.responses:
|
|
395
|
+
if response.matches_request(path, method, headers, body):
|
|
396
|
+
matching_responses.append(response)
|
|
397
|
+
|
|
398
|
+
if not matching_responses:
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
# Return the highest priority response
|
|
402
|
+
return max(matching_responses, key=lambda r: r.priority)
|
|
403
|
+
|
|
404
|
+
def get_by_path(self, path: str) -> List[MockAPIResponse]:
|
|
405
|
+
"""Get all responses for a specific path"""
|
|
406
|
+
return self._path_index.get(path, [])
|
|
407
|
+
|
|
408
|
+
def get_by_method(self, method: str) -> List[MockAPIResponse]:
|
|
409
|
+
"""Get all responses for a specific HTTP method"""
|
|
410
|
+
return self._method_index.get(method, [])
|
|
411
|
+
|
|
412
|
+
def get_by_tag(self, tag: str) -> List[MockAPIResponse]:
|
|
413
|
+
"""Get all responses with a specific tag"""
|
|
414
|
+
return self._tag_index.get(tag, [])
|
|
415
|
+
|
|
416
|
+
def get_by_name(self, name: str) -> Optional[MockAPIResponse]:
|
|
417
|
+
"""Get a response by name"""
|
|
418
|
+
return self._name_index.get(name)
|
|
419
|
+
|
|
420
|
+
def filter(self, **kwargs) -> List[MockAPIResponse]:
|
|
421
|
+
"""Filter responses by multiple criteria"""
|
|
422
|
+
filtered = self.responses
|
|
423
|
+
|
|
424
|
+
for key, value in kwargs.items():
|
|
425
|
+
if key == 'status_code':
|
|
426
|
+
filtered = [r for r in filtered if r.status_code == value]
|
|
427
|
+
elif key == 'response_type':
|
|
428
|
+
filtered = [r for r in filtered if r.response_type == value]
|
|
429
|
+
elif key == 'tags':
|
|
430
|
+
if isinstance(value, str):
|
|
431
|
+
filtered = [r for r in filtered if value in r.tags]
|
|
432
|
+
else:
|
|
433
|
+
filtered = [r for r in filtered if any(tag in r.tags for tag in value)]
|
|
434
|
+
|
|
435
|
+
return filtered
|
|
436
|
+
|
|
437
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
438
|
+
"""Convert mock set to dictionary"""
|
|
439
|
+
return {
|
|
440
|
+
'name': self.name,
|
|
441
|
+
'responses': [r.to_dict() for r in self.responses],
|
|
442
|
+
'metadata': self.metadata
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
@classmethod
|
|
446
|
+
def from_dict(cls, data: Dict[str, Any]) -> 'MockSet':
|
|
447
|
+
"""Create mock set from dictionary"""
|
|
448
|
+
responses = [MockAPIResponse.from_dict(r) for r in data['responses']]
|
|
449
|
+
return cls(
|
|
450
|
+
name=data['name'],
|
|
451
|
+
responses=responses,
|
|
452
|
+
metadata=data.get('metadata', {})
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
def save_to_file(self, filepath: str) -> None:
|
|
456
|
+
"""Save mock set to file"""
|
|
457
|
+
with open(filepath, 'w') as f:
|
|
458
|
+
yaml.dump(self.to_dict(), f, default_flow_style=False)
|
|
459
|
+
|
|
460
|
+
@classmethod
|
|
461
|
+
def load_from_file(cls, filepath: str) -> 'MockSet':
|
|
462
|
+
"""Load mock set from file"""
|
|
463
|
+
with open(filepath, 'r') as f:
|
|
464
|
+
data = yaml.safe_load(f)
|
|
465
|
+
return cls.from_dict(data)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
# Example subclasses for common API interactions
|
|
469
|
+
class CommitResponse(MockAPIResponse):
|
|
470
|
+
"""Mock response for Git commit operations"""
|
|
471
|
+
|
|
472
|
+
def __init__(self, **kwargs):
|
|
473
|
+
super().__init__(
|
|
474
|
+
path="/repos/{owner}/{repo}/git/commits",
|
|
475
|
+
method=HTTPMethod.POST,
|
|
476
|
+
status_code=201,
|
|
477
|
+
response_type=ResponseType.TEMPLATED,
|
|
478
|
+
template_vars={
|
|
479
|
+
'sha': 'abc123def456',
|
|
480
|
+
'message': 'feat: add new feature',
|
|
481
|
+
'author': 'John Doe'
|
|
482
|
+
},
|
|
483
|
+
body={
|
|
484
|
+
'sha': '{{sha}}',
|
|
485
|
+
'message': '{{message}}',
|
|
486
|
+
'author': {
|
|
487
|
+
'name': '{{author}}',
|
|
488
|
+
'email': 'john@example.com'
|
|
489
|
+
},
|
|
490
|
+
'committer': {
|
|
491
|
+
'name': '{{author}}',
|
|
492
|
+
'email': 'john@example.com'
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
**kwargs
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class ForkResponse(MockAPIResponse):
|
|
500
|
+
"""Mock response for repository fork operations"""
|
|
501
|
+
|
|
502
|
+
def __init__(self, **kwargs):
|
|
503
|
+
super().__init__(
|
|
504
|
+
path="/repos/{owner}/{repo}/forks",
|
|
505
|
+
method=HTTPMethod.POST,
|
|
506
|
+
status_code=202,
|
|
507
|
+
response_type=ResponseType.STATIC,
|
|
508
|
+
body={
|
|
509
|
+
'id': 12345,
|
|
510
|
+
'name': 'forked-repo',
|
|
511
|
+
'full_name': 'new-owner/forked-repo',
|
|
512
|
+
'fork': True,
|
|
513
|
+
'source': {
|
|
514
|
+
'id': 67890,
|
|
515
|
+
'name': 'original-repo',
|
|
516
|
+
'full_name': 'original-owner/original-repo'
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
**kwargs
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
class PushResponse(MockAPIResponse):
|
|
524
|
+
"""Mock response for Git push operations"""
|
|
525
|
+
|
|
526
|
+
def __init__(self, **kwargs):
|
|
527
|
+
super().__init__(
|
|
528
|
+
path="/repos/{owner}/{repo}/git/refs/heads/{branch}",
|
|
529
|
+
method=HTTPMethod.PATCH,
|
|
530
|
+
status_code=200,
|
|
531
|
+
response_type=ResponseType.TEMPLATED,
|
|
532
|
+
template_vars={
|
|
533
|
+
'ref': 'refs/heads/main',
|
|
534
|
+
'sha': 'def456ghi789'
|
|
535
|
+
},
|
|
536
|
+
body={
|
|
537
|
+
'ref': '{{ref}}',
|
|
538
|
+
'sha': '{{sha}}',
|
|
539
|
+
'url': 'https://api.github.com/repos/owner/repo/git/refs/heads/main'
|
|
540
|
+
},
|
|
541
|
+
**kwargs
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
class ForcePushResponse(MockAPIResponse):
|
|
546
|
+
"""Mock response for force push operations"""
|
|
547
|
+
|
|
548
|
+
def __init__(self, **kwargs):
|
|
549
|
+
super().__init__(
|
|
550
|
+
path="/repos/{owner}/{repo}/git/refs/heads/{branch}",
|
|
551
|
+
method=HTTPMethod.PATCH,
|
|
552
|
+
status_code=200,
|
|
553
|
+
response_type=ResponseType.STATIC,
|
|
554
|
+
body={
|
|
555
|
+
'ref': 'refs/heads/main',
|
|
556
|
+
'sha': 'force123push456',
|
|
557
|
+
'force': True,
|
|
558
|
+
'url': 'https://api.github.com/repos/owner/repo/git/refs/heads/main'
|
|
559
|
+
},
|
|
560
|
+
**kwargs
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
# Pytest fixture for easy integration
|
|
565
|
+
@pytest.fixture
|
|
566
|
+
def setup_api_mocks():
|
|
567
|
+
"""
|
|
568
|
+
Pytest fixture for setting up mock API responses in tests.
|
|
569
|
+
|
|
570
|
+
Usage:
|
|
571
|
+
def test_api_call(setup_api_mocks):
|
|
572
|
+
mock_set = setup_api_mocks
|
|
573
|
+
mock_set.add_response(CommitResponse())
|
|
574
|
+
# Your test code here
|
|
575
|
+
"""
|
|
576
|
+
mock_set = MockSet("test_mocks")
|
|
577
|
+
return mock_set
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
# Convenience functions for common operations
|
|
581
|
+
def create_user_response(user_id: str = "123", name: str = "John Doe") -> MockAPIResponse:
|
|
582
|
+
"""Create a mock user response"""
|
|
583
|
+
return MockAPIResponse(
|
|
584
|
+
path=f"/users/{user_id}",
|
|
585
|
+
method=HTTPMethod.GET,
|
|
586
|
+
status_code=200,
|
|
587
|
+
response_type=ResponseType.TEMPLATED,
|
|
588
|
+
template_vars={'user_id': user_id, 'name': name},
|
|
589
|
+
body={
|
|
590
|
+
'id': '{{user_id}}',
|
|
591
|
+
'name': '{{name}}',
|
|
592
|
+
'email': 'john@example.com',
|
|
593
|
+
'created_at': '2023-01-01T00:00:00Z'
|
|
594
|
+
}
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def create_error_response(status_code: int = 404, message: str = "Not found") -> MockAPIResponse:
|
|
599
|
+
"""Create a mock error response"""
|
|
600
|
+
return MockAPIResponse(
|
|
601
|
+
path="*",
|
|
602
|
+
method=HTTPMethod.GET,
|
|
603
|
+
status_code=status_code,
|
|
604
|
+
response_type=ResponseType.STATIC,
|
|
605
|
+
body={
|
|
606
|
+
'error': True,
|
|
607
|
+
'message': message,
|
|
608
|
+
'status_code': status_code
|
|
609
|
+
}
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def create_delayed_response(delay_ms: int = 1000) -> MockAPIResponse:
|
|
614
|
+
"""Create a mock response with delay"""
|
|
615
|
+
return MockAPIResponse(
|
|
616
|
+
path="/slow-endpoint",
|
|
617
|
+
method=HTTPMethod.GET,
|
|
618
|
+
status_code=200,
|
|
619
|
+
response_type=ResponseType.STATIC,
|
|
620
|
+
delay_ms=delay_ms,
|
|
621
|
+
body={'message': 'Response delayed'}
|
|
622
|
+
)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: api-mocker
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: 🚀 The Ultimate API Development Acceleration Tool - 3000+ Downloads! Production-ready FastAPI mock server with AI-powered generation, scenario-based mocking, smart response matching, enhanced analytics,
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: 🚀 The Ultimate API Development Acceleration Tool - 3000+ Downloads! Production-ready FastAPI mock server with AI-powered generation, scenario-based mocking, smart response matching, enhanced analytics, comprehensive testing framework, and advanced mock response management system.
|
|
5
5
|
Author-email: sherin joseph roy <sherin.joseph2217@gmail.com>
|
|
6
6
|
License: MIT License
|
|
7
7
|
|
|
@@ -2,11 +2,12 @@ api_mocker/__init__.py,sha256=4krN1yJyngDrqVf6weYU5n3cpHpej8tE97frHPXxYqM,168
|
|
|
2
2
|
api_mocker/advanced.py,sha256=vf7pzC-ouVgT7PkSSMKLa423Z--Lj9ihC-OCNAhPOro,13429
|
|
3
3
|
api_mocker/ai_generator.py,sha256=mdha8_9HKiD9CKOT2MnJvaPC_59RwFW5NcUsGuKPP2c,17420
|
|
4
4
|
api_mocker/analytics.py,sha256=dO4uuoi-YmY6bSBYYahiwnXvTXOylR6RVDiq46UIMoA,14205
|
|
5
|
-
api_mocker/cli.py,sha256=
|
|
5
|
+
api_mocker/cli.py,sha256=5DQlVRddvvEmajXHqlnOWkTc06mNvtZTyWe7WPftkYU,60718
|
|
6
6
|
api_mocker/config.py,sha256=zNlJCk1Bs0BrGU-92wiFv2ZTBRu9dJQ6sF8Dh6kIhLQ,913
|
|
7
7
|
api_mocker/core.py,sha256=K3rP5_cJIEpr02Qgcc_n1Ga3KPo4HumsA6Dlynaj_nQ,8478
|
|
8
8
|
api_mocker/dashboard.py,sha256=OnZOTNgKXgDU3FON6avwZ4R7NRJjqCUhQePadvRBHHM,14000
|
|
9
9
|
api_mocker/enhanced_analytics.py,sha256=cSTLOft7oKZwDuy5ibUvfuSfRHmkAr9GQYU5DvtVOwI,23028
|
|
10
|
+
api_mocker/mock_responses.py,sha256=au9-aXBXqfct_hLhCbwYgbNEfxJqPTCmETqJVsbszqU,21153
|
|
10
11
|
api_mocker/openapi.py,sha256=Pb1gKbBWosEV5i739rW0Nb3ArNq62lgMN0ecyvigNKY,7403
|
|
11
12
|
api_mocker/plugins.py,sha256=OK3OVHJszDky46JHntMVsZUH1ajBjBhAKq3TCDYuxWI,8178
|
|
12
13
|
api_mocker/recorder.py,sha256=7tiT2Krxy3nLDxFAE7rpZSimuD-rKeiwdU72cp0dg6E,9984
|
|
@@ -14,9 +15,9 @@ api_mocker/scenarios.py,sha256=wadcxu4Gp8w7i-UlPr6PlbcYnrSd1ehZA81e9dxGTgc,13392
|
|
|
14
15
|
api_mocker/server.py,sha256=xfczRj4xFXGVaGn2pVPgGvYyv3IHUlYTEz3Hop1KQu0,3812
|
|
15
16
|
api_mocker/smart_matching.py,sha256=DvTSKQwo4MhPEUHWdV3zF_H_dmp-l-47I59zz41tNe0,15067
|
|
16
17
|
api_mocker/testing.py,sha256=z4yJqS5MaSBOThpf3GtUY4dCzXTgopmnGnCuvnmKkF4,24949
|
|
17
|
-
api_mocker-0.
|
|
18
|
-
api_mocker-0.
|
|
19
|
-
api_mocker-0.
|
|
20
|
-
api_mocker-0.
|
|
21
|
-
api_mocker-0.
|
|
22
|
-
api_mocker-0.
|
|
18
|
+
api_mocker-0.4.0.dist-info/licenses/LICENSE,sha256=FzyeLcPe623lrwpFx3xQ3W0Hb_S2sbHqLzhSXaTmcGg,1074
|
|
19
|
+
api_mocker-0.4.0.dist-info/METADATA,sha256=TIiH9dsICsrjrA69vqUuQT1y8VgMrJc3Ixi5y4F3hlI,14554
|
|
20
|
+
api_mocker-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
+
api_mocker-0.4.0.dist-info/entry_points.txt,sha256=dj0UIkQ36Uq3oeSjGzmRRUQKFriq4WMCzg7TCor7wkM,51
|
|
22
|
+
api_mocker-0.4.0.dist-info/top_level.txt,sha256=ZcowEudKsJ6xbvOXIno2zZcPhjB-gGO1w7uzoUKRKDM,11
|
|
23
|
+
api_mocker-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|