turboapi 0.4.12__cp314-cp314t-macosx_10_12_x86_64.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.
turboapi/middleware.py ADDED
@@ -0,0 +1,342 @@
1
+ """
2
+ FastAPI-compatible Middleware system for TurboAPI.
3
+
4
+ Includes:
5
+ - CORS (Cross-Origin Resource Sharing)
6
+ - Trusted Host (HTTP Host Header attack prevention)
7
+ - GZip Compression
8
+ - HTTPS Redirect
9
+ - Session Management
10
+ - Custom Middleware Support
11
+ """
12
+
13
+ from typing import List, Optional, Callable, Awaitable, Pattern
14
+ import gzip
15
+ import re
16
+ import time
17
+ from .models import Request, Response
18
+
19
+
20
+ class Middleware:
21
+ """Base middleware class."""
22
+
23
+ def before_request(self, request: Request) -> None:
24
+ """Called before processing the request."""
25
+ pass
26
+
27
+ def after_request(self, request: Request, response: Response) -> Response:
28
+ """Called after processing the request."""
29
+ return response
30
+
31
+ def on_error(self, request: Request, error: Exception) -> Response:
32
+ """Called when an error occurs."""
33
+ return Response(
34
+ content={"error": "Internal Server Error"},
35
+ status_code=500
36
+ )
37
+
38
+
39
+ class CORSMiddleware(Middleware):
40
+ """
41
+ CORS (Cross-Origin Resource Sharing) middleware.
42
+
43
+ FastAPI-compatible implementation.
44
+
45
+ Usage:
46
+ app.add_middleware(
47
+ CORSMiddleware,
48
+ allow_origins=["http://localhost:8080"],
49
+ allow_credentials=True,
50
+ allow_methods=["*"],
51
+ allow_headers=["*"],
52
+ )
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ allow_origins: List[str] = None,
58
+ allow_methods: List[str] = None,
59
+ allow_headers: List[str] = None,
60
+ allow_credentials: bool = False,
61
+ allow_origin_regex: Optional[str] = None,
62
+ expose_headers: List[str] = None,
63
+ max_age: int = 600,
64
+ ):
65
+ self.allow_origins = allow_origins or ["*"]
66
+ self.allow_methods = allow_methods or ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "HEAD"]
67
+ self.allow_headers = allow_headers or ["*"]
68
+ self.allow_credentials = allow_credentials
69
+ self.allow_origin_regex = re.compile(allow_origin_regex) if allow_origin_regex else None
70
+ self.expose_headers = expose_headers or []
71
+ self.max_age = max_age
72
+
73
+ def before_request(self, request: Request) -> None:
74
+ """Handle preflight OPTIONS requests."""
75
+ if request.method == "OPTIONS":
76
+ # Preflight request
77
+ pass
78
+
79
+ def after_request(self, request: Request, response: Response) -> Response:
80
+ """Add CORS headers to response."""
81
+ origin = request.headers.get("origin", "")
82
+
83
+ # Check if origin is allowed
84
+ if self.allow_origin_regex and self.allow_origin_regex.match(origin):
85
+ response.set_header("Access-Control-Allow-Origin", origin)
86
+ elif "*" in self.allow_origins:
87
+ response.set_header("Access-Control-Allow-Origin", "*")
88
+ elif origin in self.allow_origins:
89
+ response.set_header("Access-Control-Allow-Origin", origin)
90
+
91
+ response.set_header("Access-Control-Allow-Methods", ", ".join(self.allow_methods))
92
+ response.set_header("Access-Control-Allow-Headers", ", ".join(self.allow_headers))
93
+
94
+ if self.expose_headers:
95
+ response.set_header("Access-Control-Expose-Headers", ", ".join(self.expose_headers))
96
+
97
+ if self.allow_credentials:
98
+ response.set_header("Access-Control-Allow-Credentials", "true")
99
+
100
+ response.set_header("Access-Control-Max-Age", str(self.max_age))
101
+
102
+ return response
103
+
104
+
105
+ class TrustedHostMiddleware(Middleware):
106
+ """
107
+ Trusted Host middleware - prevents HTTP Host Header attacks.
108
+
109
+ FastAPI-compatible implementation.
110
+
111
+ Usage:
112
+ app.add_middleware(
113
+ TrustedHostMiddleware,
114
+ allowed_hosts=["example.com", "*.example.com"]
115
+ )
116
+ """
117
+
118
+ def __init__(
119
+ self,
120
+ allowed_hosts: List[str] = None,
121
+ www_redirect: bool = True,
122
+ ):
123
+ if allowed_hosts is None:
124
+ allowed_hosts = ["*"]
125
+
126
+ self.allowed_hosts = allowed_hosts
127
+ self.www_redirect = www_redirect
128
+
129
+ # Compile regex patterns for wildcard hosts
130
+ self.allowed_host_patterns = []
131
+ for host in allowed_hosts:
132
+ if host == "*":
133
+ self.allowed_host_patterns.append(re.compile(".*"))
134
+ else:
135
+ # Convert wildcard to regex
136
+ pattern = host.replace(".", r"\.").replace("*", ".*")
137
+ self.allowed_host_patterns.append(re.compile(f"^{pattern}$"))
138
+
139
+ def before_request(self, request: Request) -> None:
140
+ """Validate Host header."""
141
+ host = request.headers.get("host", "").split(":")[0]
142
+
143
+ # Check if host is allowed
144
+ if not any(pattern.match(host) for pattern in self.allowed_host_patterns):
145
+ raise Exception(f"Invalid host header: {host}")
146
+
147
+
148
+ class GZipMiddleware(Middleware):
149
+ """
150
+ GZip compression middleware.
151
+
152
+ FastAPI-compatible implementation.
153
+
154
+ Usage:
155
+ app.add_middleware(GZipMiddleware, minimum_size=1000)
156
+ """
157
+
158
+ def __init__(
159
+ self,
160
+ minimum_size: int = 500,
161
+ compresslevel: int = 9,
162
+ ):
163
+ self.minimum_size = minimum_size
164
+ self.compresslevel = compresslevel
165
+
166
+ def after_request(self, request: Request, response: Response) -> Response:
167
+ """Compress response if client accepts gzip."""
168
+ accept_encoding = request.headers.get("accept-encoding", "")
169
+
170
+ if "gzip" not in accept_encoding.lower():
171
+ return response
172
+
173
+ # Check if response is large enough to compress
174
+ if hasattr(response, 'content'):
175
+ content = response.content
176
+ if isinstance(content, str):
177
+ content = content.encode('utf-8')
178
+
179
+ if len(content) < self.minimum_size:
180
+ return response
181
+
182
+ # Compress content
183
+ compressed = gzip.compress(content, compresslevel=self.compresslevel)
184
+ response.content = compressed
185
+ response.set_header("Content-Encoding", "gzip")
186
+ response.set_header("Content-Length", str(len(compressed)))
187
+ response.set_header("Vary", "Accept-Encoding")
188
+
189
+ return response
190
+
191
+
192
+ class HTTPSRedirectMiddleware(Middleware):
193
+ """
194
+ HTTPS redirect middleware - redirects HTTP to HTTPS.
195
+
196
+ FastAPI-compatible implementation.
197
+
198
+ Usage:
199
+ app.add_middleware(HTTPSRedirectMiddleware)
200
+ """
201
+
202
+ def before_request(self, request: Request) -> None:
203
+ """Redirect HTTP to HTTPS."""
204
+ # Check if request is HTTP
205
+ scheme = request.headers.get("x-forwarded-proto", "http")
206
+ if scheme == "http":
207
+ # Redirect to HTTPS
208
+ https_url = f"https://{request.headers.get('host', '')}{request.path}"
209
+ if request.query_string:
210
+ https_url += f"?{request.query_string}"
211
+
212
+ raise HTTPSRedirect(https_url)
213
+
214
+
215
+ class HTTPSRedirect(Exception):
216
+ """Exception to trigger HTTPS redirect."""
217
+ def __init__(self, url: str):
218
+ self.url = url
219
+
220
+
221
+ class SessionMiddleware(Middleware):
222
+ """
223
+ Session management middleware.
224
+
225
+ Usage:
226
+ app.add_middleware(
227
+ SessionMiddleware,
228
+ secret_key="your-secret-key-here",
229
+ session_cookie="session"
230
+ )
231
+ """
232
+
233
+ def __init__(
234
+ self,
235
+ secret_key: str,
236
+ session_cookie: str = "session",
237
+ max_age: int = 14 * 24 * 60 * 60, # 14 days
238
+ same_site: str = "lax",
239
+ https_only: bool = False,
240
+ ):
241
+ self.secret_key = secret_key
242
+ self.session_cookie = session_cookie
243
+ self.max_age = max_age
244
+ self.same_site = same_site
245
+ self.https_only = https_only
246
+
247
+ def before_request(self, request: Request) -> None:
248
+ """Load session from cookie."""
249
+ # TODO: Implement session loading
250
+ request.session = {}
251
+
252
+ def after_request(self, request: Request, response: Response) -> Response:
253
+ """Save session to cookie."""
254
+ # TODO: Implement session saving
255
+ return response
256
+
257
+
258
+ class RateLimitMiddleware(Middleware):
259
+ """
260
+ Rate limiting middleware.
261
+
262
+ Usage:
263
+ app.add_middleware(
264
+ RateLimitMiddleware,
265
+ requests_per_minute=60
266
+ )
267
+ """
268
+
269
+ def __init__(
270
+ self,
271
+ requests_per_minute: int = 60,
272
+ burst: int = 10,
273
+ ):
274
+ self.requests_per_minute = requests_per_minute
275
+ self.burst = burst
276
+ self.requests = {} # IP -> [(timestamp, count)]
277
+
278
+ def before_request(self, request: Request) -> None:
279
+ """Check rate limit."""
280
+ client_ip = request.headers.get("x-forwarded-for", "unknown").split(",")[0].strip()
281
+ now = time.time()
282
+
283
+ # Clean old requests
284
+ if client_ip in self.requests:
285
+ self.requests[client_ip] = [
286
+ (ts, count) for ts, count in self.requests[client_ip]
287
+ if now - ts < 60
288
+ ]
289
+
290
+ # Count requests in last minute
291
+ if client_ip not in self.requests:
292
+ self.requests[client_ip] = []
293
+
294
+ request_count = sum(count for _, count in self.requests[client_ip])
295
+
296
+ if request_count >= self.requests_per_minute:
297
+ raise Exception("Rate limit exceeded")
298
+
299
+ # Add this request
300
+ self.requests[client_ip].append((now, 1))
301
+
302
+
303
+ class LoggingMiddleware(Middleware):
304
+ """
305
+ Request logging middleware.
306
+
307
+ Usage:
308
+ app.add_middleware(LoggingMiddleware)
309
+ """
310
+
311
+ def before_request(self, request: Request) -> None:
312
+ """Log incoming request."""
313
+ request._start_time = time.time()
314
+ print(f"[REQUEST] {request.method} {request.path}")
315
+
316
+ def after_request(self, request: Request, response: Response) -> Response:
317
+ """Log response with timing."""
318
+ duration = time.time() - getattr(request, '_start_time', time.time())
319
+ print(f"[RESPONSE] {request.method} {request.path} -> {response.status_code} ({duration*1000:.2f}ms)")
320
+ return response
321
+
322
+
323
+ class CustomMiddleware(Middleware):
324
+ """
325
+ Custom middleware wrapper for function-based middleware.
326
+
327
+ Usage:
328
+ @app.middleware("http")
329
+ async def add_process_time_header(request, call_next):
330
+ start_time = time.time()
331
+ response = await call_next(request)
332
+ process_time = time.time() - start_time
333
+ response.headers["X-Process-Time"] = str(process_time)
334
+ return response
335
+ """
336
+
337
+ def __init__(self, func: Callable):
338
+ self.func = func
339
+
340
+ async def __call__(self, request: Request, call_next: Callable) -> Response:
341
+ """Execute custom middleware function."""
342
+ return await self.func(request, call_next)
turboapi/models.py ADDED
@@ -0,0 +1,148 @@
1
+ """
2
+ Request and Response models for TurboAPI with Satya integration.
3
+ """
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from satya import Field, Model
9
+
10
+
11
+ class TurboRequest(Model):
12
+ """High-performance HTTP Request model powered by Satya."""
13
+
14
+ method: str = Field(description="HTTP method")
15
+ path: str = Field(description="Request path")
16
+ query_string: str = Field(default="", description="Query string")
17
+ headers: dict[str, str] = Field(default={}, description="HTTP headers")
18
+ path_params: dict[str, str] = Field(default={}, description="Path parameters")
19
+ query_params: dict[str, str] = Field(default={}, description="Query parameters")
20
+ body: bytes | None = Field(default=None, description="Request body")
21
+
22
+ def get_header(self, name: str, default: str | None = None) -> str | None:
23
+ """Get header value (case-insensitive)."""
24
+ name_lower = name.lower()
25
+ for key, value in self.headers.items():
26
+ if key.lower() == name_lower:
27
+ return value
28
+ return default
29
+
30
+ def json(self) -> Any:
31
+ """Parse request body as JSON using Satya's fast parsing."""
32
+ if not self.body:
33
+ return None
34
+ # Use Satya's streaming JSON parsing for performance
35
+ return json.loads(self.body.decode('utf-8'))
36
+
37
+ def validate_json(self, model_class: type) -> Any:
38
+ """Validate JSON body against a Satya model."""
39
+ if not self.body:
40
+ return None
41
+ return model_class.model_validate_json_bytes(self.body, streaming=True)
42
+
43
+ def text(self) -> str:
44
+ """Get request body as text."""
45
+ return self.body.decode('utf-8') if self.body else ""
46
+
47
+ @property
48
+ def content_type(self) -> str | None:
49
+ """Get Content-Type header."""
50
+ return self.get_header('content-type')
51
+
52
+ @property
53
+ def content_length(self) -> int:
54
+ """Get Content-Length."""
55
+ length_str = self.get_header('content-length')
56
+ return int(length_str) if length_str else len(self.body or b"")
57
+
58
+
59
+ # Backward compatibility alias
60
+ Request = TurboRequest
61
+
62
+
63
+ class TurboResponse(Model):
64
+ """High-performance HTTP Response model powered by Satya."""
65
+
66
+ status_code: int = Field(ge=100, le=599, default=200, description="HTTP status code")
67
+ headers: dict[str, str] = Field(default={}, description="HTTP headers")
68
+ content: Any = Field(default="", description="Response content")
69
+
70
+ def __init__(self, **data):
71
+ # Handle content serialization before validation
72
+ if 'content' in data:
73
+ content = data['content']
74
+ if isinstance(content, dict):
75
+ # Serialize dict to JSON
76
+ data['content'] = json.dumps(content)
77
+ if 'headers' not in data:
78
+ data['headers'] = {}
79
+ data['headers']['content-type'] = 'application/json'
80
+ elif isinstance(content, (str, int, float, bool)):
81
+ # Keep as-is, will be converted to string
82
+ pass
83
+ elif isinstance(content, bytes):
84
+ # Convert bytes to string for storage
85
+ data['content'] = content.decode('utf-8')
86
+ else:
87
+ # Convert other types to string
88
+ data['content'] = str(content)
89
+
90
+ super().__init__(**data)
91
+
92
+ @property
93
+ def body(self) -> bytes:
94
+ """Get response body as bytes."""
95
+ if isinstance(self.content, str):
96
+ return self.content.encode('utf-8')
97
+ elif isinstance(self.content, bytes):
98
+ return self.content
99
+ else:
100
+ return str(self.content).encode('utf-8')
101
+
102
+ def set_header(self, name: str, value: str) -> None:
103
+ """Set a response header."""
104
+ self.headers[name] = value
105
+
106
+ def get_header(self, name: str, default: str | None = None) -> str | None:
107
+ """Get a response header."""
108
+ return self.headers.get(name, default)
109
+
110
+ @classmethod
111
+ def json(cls, data: Any, status_code: int = 200, headers: dict[str, str] | None = None):
112
+ """Create a JSON response with automatic serialization."""
113
+ response_headers = headers or {}
114
+ response_headers['content-type'] = 'application/json'
115
+
116
+ return cls(
117
+ content=data, # Will be auto-serialized to JSON
118
+ status_code=status_code,
119
+ headers=response_headers
120
+ )
121
+
122
+ @classmethod
123
+ def text(cls, content: str, status_code: int = 200, headers: dict[str, str] | None = None):
124
+ """Create a text response."""
125
+ response_headers = headers or {}
126
+ response_headers['content-type'] = 'text/plain; charset=utf-8'
127
+
128
+ return cls(
129
+ content=content,
130
+ status_code=status_code,
131
+ headers=response_headers
132
+ )
133
+
134
+ @classmethod
135
+ def html(cls, content: str, status_code: int = 200, headers: dict[str, str] | None = None):
136
+ """Create an HTML response."""
137
+ response_headers = headers or {}
138
+ response_headers['content-type'] = 'text/html; charset=utf-8'
139
+
140
+ return cls(
141
+ content=content,
142
+ status_code=status_code,
143
+ headers=response_headers
144
+ )
145
+
146
+
147
+ # Backward compatibility alias
148
+ Response = TurboResponse