api-mocker 0.1.2__py3-none-any.whl → 0.1.3__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 CHANGED
@@ -2,6 +2,6 @@
2
2
  api-mocker: The industry-standard, production-ready, free API mocking and development acceleration tool.
3
3
  """
4
4
 
5
- __version__ = "0.1.0"
5
+ __version__ = "0.1.3"
6
6
 
7
7
  from .server import MockServer
api_mocker/advanced.py ADDED
@@ -0,0 +1,391 @@
1
+ """
2
+ Advanced features for api-mocker including rate limiting, authentication, caching, and more.
3
+ """
4
+
5
+ import time
6
+ import hashlib
7
+ import asyncio
8
+ from typing import Dict, List, Optional, Any, Callable
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime, timedelta
11
+ from fastapi import HTTPException, Depends, Request
12
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
13
+ import json
14
+ import logging
15
+
16
+ # Optional imports for advanced features
17
+ try:
18
+ import jwt
19
+ except ImportError:
20
+ jwt = None
21
+
22
+ try:
23
+ import redis
24
+ except ImportError:
25
+ redis = None
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ @dataclass
30
+ class RateLimitConfig:
31
+ """Rate limiting configuration."""
32
+ requests_per_minute: int = 60
33
+ requests_per_hour: int = 1000
34
+ burst_size: int = 10
35
+ window_size: int = 60 # seconds
36
+
37
+ @dataclass
38
+ class CacheConfig:
39
+ """Caching configuration."""
40
+ enabled: bool = True
41
+ ttl_seconds: int = 300
42
+ max_size: int = 1000
43
+ strategy: str = "lru" # lru, fifo, random
44
+
45
+ @dataclass
46
+ class AuthConfig:
47
+ """Authentication configuration."""
48
+ enabled: bool = False
49
+ secret_key: str = "your-secret-key"
50
+ algorithm: str = "HS256"
51
+ token_expiry_hours: int = 24
52
+ require_auth: List[str] = field(default_factory=list) # List of paths that require auth
53
+
54
+ class RateLimiter:
55
+ """Rate limiting implementation using sliding window."""
56
+
57
+ def __init__(self, config: RateLimitConfig):
58
+ self.config = config
59
+ self.requests = {} # client_id -> list of timestamps
60
+
61
+ def is_allowed(self, client_id: str) -> bool:
62
+ """Check if request is allowed based on rate limits."""
63
+ now = time.time()
64
+
65
+ if client_id not in self.requests:
66
+ self.requests[client_id] = []
67
+
68
+ # Remove old requests outside the window
69
+ window_start = now - self.config.window_size
70
+ self.requests[client_id] = [
71
+ req_time for req_time in self.requests[client_id]
72
+ if req_time > window_start
73
+ ]
74
+
75
+ # Check if we're within limits
76
+ if len(self.requests[client_id]) >= self.config.requests_per_minute:
77
+ return False
78
+
79
+ # Add current request
80
+ self.requests[client_id].append(now)
81
+ return True
82
+
83
+ def get_remaining_requests(self, client_id: str) -> int:
84
+ """Get remaining requests for a client."""
85
+ now = time.time()
86
+ window_start = now - self.config.window_size
87
+
88
+ if client_id not in self.requests:
89
+ return self.config.requests_per_minute
90
+
91
+ recent_requests = len([
92
+ req_time for req_time in self.requests[client_id]
93
+ if req_time > window_start
94
+ ])
95
+
96
+ return max(0, self.config.requests_per_minute - recent_requests)
97
+
98
+ class CacheManager:
99
+ """Simple in-memory cache with TTL support."""
100
+
101
+ def __init__(self, config: CacheConfig):
102
+ self.config = config
103
+ self.cache = {}
104
+ self.access_times = {}
105
+ self.max_size = config.max_size
106
+
107
+ def get(self, key: str) -> Optional[Any]:
108
+ """Get value from cache."""
109
+ if not self.config.enabled:
110
+ return None
111
+
112
+ if key not in self.cache:
113
+ return None
114
+
115
+ # Check TTL
116
+ if time.time() - self.access_times[key] > self.config.ttl_seconds:
117
+ self.delete(key)
118
+ return None
119
+
120
+ # Update access time
121
+ self.access_times[key] = time.time()
122
+ return self.cache[key]
123
+
124
+ def set(self, key: str, value: Any) -> None:
125
+ """Set value in cache."""
126
+ if not self.config.enabled:
127
+ return
128
+
129
+ # Evict if cache is full
130
+ if len(self.cache) >= self.max_size:
131
+ self._evict_oldest()
132
+
133
+ self.cache[key] = value
134
+ self.access_times[key] = time.time()
135
+
136
+ def delete(self, key: str) -> None:
137
+ """Delete key from cache."""
138
+ self.cache.pop(key, None)
139
+ self.access_times.pop(key, None)
140
+
141
+ def clear(self) -> None:
142
+ """Clear all cache."""
143
+ self.cache.clear()
144
+ self.access_times.clear()
145
+
146
+ def _evict_oldest(self) -> None:
147
+ """Evict oldest entry based on strategy."""
148
+ if not self.access_times:
149
+ return
150
+
151
+ if self.config.strategy == "lru":
152
+ oldest_key = min(self.access_times.keys(), key=lambda k: self.access_times[k])
153
+ elif self.config.strategy == "fifo":
154
+ oldest_key = min(self.access_times.keys(), key=lambda k: self.access_times[k])
155
+ else: # random
156
+ import random
157
+ oldest_key = random.choice(list(self.access_times.keys()))
158
+
159
+ self.delete(oldest_key)
160
+
161
+ class AuthManager:
162
+ """JWT-based authentication manager."""
163
+
164
+ def __init__(self, config: AuthConfig):
165
+ self.config = config
166
+ self.security = HTTPBearer()
167
+
168
+ def create_token(self, user_id: str, roles: Optional[List[str]] = None) -> str:
169
+ """Create JWT token for user."""
170
+ if jwt is None:
171
+ raise HTTPException(status_code=500, detail="JWT library not available")
172
+
173
+ payload = {
174
+ "user_id": user_id,
175
+ "roles": roles or [],
176
+ "exp": datetime.utcnow() + timedelta(hours=self.config.token_expiry_hours),
177
+ "iat": datetime.utcnow()
178
+ }
179
+ return jwt.encode(payload, self.config.secret_key, algorithm=self.config.algorithm)
180
+
181
+ def verify_token(self, token: str) -> Dict[str, Any]:
182
+ """Verify JWT token and return payload."""
183
+ if jwt is None:
184
+ raise HTTPException(status_code=500, detail="JWT library not available")
185
+
186
+ try:
187
+ payload = jwt.decode(token, self.config.secret_key, algorithms=[self.config.algorithm])
188
+ return payload
189
+ except jwt.ExpiredSignatureError:
190
+ raise HTTPException(status_code=401, detail="Token has expired")
191
+ except jwt.InvalidTokenError:
192
+ raise HTTPException(status_code=401, detail="Invalid token")
193
+
194
+ async def get_current_user(self, credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer())):
195
+ """Dependency to get current user from token."""
196
+ if not self.config.enabled:
197
+ return None
198
+
199
+ token = credentials.credentials
200
+ payload = self.verify_token(token)
201
+ return payload
202
+
203
+ class AdvancedFeatures:
204
+ """Main class for advanced features."""
205
+
206
+ def __init__(self,
207
+ rate_limit_config: Optional[RateLimitConfig] = None,
208
+ cache_config: Optional[CacheConfig] = None,
209
+ auth_config: Optional[AuthConfig] = None):
210
+
211
+ self.rate_limiter = RateLimiter(rate_limit_config or RateLimitConfig())
212
+ self.cache_manager = CacheManager(cache_config or CacheConfig())
213
+ self.auth_manager = AuthManager(auth_config or AuthConfig())
214
+
215
+ def get_client_id(self, request: Request) -> str:
216
+ """Get client identifier for rate limiting."""
217
+ # Try to get from X-Forwarded-For header first
218
+ forwarded_for = request.headers.get("X-Forwarded-For")
219
+ if forwarded_for:
220
+ return forwarded_for.split(",")[0].strip()
221
+
222
+ # Fall back to client host
223
+ return request.client.host if request.client else "unknown"
224
+
225
+ def create_cache_key(self, method: str, path: str, query_params: str = "") -> str:
226
+ """Create cache key for request."""
227
+ key_data = f"{method}:{path}:{query_params}"
228
+ return hashlib.md5(key_data.encode()).hexdigest()
229
+
230
+ async def rate_limit_middleware(self, request: Request, call_next):
231
+ """Rate limiting middleware."""
232
+ client_id = self.get_client_id(request)
233
+
234
+ if not self.rate_limiter.is_allowed(client_id):
235
+ remaining = self.rate_limiter.get_remaining_requests(client_id)
236
+ raise HTTPException(
237
+ status_code=429,
238
+ detail=f"Rate limit exceeded. Try again in {60 - int(time.time() % 60)} seconds.",
239
+ headers={"X-RateLimit-Remaining": str(remaining)}
240
+ )
241
+
242
+ response = await call_next(request)
243
+
244
+ # Add rate limit headers
245
+ remaining = self.rate_limiter.get_remaining_requests(client_id)
246
+ response.headers["X-RateLimit-Remaining"] = str(remaining)
247
+ response.headers["X-RateLimit-Limit"] = str(self.rate_limiter.config.requests_per_minute)
248
+
249
+ return response
250
+
251
+ async def cache_middleware(self, request: Request, call_next):
252
+ """Caching middleware."""
253
+ if request.method != "GET":
254
+ return await call_next(request)
255
+
256
+ cache_key = self.create_cache_key(
257
+ request.method,
258
+ request.url.path,
259
+ str(request.query_params)
260
+ )
261
+
262
+ # Try to get from cache
263
+ cached_response = self.cache_manager.get(cache_key)
264
+ if cached_response:
265
+ return cached_response
266
+
267
+ # Process request
268
+ response = await call_next(request)
269
+
270
+ # Cache successful responses
271
+ if response.status_code == 200:
272
+ self.cache_manager.set(cache_key, response)
273
+
274
+ return response
275
+
276
+ async def auth_middleware(self, request: Request, call_next):
277
+ """Authentication middleware."""
278
+ if not self.auth_manager.config.enabled:
279
+ return await call_next(request)
280
+
281
+ # Check if path requires authentication
282
+ if self.auth_manager.config.require_auth:
283
+ path_requires_auth = any(
284
+ request.url.path.startswith(auth_path)
285
+ for auth_path in self.auth_manager.config.require_auth
286
+ )
287
+ if path_requires_auth:
288
+ # Verify token
289
+ auth_header = request.headers.get("Authorization")
290
+ if not auth_header or not auth_header.startswith("Bearer "):
291
+ raise HTTPException(status_code=401, detail="Authentication required")
292
+
293
+ token = auth_header.split(" ")[1]
294
+ try:
295
+ payload = self.auth_manager.verify_token(token)
296
+ request.state.user = payload
297
+ except HTTPException:
298
+ raise HTTPException(status_code=401, detail="Invalid token")
299
+
300
+ return await call_next(request)
301
+
302
+ class MetricsCollector:
303
+ """Advanced metrics collection."""
304
+
305
+ def __init__(self):
306
+ self.metrics = {
307
+ "rate_limit_hits": 0,
308
+ "cache_hits": 0,
309
+ "cache_misses": 0,
310
+ "auth_failures": 0,
311
+ "slow_queries": 0,
312
+ "error_counts": {}
313
+ }
314
+
315
+ def increment(self, metric: str, value: int = 1):
316
+ """Increment a metric."""
317
+ if metric in self.metrics:
318
+ if isinstance(self.metrics[metric], dict):
319
+ self.metrics[metric][str(value)] = self.metrics[metric].get(str(value), 0) + 1
320
+ else:
321
+ self.metrics[metric] += value
322
+
323
+ def get_metrics(self) -> Dict[str, Any]:
324
+ """Get current metrics."""
325
+ return self.metrics.copy()
326
+
327
+ def reset_metrics(self):
328
+ """Reset all metrics."""
329
+ self.metrics = {
330
+ "rate_limit_hits": 0,
331
+ "cache_hits": 0,
332
+ "cache_misses": 0,
333
+ "auth_failures": 0,
334
+ "slow_queries": 0,
335
+ "error_counts": {}
336
+ }
337
+
338
+ class HealthChecker:
339
+ """Health check and monitoring."""
340
+
341
+ def __init__(self):
342
+ self.checks = {}
343
+
344
+ def add_check(self, name: str, check_func: Callable[[], bool]):
345
+ """Add a health check."""
346
+ self.checks[name] = check_func
347
+
348
+ def run_checks(self) -> Dict[str, bool]:
349
+ """Run all health checks."""
350
+ results = {}
351
+ for name, check_func in self.checks.items():
352
+ try:
353
+ results[name] = check_func()
354
+ except Exception as e:
355
+ logger.error(f"Health check {name} failed: {e}")
356
+ results[name] = False
357
+ return results
358
+
359
+ def get_health_status(self) -> Dict[str, Any]:
360
+ """Get overall health status."""
361
+ results = self.run_checks()
362
+ overall_healthy = all(results.values())
363
+
364
+ return {
365
+ "status": "healthy" if overall_healthy else "unhealthy",
366
+ "checks": results,
367
+ "timestamp": datetime.utcnow().isoformat()
368
+ }
369
+
370
+ # Predefined health checks
371
+ def check_database_connection():
372
+ """Check database connectivity."""
373
+ try:
374
+ import sqlite3
375
+ conn = sqlite3.connect(":memory:")
376
+ conn.close()
377
+ return True
378
+ except:
379
+ return False
380
+
381
+ def check_memory_usage():
382
+ """Check if memory usage is acceptable."""
383
+ import psutil
384
+ memory_percent = psutil.virtual_memory().percent
385
+ return memory_percent < 90 # Consider healthy if < 90%
386
+
387
+ def check_disk_space():
388
+ """Check if disk space is sufficient."""
389
+ import psutil
390
+ disk_percent = psutil.disk_usage('/').percent
391
+ return disk_percent < 95 # Consider healthy if < 95%