api-mocker 0.2.0__py3-none-any.whl → 0.3.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 +405 -0
- api_mocker/enhanced_analytics.py +542 -0
- api_mocker/scenarios.py +338 -0
- api_mocker/smart_matching.py +415 -0
- {api_mocker-0.2.0.dist-info → api_mocker-0.3.0.dist-info}/METADATA +2 -2
- {api_mocker-0.2.0.dist-info → api_mocker-0.3.0.dist-info}/RECORD +10 -7
- {api_mocker-0.2.0.dist-info → api_mocker-0.3.0.dist-info}/WHEEL +0 -0
- {api_mocker-0.2.0.dist-info → api_mocker-0.3.0.dist-info}/entry_points.txt +0 -0
- {api_mocker-0.2.0.dist-info → api_mocker-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {api_mocker-0.2.0.dist-info → api_mocker-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Smart Response Matching System
|
|
3
|
+
|
|
4
|
+
Provides intelligent response selection based on:
|
|
5
|
+
- Request body analysis for response selection
|
|
6
|
+
- Header-based routing
|
|
7
|
+
- Query parameter matching
|
|
8
|
+
- Custom logic for response selection
|
|
9
|
+
- Dynamic response generation based on request context
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
from typing import Dict, Any, List, Optional, Callable, Union, Tuple
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
import operator
|
|
18
|
+
from enum import Enum
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MatchType(Enum):
|
|
22
|
+
EXACT = "exact"
|
|
23
|
+
REGEX = "regex"
|
|
24
|
+
CONTAINS = "contains"
|
|
25
|
+
GREATER_THAN = "greater_than"
|
|
26
|
+
LESS_THAN = "less_than"
|
|
27
|
+
EQUALS = "equals"
|
|
28
|
+
NOT_EQUALS = "not_equals"
|
|
29
|
+
EXISTS = "exists"
|
|
30
|
+
NOT_EXISTS = "not_exists"
|
|
31
|
+
CUSTOM = "custom"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class MatchCondition:
|
|
36
|
+
"""Defines a condition for matching requests."""
|
|
37
|
+
field: str # e.g., "body.user_id", "headers.authorization", "query.page"
|
|
38
|
+
match_type: MatchType
|
|
39
|
+
value: Any = None
|
|
40
|
+
custom_function: Optional[Callable] = None
|
|
41
|
+
case_sensitive: bool = True
|
|
42
|
+
|
|
43
|
+
def matches(self, request_data: Dict[str, Any]) -> bool:
|
|
44
|
+
"""Check if the condition matches the request data."""
|
|
45
|
+
field_value = self._extract_field_value(request_data, self.field)
|
|
46
|
+
|
|
47
|
+
if self.match_type == MatchType.CUSTOM and self.custom_function:
|
|
48
|
+
return self.custom_function(field_value, request_data)
|
|
49
|
+
|
|
50
|
+
if self.match_type == MatchType.EXISTS:
|
|
51
|
+
return field_value is not None
|
|
52
|
+
|
|
53
|
+
if self.match_type == MatchType.NOT_EXISTS:
|
|
54
|
+
return field_value is None
|
|
55
|
+
|
|
56
|
+
if field_value is None:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
if self.match_type == MatchType.EXACT:
|
|
60
|
+
return field_value == self.value
|
|
61
|
+
|
|
62
|
+
if self.match_type == MatchType.REGEX:
|
|
63
|
+
if not isinstance(field_value, str):
|
|
64
|
+
return False
|
|
65
|
+
flags = 0 if self.case_sensitive else re.IGNORECASE
|
|
66
|
+
return bool(re.search(self.value, field_value, flags))
|
|
67
|
+
|
|
68
|
+
if self.match_type == MatchType.CONTAINS:
|
|
69
|
+
if isinstance(field_value, str) and isinstance(self.value, str):
|
|
70
|
+
if not self.case_sensitive:
|
|
71
|
+
return self.value.lower() in field_value.lower()
|
|
72
|
+
return self.value in field_value
|
|
73
|
+
elif isinstance(field_value, (list, dict)):
|
|
74
|
+
return self.value in field_value
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
if self.match_type == MatchType.GREATER_THAN:
|
|
78
|
+
return self._compare_values(field_value, self.value, operator.gt)
|
|
79
|
+
|
|
80
|
+
if self.match_type == MatchType.LESS_THAN:
|
|
81
|
+
return self._compare_values(field_value, self.value, operator.lt)
|
|
82
|
+
|
|
83
|
+
if self.match_type == MatchType.EQUALS:
|
|
84
|
+
return field_value == self.value
|
|
85
|
+
|
|
86
|
+
if self.match_type == MatchType.NOT_EQUALS:
|
|
87
|
+
return field_value != self.value
|
|
88
|
+
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def _extract_field_value(self, data: Dict[str, Any], field_path: str) -> Any:
|
|
92
|
+
"""Extract a value from nested data using dot notation."""
|
|
93
|
+
keys = field_path.split('.')
|
|
94
|
+
current = data
|
|
95
|
+
|
|
96
|
+
for key in keys:
|
|
97
|
+
if isinstance(current, dict) and key in current:
|
|
98
|
+
current = current[key]
|
|
99
|
+
elif isinstance(current, list) and key.isdigit():
|
|
100
|
+
index = int(key)
|
|
101
|
+
if 0 <= index < len(current):
|
|
102
|
+
current = current[index]
|
|
103
|
+
else:
|
|
104
|
+
return None
|
|
105
|
+
else:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
return current
|
|
109
|
+
|
|
110
|
+
def _compare_values(self, a: Any, b: Any, op: Callable) -> bool:
|
|
111
|
+
"""Compare two values using the specified operator."""
|
|
112
|
+
try:
|
|
113
|
+
# Try to convert to numbers for comparison
|
|
114
|
+
if isinstance(a, str) and a.isdigit():
|
|
115
|
+
a = int(a)
|
|
116
|
+
if isinstance(b, str) and b.isdigit():
|
|
117
|
+
b = int(b)
|
|
118
|
+
|
|
119
|
+
if isinstance(a, (int, float)) and isinstance(b, (int, float)):
|
|
120
|
+
return op(a, b)
|
|
121
|
+
|
|
122
|
+
# Fallback to string comparison
|
|
123
|
+
return op(str(a), str(b))
|
|
124
|
+
except (ValueError, TypeError):
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class ResponseRule:
|
|
130
|
+
"""Defines a response rule with conditions and response."""
|
|
131
|
+
name: str
|
|
132
|
+
conditions: List[MatchCondition]
|
|
133
|
+
response: Dict[str, Any]
|
|
134
|
+
priority: int = 0 # Higher priority rules are checked first
|
|
135
|
+
weight: float = 1.0 # For weighted random selection
|
|
136
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class SmartResponseMatcher:
|
|
140
|
+
"""Intelligent response matching system."""
|
|
141
|
+
|
|
142
|
+
def __init__(self):
|
|
143
|
+
self.rules: List[ResponseRule] = []
|
|
144
|
+
self.default_response: Optional[Dict[str, Any]] = None
|
|
145
|
+
self.match_history: List[Dict] = []
|
|
146
|
+
|
|
147
|
+
def add_rule(self, rule: ResponseRule):
|
|
148
|
+
"""Add a response rule."""
|
|
149
|
+
self.rules.append(rule)
|
|
150
|
+
# Sort rules by priority (highest first)
|
|
151
|
+
self.rules.sort(key=lambda r: r.priority, reverse=True)
|
|
152
|
+
|
|
153
|
+
def set_default_response(self, response: Dict[str, Any]):
|
|
154
|
+
"""Set the default response when no rules match."""
|
|
155
|
+
self.default_response = response
|
|
156
|
+
|
|
157
|
+
def find_matching_response(self, request_data: Dict[str, Any]) -> Tuple[Optional[Dict[str, Any]], Optional[ResponseRule]]:
|
|
158
|
+
"""Find the best matching response for the request."""
|
|
159
|
+
matching_rules = []
|
|
160
|
+
|
|
161
|
+
for rule in self.rules:
|
|
162
|
+
if self._rule_matches(rule, request_data):
|
|
163
|
+
matching_rules.append(rule)
|
|
164
|
+
|
|
165
|
+
if not matching_rules:
|
|
166
|
+
self._log_match("no_match", None, request_data)
|
|
167
|
+
return self.default_response, None
|
|
168
|
+
|
|
169
|
+
# If multiple rules match, use weighted random selection
|
|
170
|
+
if len(matching_rules) > 1:
|
|
171
|
+
selected_rule = self._select_weighted_rule(matching_rules)
|
|
172
|
+
else:
|
|
173
|
+
selected_rule = matching_rules[0]
|
|
174
|
+
|
|
175
|
+
self._log_match("match", selected_rule, request_data)
|
|
176
|
+
return selected_rule.response, selected_rule
|
|
177
|
+
|
|
178
|
+
def _rule_matches(self, rule: ResponseRule, request_data: Dict[str, Any]) -> bool:
|
|
179
|
+
"""Check if a rule matches the request data."""
|
|
180
|
+
for condition in rule.conditions:
|
|
181
|
+
if not condition.matches(request_data):
|
|
182
|
+
return False
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
def _select_weighted_rule(self, rules: List[ResponseRule]) -> ResponseRule:
|
|
186
|
+
"""Select a rule using weighted random selection."""
|
|
187
|
+
import random
|
|
188
|
+
|
|
189
|
+
total_weight = sum(rule.weight for rule in rules)
|
|
190
|
+
if total_weight == 0:
|
|
191
|
+
return rules[0] # Fallback to first rule
|
|
192
|
+
|
|
193
|
+
rand = random.uniform(0, total_weight)
|
|
194
|
+
current_weight = 0
|
|
195
|
+
|
|
196
|
+
for rule in rules:
|
|
197
|
+
current_weight += rule.weight
|
|
198
|
+
if rand <= current_weight:
|
|
199
|
+
return rule
|
|
200
|
+
|
|
201
|
+
return rules[-1] # Fallback to last rule
|
|
202
|
+
|
|
203
|
+
def create_user_type_rule(self, user_type: str, response: Dict[str, Any]) -> ResponseRule:
|
|
204
|
+
"""Create a rule based on user type in request body."""
|
|
205
|
+
return ResponseRule(
|
|
206
|
+
name=f"user_type_{user_type}",
|
|
207
|
+
conditions=[
|
|
208
|
+
MatchCondition(
|
|
209
|
+
field="body.user_type",
|
|
210
|
+
match_type=MatchType.EXACT,
|
|
211
|
+
value=user_type
|
|
212
|
+
)
|
|
213
|
+
],
|
|
214
|
+
response=response,
|
|
215
|
+
priority=10
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def create_api_version_rule(self, version: str, response: Dict[str, Any]) -> ResponseRule:
|
|
219
|
+
"""Create a rule based on API version in headers."""
|
|
220
|
+
return ResponseRule(
|
|
221
|
+
name=f"api_version_{version}",
|
|
222
|
+
conditions=[
|
|
223
|
+
MatchCondition(
|
|
224
|
+
field="headers.x-api-version",
|
|
225
|
+
match_type=MatchType.EXACT,
|
|
226
|
+
value=version
|
|
227
|
+
)
|
|
228
|
+
],
|
|
229
|
+
response=response,
|
|
230
|
+
priority=15
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def create_premium_user_rule(self, response: Dict[str, Any]) -> ResponseRule:
|
|
234
|
+
"""Create a rule for premium users."""
|
|
235
|
+
return ResponseRule(
|
|
236
|
+
name="premium_user",
|
|
237
|
+
conditions=[
|
|
238
|
+
MatchCondition(
|
|
239
|
+
field="body.user_type",
|
|
240
|
+
match_type=MatchType.EXACT,
|
|
241
|
+
value="premium"
|
|
242
|
+
),
|
|
243
|
+
MatchCondition(
|
|
244
|
+
field="headers.authorization",
|
|
245
|
+
match_type=MatchType.EXISTS
|
|
246
|
+
)
|
|
247
|
+
],
|
|
248
|
+
response=response,
|
|
249
|
+
priority=20
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def create_rate_limit_rule(self, threshold: int, response: Dict[str, Any]) -> ResponseRule:
|
|
253
|
+
"""Create a rule for rate limiting based on request count."""
|
|
254
|
+
def rate_limit_check(field_value, request_data):
|
|
255
|
+
# This would typically check against a rate limiter
|
|
256
|
+
# For now, we'll use a simple example
|
|
257
|
+
request_count = request_data.get("request_count", 0)
|
|
258
|
+
return request_count > threshold
|
|
259
|
+
|
|
260
|
+
return ResponseRule(
|
|
261
|
+
name="rate_limited",
|
|
262
|
+
conditions=[
|
|
263
|
+
MatchCondition(
|
|
264
|
+
field="request_count",
|
|
265
|
+
match_type=MatchType.CUSTOM,
|
|
266
|
+
custom_function=rate_limit_check
|
|
267
|
+
)
|
|
268
|
+
],
|
|
269
|
+
response=response,
|
|
270
|
+
priority=25
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def create_error_rule(self, error_condition: str, response: Dict[str, Any]) -> ResponseRule:
|
|
274
|
+
"""Create a rule for error scenarios."""
|
|
275
|
+
error_conditions = {
|
|
276
|
+
"invalid_token": MatchCondition(
|
|
277
|
+
field="headers.authorization",
|
|
278
|
+
match_type=MatchType.REGEX,
|
|
279
|
+
value=r"invalid|expired"
|
|
280
|
+
),
|
|
281
|
+
"missing_required": MatchCondition(
|
|
282
|
+
field="body",
|
|
283
|
+
match_type=MatchType.CUSTOM,
|
|
284
|
+
custom_function=lambda body, _: not body or not isinstance(body, dict)
|
|
285
|
+
),
|
|
286
|
+
"malformed_request": MatchCondition(
|
|
287
|
+
field="headers.content-type",
|
|
288
|
+
match_type=MatchType.NOT_EQUALS,
|
|
289
|
+
value="application/json"
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return ResponseRule(
|
|
294
|
+
name=f"error_{error_condition}",
|
|
295
|
+
conditions=[error_conditions.get(error_condition)],
|
|
296
|
+
response=response,
|
|
297
|
+
priority=30
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def create_performance_rule(self, delay_range: Tuple[float, float], response: Dict[str, Any]) -> ResponseRule:
|
|
301
|
+
"""Create a rule for performance testing with delays."""
|
|
302
|
+
import random
|
|
303
|
+
|
|
304
|
+
response_with_delay = response.copy()
|
|
305
|
+
response_with_delay["delay"] = random.uniform(*delay_range)
|
|
306
|
+
|
|
307
|
+
return ResponseRule(
|
|
308
|
+
name="performance_test",
|
|
309
|
+
conditions=[
|
|
310
|
+
MatchCondition(
|
|
311
|
+
field="headers.x-performance-test",
|
|
312
|
+
match_type=MatchType.EXISTS
|
|
313
|
+
)
|
|
314
|
+
],
|
|
315
|
+
response=response_with_delay,
|
|
316
|
+
priority=5
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def export_rules(self, format: str = "json") -> str:
|
|
320
|
+
"""Export rules to various formats."""
|
|
321
|
+
if format == "json":
|
|
322
|
+
rules_data = {
|
|
323
|
+
"rules": [
|
|
324
|
+
{
|
|
325
|
+
"name": rule.name,
|
|
326
|
+
"priority": rule.priority,
|
|
327
|
+
"weight": rule.weight,
|
|
328
|
+
"metadata": rule.metadata,
|
|
329
|
+
"conditions": [
|
|
330
|
+
{
|
|
331
|
+
"field": cond.field,
|
|
332
|
+
"match_type": cond.match_type.value,
|
|
333
|
+
"value": cond.value,
|
|
334
|
+
"case_sensitive": cond.case_sensitive
|
|
335
|
+
}
|
|
336
|
+
for cond in rule.conditions
|
|
337
|
+
],
|
|
338
|
+
"response": rule.response
|
|
339
|
+
}
|
|
340
|
+
for rule in self.rules
|
|
341
|
+
],
|
|
342
|
+
"default_response": self.default_response
|
|
343
|
+
}
|
|
344
|
+
return json.dumps(rules_data, indent=2, default=str)
|
|
345
|
+
else:
|
|
346
|
+
raise ValueError(f"Unsupported export format: {format}")
|
|
347
|
+
|
|
348
|
+
def import_rules(self, data: str, format: str = "json"):
|
|
349
|
+
"""Import rules from various formats."""
|
|
350
|
+
if format == "json":
|
|
351
|
+
rules_data = json.loads(data)
|
|
352
|
+
|
|
353
|
+
# Clear existing rules
|
|
354
|
+
self.rules = []
|
|
355
|
+
|
|
356
|
+
# Import rules
|
|
357
|
+
for rule_data in rules_data.get("rules", []):
|
|
358
|
+
conditions = [
|
|
359
|
+
MatchCondition(
|
|
360
|
+
field=cond["field"],
|
|
361
|
+
match_type=MatchType(cond["match_type"]),
|
|
362
|
+
value=cond.get("value"),
|
|
363
|
+
case_sensitive=cond.get("case_sensitive", True)
|
|
364
|
+
)
|
|
365
|
+
for cond in rule_data["conditions"]
|
|
366
|
+
]
|
|
367
|
+
|
|
368
|
+
rule = ResponseRule(
|
|
369
|
+
name=rule_data["name"],
|
|
370
|
+
conditions=conditions,
|
|
371
|
+
response=rule_data["response"],
|
|
372
|
+
priority=rule_data.get("priority", 0),
|
|
373
|
+
weight=rule_data.get("weight", 1.0),
|
|
374
|
+
metadata=rule_data.get("metadata", {})
|
|
375
|
+
)
|
|
376
|
+
self.add_rule(rule)
|
|
377
|
+
|
|
378
|
+
# Set default response
|
|
379
|
+
if "default_response" in rules_data:
|
|
380
|
+
self.default_response = rules_data["default_response"]
|
|
381
|
+
else:
|
|
382
|
+
raise ValueError(f"Unsupported import format: {format}")
|
|
383
|
+
|
|
384
|
+
def get_matching_statistics(self) -> Dict[str, Any]:
|
|
385
|
+
"""Get statistics about rule matching."""
|
|
386
|
+
stats = {
|
|
387
|
+
"total_rules": len(self.rules),
|
|
388
|
+
"match_history": self.match_history[-100:], # Last 100 matches
|
|
389
|
+
"rule_usage": {},
|
|
390
|
+
"no_match_count": 0
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
# Count rule usage
|
|
394
|
+
for match in self.match_history:
|
|
395
|
+
if match["result"] == "match":
|
|
396
|
+
rule_name = match["rule_name"]
|
|
397
|
+
stats["rule_usage"][rule_name] = stats["rule_usage"].get(rule_name, 0) + 1
|
|
398
|
+
else:
|
|
399
|
+
stats["no_match_count"] += 1
|
|
400
|
+
|
|
401
|
+
return stats
|
|
402
|
+
|
|
403
|
+
def _log_match(self, result: str, rule: Optional[ResponseRule], request_data: Dict[str, Any]):
|
|
404
|
+
"""Log match results for analytics."""
|
|
405
|
+
self.match_history.append({
|
|
406
|
+
"result": result,
|
|
407
|
+
"rule_name": rule.name if rule else None,
|
|
408
|
+
"timestamp": datetime.now().isoformat(),
|
|
409
|
+
"request_path": request_data.get("path"),
|
|
410
|
+
"request_method": request_data.get("method")
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# Global smart matcher instance
|
|
415
|
+
smart_matcher = SmartResponseMatcher()
|
|
@@ -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,
|
|
3
|
+
Version: 0.3.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, and comprehensive testing framework.
|
|
5
5
|
Author-email: sherin joseph roy <sherin.joseph2217@gmail.com>
|
|
6
6
|
License: MIT License
|
|
7
7
|
|
|
@@ -2,18 +2,21 @@ 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=SwRZv2OL970YysfrT7YL8BqcbUNYxLn3s6J7EKAfO4Q,55523
|
|
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
|
+
api_mocker/enhanced_analytics.py,sha256=cSTLOft7oKZwDuy5ibUvfuSfRHmkAr9GQYU5DvtVOwI,23028
|
|
9
10
|
api_mocker/openapi.py,sha256=Pb1gKbBWosEV5i739rW0Nb3ArNq62lgMN0ecyvigNKY,7403
|
|
10
11
|
api_mocker/plugins.py,sha256=OK3OVHJszDky46JHntMVsZUH1ajBjBhAKq3TCDYuxWI,8178
|
|
11
12
|
api_mocker/recorder.py,sha256=7tiT2Krxy3nLDxFAE7rpZSimuD-rKeiwdU72cp0dg6E,9984
|
|
13
|
+
api_mocker/scenarios.py,sha256=wadcxu4Gp8w7i-UlPr6PlbcYnrSd1ehZA81e9dxGTgc,13392
|
|
12
14
|
api_mocker/server.py,sha256=xfczRj4xFXGVaGn2pVPgGvYyv3IHUlYTEz3Hop1KQu0,3812
|
|
15
|
+
api_mocker/smart_matching.py,sha256=DvTSKQwo4MhPEUHWdV3zF_H_dmp-l-47I59zz41tNe0,15067
|
|
13
16
|
api_mocker/testing.py,sha256=z4yJqS5MaSBOThpf3GtUY4dCzXTgopmnGnCuvnmKkF4,24949
|
|
14
|
-
api_mocker-0.
|
|
15
|
-
api_mocker-0.
|
|
16
|
-
api_mocker-0.
|
|
17
|
-
api_mocker-0.
|
|
18
|
-
api_mocker-0.
|
|
19
|
-
api_mocker-0.
|
|
17
|
+
api_mocker-0.3.0.dist-info/licenses/LICENSE,sha256=FzyeLcPe623lrwpFx3xQ3W0Hb_S2sbHqLzhSXaTmcGg,1074
|
|
18
|
+
api_mocker-0.3.0.dist-info/METADATA,sha256=nCBFAoH3iip-BahAo_5Cf5zHfXtf1gw4gKEkpzN_MCs,14512
|
|
19
|
+
api_mocker-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
+
api_mocker-0.3.0.dist-info/entry_points.txt,sha256=dj0UIkQ36Uq3oeSjGzmRRUQKFriq4WMCzg7TCor7wkM,51
|
|
21
|
+
api_mocker-0.3.0.dist-info/top_level.txt,sha256=ZcowEudKsJ6xbvOXIno2zZcPhjB-gGO1w7uzoUKRKDM,11
|
|
22
|
+
api_mocker-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|