api-mocker 0.1.3__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.
@@ -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()