turboapi 0.4.15__cp313-cp313-win_amd64.whl → 0.5.2__cp313-cp313-win_amd64.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/__init__.py +125 -8
- turboapi/background.py +51 -0
- turboapi/datastructures.py +262 -0
- turboapi/encoders.py +323 -0
- turboapi/exceptions.py +111 -0
- turboapi/main_app.py +59 -4
- turboapi/middleware.py +4 -6
- turboapi/models.py +15 -33
- turboapi/openapi.py +236 -0
- turboapi/request_handler.py +172 -32
- turboapi/responses.py +209 -0
- turboapi/routing.py +2 -1
- turboapi/rust_integration.py +133 -20
- turboapi/security.py +32 -3
- turboapi/staticfiles.py +91 -0
- turboapi/status.py +104 -0
- turboapi/templating.py +73 -0
- turboapi/testclient.py +321 -0
- turboapi/turbonet.cp313-win_amd64.pyd +0 -0
- turboapi/websockets.py +130 -0
- turboapi-0.5.2.dist-info/METADATA +534 -0
- turboapi-0.5.2.dist-info/RECORD +29 -0
- {turboapi-0.4.15.dist-info → turboapi-0.5.2.dist-info}/WHEEL +1 -1
- turboapi-0.5.2.dist-info/licenses/LICENSE +21 -0
- turboapi-0.4.15.dist-info/METADATA +0 -31
- turboapi-0.4.15.dist-info/RECORD +0 -17
turboapi/encoders.py
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JSON encoding utilities (FastAPI-compatible).
|
|
3
|
+
|
|
4
|
+
This module provides the jsonable_encoder function for converting
|
|
5
|
+
objects to JSON-serializable dictionaries.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import dataclasses
|
|
9
|
+
from collections import deque
|
|
10
|
+
from datetime import date, datetime, time, timedelta
|
|
11
|
+
from decimal import Decimal
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from pathlib import Path, PurePath
|
|
14
|
+
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Type, Union
|
|
15
|
+
from uuid import UUID
|
|
16
|
+
|
|
17
|
+
# Try to import dhi BaseModel
|
|
18
|
+
try:
|
|
19
|
+
from dhi import BaseModel
|
|
20
|
+
|
|
21
|
+
HAS_DHI = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
BaseModel = None
|
|
24
|
+
HAS_DHI = False
|
|
25
|
+
|
|
26
|
+
# Try to import Pydantic for compatibility
|
|
27
|
+
try:
|
|
28
|
+
import pydantic
|
|
29
|
+
|
|
30
|
+
HAS_PYDANTIC = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
HAS_PYDANTIC = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
ENCODERS_BY_TYPE: Dict[Type[Any], Callable[[Any], Any]] = {
|
|
36
|
+
bytes: lambda o: o.decode(),
|
|
37
|
+
date: lambda o: o.isoformat(),
|
|
38
|
+
datetime: lambda o: o.isoformat(),
|
|
39
|
+
time: lambda o: o.isoformat(),
|
|
40
|
+
timedelta: lambda o: o.total_seconds(),
|
|
41
|
+
Decimal: float,
|
|
42
|
+
Enum: lambda o: o.value,
|
|
43
|
+
frozenset: list,
|
|
44
|
+
deque: list,
|
|
45
|
+
set: list,
|
|
46
|
+
Path: str,
|
|
47
|
+
PurePath: str,
|
|
48
|
+
UUID: str,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def jsonable_encoder(
|
|
53
|
+
obj: Any,
|
|
54
|
+
include: Optional[Set[str]] = None,
|
|
55
|
+
exclude: Optional[Set[str]] = None,
|
|
56
|
+
by_alias: bool = True,
|
|
57
|
+
exclude_unset: bool = False,
|
|
58
|
+
exclude_defaults: bool = False,
|
|
59
|
+
exclude_none: bool = False,
|
|
60
|
+
custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None,
|
|
61
|
+
sqlalchemy_safe: bool = True,
|
|
62
|
+
) -> Any:
|
|
63
|
+
"""
|
|
64
|
+
Convert any object to a JSON-serializable value (FastAPI-compatible).
|
|
65
|
+
|
|
66
|
+
This function is useful for converting complex objects (like Pydantic/dhi models,
|
|
67
|
+
dataclasses, etc.) to dictionaries that can be serialized to JSON.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
obj: The object to convert
|
|
71
|
+
include: Set of field names to include (all if None)
|
|
72
|
+
exclude: Set of field names to exclude
|
|
73
|
+
by_alias: Use field aliases if available
|
|
74
|
+
exclude_unset: Exclude fields that were not explicitly set
|
|
75
|
+
exclude_defaults: Exclude fields with default values
|
|
76
|
+
exclude_none: Exclude fields with None values
|
|
77
|
+
custom_encoder: Custom encoders for specific types
|
|
78
|
+
sqlalchemy_safe: If True, avoid encoding SQLAlchemy lazy-loaded attributes
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
JSON-serializable value
|
|
82
|
+
|
|
83
|
+
Usage:
|
|
84
|
+
from turboapi.encoders import jsonable_encoder
|
|
85
|
+
from turboapi import BaseModel
|
|
86
|
+
|
|
87
|
+
class User(BaseModel):
|
|
88
|
+
name: str
|
|
89
|
+
created_at: datetime
|
|
90
|
+
|
|
91
|
+
user = User(name="Alice", created_at=datetime.now())
|
|
92
|
+
json_data = jsonable_encoder(user)
|
|
93
|
+
# {"name": "Alice", "created_at": "2024-01-01T12:00:00"}
|
|
94
|
+
"""
|
|
95
|
+
custom_encoder = custom_encoder or {}
|
|
96
|
+
exclude = exclude or set()
|
|
97
|
+
|
|
98
|
+
# Handle None
|
|
99
|
+
if obj is None:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
# Handle dhi BaseModel
|
|
103
|
+
if HAS_DHI and BaseModel is not None and isinstance(obj, BaseModel):
|
|
104
|
+
return _encode_model(
|
|
105
|
+
obj,
|
|
106
|
+
include=include,
|
|
107
|
+
exclude=exclude,
|
|
108
|
+
by_alias=by_alias,
|
|
109
|
+
exclude_unset=exclude_unset,
|
|
110
|
+
exclude_defaults=exclude_defaults,
|
|
111
|
+
exclude_none=exclude_none,
|
|
112
|
+
custom_encoder=custom_encoder,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Handle Pydantic models
|
|
116
|
+
if HAS_PYDANTIC:
|
|
117
|
+
if hasattr(pydantic, "BaseModel") and isinstance(obj, pydantic.BaseModel):
|
|
118
|
+
return _encode_pydantic(
|
|
119
|
+
obj,
|
|
120
|
+
include=include,
|
|
121
|
+
exclude=exclude,
|
|
122
|
+
by_alias=by_alias,
|
|
123
|
+
exclude_unset=exclude_unset,
|
|
124
|
+
exclude_defaults=exclude_defaults,
|
|
125
|
+
exclude_none=exclude_none,
|
|
126
|
+
custom_encoder=custom_encoder,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Handle dataclasses
|
|
130
|
+
if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
|
|
131
|
+
return _encode_dataclass(
|
|
132
|
+
obj,
|
|
133
|
+
include=include,
|
|
134
|
+
exclude=exclude,
|
|
135
|
+
exclude_none=exclude_none,
|
|
136
|
+
custom_encoder=custom_encoder,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Handle custom encoders
|
|
140
|
+
if type(obj) in custom_encoder:
|
|
141
|
+
return custom_encoder[type(obj)](obj)
|
|
142
|
+
|
|
143
|
+
# Handle built-in encoders
|
|
144
|
+
if type(obj) in ENCODERS_BY_TYPE:
|
|
145
|
+
return ENCODERS_BY_TYPE[type(obj)](obj)
|
|
146
|
+
|
|
147
|
+
# Handle dicts
|
|
148
|
+
if isinstance(obj, dict):
|
|
149
|
+
return {
|
|
150
|
+
jsonable_encoder(
|
|
151
|
+
key,
|
|
152
|
+
custom_encoder=custom_encoder,
|
|
153
|
+
): jsonable_encoder(
|
|
154
|
+
value,
|
|
155
|
+
include=include,
|
|
156
|
+
exclude=exclude,
|
|
157
|
+
by_alias=by_alias,
|
|
158
|
+
exclude_unset=exclude_unset,
|
|
159
|
+
exclude_defaults=exclude_defaults,
|
|
160
|
+
exclude_none=exclude_none,
|
|
161
|
+
custom_encoder=custom_encoder,
|
|
162
|
+
)
|
|
163
|
+
for key, value in obj.items()
|
|
164
|
+
if not (exclude_none and value is None)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Handle lists, tuples, sets, frozensets
|
|
168
|
+
if isinstance(obj, (list, tuple, set, frozenset, deque)):
|
|
169
|
+
return [
|
|
170
|
+
jsonable_encoder(
|
|
171
|
+
item,
|
|
172
|
+
include=include,
|
|
173
|
+
exclude=exclude,
|
|
174
|
+
by_alias=by_alias,
|
|
175
|
+
exclude_unset=exclude_unset,
|
|
176
|
+
exclude_defaults=exclude_defaults,
|
|
177
|
+
exclude_none=exclude_none,
|
|
178
|
+
custom_encoder=custom_encoder,
|
|
179
|
+
)
|
|
180
|
+
for item in obj
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
# Handle Enum
|
|
184
|
+
if isinstance(obj, Enum):
|
|
185
|
+
return obj.value
|
|
186
|
+
|
|
187
|
+
# Handle primitives
|
|
188
|
+
if isinstance(obj, (str, int, float, bool)):
|
|
189
|
+
return obj
|
|
190
|
+
|
|
191
|
+
# Handle objects with __dict__
|
|
192
|
+
if hasattr(obj, "__dict__"):
|
|
193
|
+
data = {}
|
|
194
|
+
for key, value in obj.__dict__.items():
|
|
195
|
+
if key.startswith("_"):
|
|
196
|
+
continue
|
|
197
|
+
if sqlalchemy_safe and key.startswith("_sa_"):
|
|
198
|
+
continue
|
|
199
|
+
if exclude and key in exclude:
|
|
200
|
+
continue
|
|
201
|
+
if include is not None and key not in include:
|
|
202
|
+
continue
|
|
203
|
+
if exclude_none and value is None:
|
|
204
|
+
continue
|
|
205
|
+
data[key] = jsonable_encoder(
|
|
206
|
+
value,
|
|
207
|
+
by_alias=by_alias,
|
|
208
|
+
exclude_unset=exclude_unset,
|
|
209
|
+
exclude_defaults=exclude_defaults,
|
|
210
|
+
exclude_none=exclude_none,
|
|
211
|
+
custom_encoder=custom_encoder,
|
|
212
|
+
)
|
|
213
|
+
return data
|
|
214
|
+
|
|
215
|
+
# Fallback: try to convert to string
|
|
216
|
+
try:
|
|
217
|
+
return str(obj)
|
|
218
|
+
except Exception:
|
|
219
|
+
return repr(obj)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _encode_model(
|
|
223
|
+
obj: Any,
|
|
224
|
+
include: Optional[Set[str]],
|
|
225
|
+
exclude: Set[str],
|
|
226
|
+
by_alias: bool,
|
|
227
|
+
exclude_unset: bool,
|
|
228
|
+
exclude_defaults: bool,
|
|
229
|
+
exclude_none: bool,
|
|
230
|
+
custom_encoder: Dict[Any, Callable[[Any], Any]],
|
|
231
|
+
) -> Dict[str, Any]:
|
|
232
|
+
"""Encode a dhi BaseModel to a dict."""
|
|
233
|
+
# Use model_dump if available
|
|
234
|
+
if hasattr(obj, "model_dump"):
|
|
235
|
+
# Try with full parameters (Pydantic v2 style)
|
|
236
|
+
try:
|
|
237
|
+
data = obj.model_dump(
|
|
238
|
+
include=include,
|
|
239
|
+
exclude=exclude,
|
|
240
|
+
by_alias=by_alias,
|
|
241
|
+
exclude_unset=exclude_unset,
|
|
242
|
+
exclude_defaults=exclude_defaults,
|
|
243
|
+
exclude_none=exclude_none,
|
|
244
|
+
)
|
|
245
|
+
except TypeError:
|
|
246
|
+
# Fallback for dhi or simpler model_dump implementations
|
|
247
|
+
data = obj.model_dump()
|
|
248
|
+
else:
|
|
249
|
+
# Fallback to dict() or __dict__
|
|
250
|
+
data = dict(obj) if hasattr(obj, "__iter__") else vars(obj).copy()
|
|
251
|
+
|
|
252
|
+
# Apply include/exclude filters manually if needed
|
|
253
|
+
if include is not None:
|
|
254
|
+
data = {k: v for k, v in data.items() if k in include}
|
|
255
|
+
|
|
256
|
+
# Recursively encode nested values
|
|
257
|
+
return {
|
|
258
|
+
key: jsonable_encoder(value, custom_encoder=custom_encoder)
|
|
259
|
+
for key, value in data.items()
|
|
260
|
+
if key not in exclude and not (exclude_none and value is None)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _encode_pydantic(
|
|
265
|
+
obj: Any,
|
|
266
|
+
include: Optional[Set[str]],
|
|
267
|
+
exclude: Set[str],
|
|
268
|
+
by_alias: bool,
|
|
269
|
+
exclude_unset: bool,
|
|
270
|
+
exclude_defaults: bool,
|
|
271
|
+
exclude_none: bool,
|
|
272
|
+
custom_encoder: Dict[Any, Callable[[Any], Any]],
|
|
273
|
+
) -> Dict[str, Any]:
|
|
274
|
+
"""Encode a Pydantic model to a dict."""
|
|
275
|
+
# Pydantic v2
|
|
276
|
+
if hasattr(obj, "model_dump"):
|
|
277
|
+
data = obj.model_dump(
|
|
278
|
+
include=include,
|
|
279
|
+
exclude=exclude,
|
|
280
|
+
by_alias=by_alias,
|
|
281
|
+
exclude_unset=exclude_unset,
|
|
282
|
+
exclude_defaults=exclude_defaults,
|
|
283
|
+
exclude_none=exclude_none,
|
|
284
|
+
)
|
|
285
|
+
# Pydantic v1
|
|
286
|
+
elif hasattr(obj, "dict"):
|
|
287
|
+
data = obj.dict(
|
|
288
|
+
include=include,
|
|
289
|
+
exclude=exclude,
|
|
290
|
+
by_alias=by_alias,
|
|
291
|
+
exclude_unset=exclude_unset,
|
|
292
|
+
exclude_defaults=exclude_defaults,
|
|
293
|
+
exclude_none=exclude_none,
|
|
294
|
+
)
|
|
295
|
+
else:
|
|
296
|
+
data = vars(obj).copy()
|
|
297
|
+
|
|
298
|
+
# Recursively encode nested values
|
|
299
|
+
return {
|
|
300
|
+
key: jsonable_encoder(value, custom_encoder=custom_encoder)
|
|
301
|
+
for key, value in data.items()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _encode_dataclass(
|
|
306
|
+
obj: Any,
|
|
307
|
+
include: Optional[Set[str]],
|
|
308
|
+
exclude: Set[str],
|
|
309
|
+
exclude_none: bool,
|
|
310
|
+
custom_encoder: Dict[Any, Callable[[Any], Any]],
|
|
311
|
+
) -> Dict[str, Any]:
|
|
312
|
+
"""Encode a dataclass to a dict."""
|
|
313
|
+
data = dataclasses.asdict(obj)
|
|
314
|
+
return {
|
|
315
|
+
key: jsonable_encoder(value, custom_encoder=custom_encoder)
|
|
316
|
+
for key, value in data.items()
|
|
317
|
+
if key not in exclude
|
|
318
|
+
and (include is None or key in include)
|
|
319
|
+
and not (exclude_none and value is None)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
__all__ = ["jsonable_encoder", "ENCODERS_BY_TYPE"]
|
turboapi/exceptions.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI-compatible exception classes for TurboAPI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional, Sequence, Union
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HTTPException(Exception):
|
|
9
|
+
"""
|
|
10
|
+
HTTP exception for API errors.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
raise HTTPException(status_code=404, detail="Item not found")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
status_code: int,
|
|
19
|
+
detail: Any = None,
|
|
20
|
+
headers: Optional[Dict[str, str]] = None,
|
|
21
|
+
):
|
|
22
|
+
self.status_code = status_code
|
|
23
|
+
self.detail = detail
|
|
24
|
+
self.headers = headers
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RequestValidationError(Exception):
|
|
28
|
+
"""
|
|
29
|
+
Request validation error (FastAPI-compatible).
|
|
30
|
+
|
|
31
|
+
Raised when request data fails validation.
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
from turboapi import RequestValidationError
|
|
35
|
+
|
|
36
|
+
@app.exception_handler(RequestValidationError)
|
|
37
|
+
async def validation_exception_handler(request, exc):
|
|
38
|
+
return JSONResponse(
|
|
39
|
+
status_code=422,
|
|
40
|
+
content={"detail": exc.errors()}
|
|
41
|
+
)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
errors: Sequence[Any],
|
|
47
|
+
*,
|
|
48
|
+
body: Any = None,
|
|
49
|
+
):
|
|
50
|
+
self._errors = errors
|
|
51
|
+
self.body = body
|
|
52
|
+
|
|
53
|
+
def errors(self) -> List[Dict[str, Any]]:
|
|
54
|
+
"""Return list of validation errors."""
|
|
55
|
+
return list(self._errors)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class WebSocketException(Exception):
|
|
59
|
+
"""
|
|
60
|
+
WebSocket exception (FastAPI-compatible).
|
|
61
|
+
|
|
62
|
+
Raised when a WebSocket error occurs.
|
|
63
|
+
|
|
64
|
+
Usage:
|
|
65
|
+
raise WebSocketException(code=1008, reason="Policy violation")
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
code: int = 1000,
|
|
71
|
+
reason: Optional[str] = None,
|
|
72
|
+
):
|
|
73
|
+
self.code = code
|
|
74
|
+
self.reason = reason
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ValidationError(Exception):
|
|
78
|
+
"""
|
|
79
|
+
Generic validation error.
|
|
80
|
+
|
|
81
|
+
Provides a base for validation-related exceptions.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
errors: List[Dict[str, Any]],
|
|
87
|
+
):
|
|
88
|
+
self._errors = errors
|
|
89
|
+
|
|
90
|
+
def errors(self) -> List[Dict[str, Any]]:
|
|
91
|
+
"""Return list of validation errors."""
|
|
92
|
+
return self._errors
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class StarletteHTTPException(HTTPException):
|
|
96
|
+
"""
|
|
97
|
+
Starlette-compatible HTTP exception alias.
|
|
98
|
+
|
|
99
|
+
Some applications expect this for compatibility.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = [
|
|
106
|
+
"HTTPException",
|
|
107
|
+
"RequestValidationError",
|
|
108
|
+
"WebSocketException",
|
|
109
|
+
"ValidationError",
|
|
110
|
+
"StarletteHTTPException",
|
|
111
|
+
]
|
turboapi/main_app.py
CHANGED
|
@@ -4,9 +4,11 @@ FastAPI-compatible application with revolutionary performance
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import asyncio
|
|
7
|
+
import contextlib
|
|
7
8
|
import inspect
|
|
8
|
-
|
|
9
|
-
from
|
|
9
|
+
import json
|
|
10
|
+
from collections.abc import AsyncGenerator, Callable
|
|
11
|
+
from typing import Any, Optional
|
|
10
12
|
|
|
11
13
|
from .routing import Router
|
|
12
14
|
from .version_check import CHECK_MARK, ROCKET
|
|
@@ -20,6 +22,10 @@ class TurboAPI(Router):
|
|
|
20
22
|
title: str = "TurboAPI",
|
|
21
23
|
version: str = "0.1.0",
|
|
22
24
|
description: str = "A revolutionary Python web framework",
|
|
25
|
+
docs_url: Optional[str] = "/docs",
|
|
26
|
+
redoc_url: Optional[str] = "/redoc",
|
|
27
|
+
openapi_url: Optional[str] = "/openapi.json",
|
|
28
|
+
lifespan: Optional[Callable] = None,
|
|
23
29
|
**kwargs
|
|
24
30
|
):
|
|
25
31
|
super().__init__()
|
|
@@ -29,6 +35,14 @@ class TurboAPI(Router):
|
|
|
29
35
|
self.middleware_stack = []
|
|
30
36
|
self.startup_handlers = []
|
|
31
37
|
self.shutdown_handlers = []
|
|
38
|
+
self.docs_url = docs_url
|
|
39
|
+
self.redoc_url = redoc_url
|
|
40
|
+
self.openapi_url = openapi_url
|
|
41
|
+
self._lifespan = lifespan
|
|
42
|
+
self._mounts: dict[str, Any] = {}
|
|
43
|
+
self._websocket_routes: dict[str, Callable] = {}
|
|
44
|
+
self._exception_handlers: dict[type, Callable] = {}
|
|
45
|
+
self._openapi_schema: Optional[dict] = None
|
|
32
46
|
|
|
33
47
|
print(f"{ROCKET} TurboAPI application created: {title} v{version}")
|
|
34
48
|
|
|
@@ -65,8 +79,49 @@ class TurboAPI(Router):
|
|
|
65
79
|
super().include_router(router, prefix, tags)
|
|
66
80
|
print(f"[ROUTER] Included router with prefix: {prefix}")
|
|
67
81
|
|
|
68
|
-
|
|
69
|
-
|
|
82
|
+
def mount(self, path: str, app: Any, name: Optional[str] = None) -> None:
|
|
83
|
+
"""Mount a sub-application or static files at a path.
|
|
84
|
+
|
|
85
|
+
Usage:
|
|
86
|
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
87
|
+
"""
|
|
88
|
+
self._mounts[path] = {"app": app, "name": name}
|
|
89
|
+
print(f"[MOUNT] Mounted {name or 'app'} at {path}")
|
|
90
|
+
|
|
91
|
+
def websocket(self, path: str):
|
|
92
|
+
"""Register a WebSocket endpoint.
|
|
93
|
+
|
|
94
|
+
Usage:
|
|
95
|
+
@app.websocket("/ws")
|
|
96
|
+
async def websocket_endpoint(websocket: WebSocket):
|
|
97
|
+
await websocket.accept()
|
|
98
|
+
data = await websocket.receive_text()
|
|
99
|
+
await websocket.send_text(f"Echo: {data}")
|
|
100
|
+
"""
|
|
101
|
+
def decorator(func: Callable):
|
|
102
|
+
self._websocket_routes[path] = func
|
|
103
|
+
return func
|
|
104
|
+
return decorator
|
|
105
|
+
|
|
106
|
+
def exception_handler(self, exc_class: type):
|
|
107
|
+
"""Register a custom exception handler.
|
|
108
|
+
|
|
109
|
+
Usage:
|
|
110
|
+
@app.exception_handler(ValueError)
|
|
111
|
+
async def value_error_handler(request, exc):
|
|
112
|
+
return JSONResponse(status_code=400, content={"detail": str(exc)})
|
|
113
|
+
"""
|
|
114
|
+
def decorator(func: Callable):
|
|
115
|
+
self._exception_handlers[exc_class] = func
|
|
116
|
+
return func
|
|
117
|
+
return decorator
|
|
118
|
+
|
|
119
|
+
def openapi(self) -> dict:
|
|
120
|
+
"""Get the OpenAPI schema for this application."""
|
|
121
|
+
if self._openapi_schema is None:
|
|
122
|
+
from .openapi import generate_openapi_schema
|
|
123
|
+
self._openapi_schema = generate_openapi_schema(self)
|
|
124
|
+
return self._openapi_schema
|
|
70
125
|
|
|
71
126
|
async def _run_startup_handlers(self):
|
|
72
127
|
"""Run all startup event handlers."""
|
turboapi/middleware.py
CHANGED
|
@@ -171,14 +171,12 @@ class GZipMiddleware(Middleware):
|
|
|
171
171
|
return response
|
|
172
172
|
|
|
173
173
|
# Check if response is large enough to compress
|
|
174
|
-
if hasattr(response, '
|
|
175
|
-
content = response.
|
|
176
|
-
|
|
177
|
-
content = content.encode('utf-8')
|
|
178
|
-
|
|
174
|
+
if hasattr(response, 'body'):
|
|
175
|
+
content = response.body
|
|
176
|
+
|
|
179
177
|
if len(content) < self.minimum_size:
|
|
180
178
|
return response
|
|
181
|
-
|
|
179
|
+
|
|
182
180
|
# Compress content
|
|
183
181
|
compressed = gzip.compress(content, compresslevel=self.compresslevel)
|
|
184
182
|
response.content = compressed
|
turboapi/models.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Request and Response models for TurboAPI with
|
|
2
|
+
Request and Response models for TurboAPI with Dhi integration.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
-
from
|
|
8
|
+
from dhi import BaseModel, Field
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
class TurboRequest(
|
|
12
|
-
"""High-performance HTTP Request model powered by
|
|
11
|
+
class TurboRequest(BaseModel):
|
|
12
|
+
"""High-performance HTTP Request model powered by Dhi."""
|
|
13
13
|
|
|
14
14
|
method: str = Field(description="HTTP method")
|
|
15
15
|
path: str = Field(description="Request path")
|
|
@@ -28,17 +28,17 @@ class TurboRequest(Model):
|
|
|
28
28
|
return default
|
|
29
29
|
|
|
30
30
|
def json(self) -> Any:
|
|
31
|
-
"""Parse request body as JSON
|
|
31
|
+
"""Parse request body as JSON."""
|
|
32
32
|
if not self.body:
|
|
33
33
|
return None
|
|
34
|
-
# Use Satya's streaming JSON parsing for performance
|
|
35
34
|
return json.loads(self.body.decode('utf-8'))
|
|
36
35
|
|
|
37
36
|
def validate_json(self, model_class: type) -> Any:
|
|
38
|
-
"""Validate JSON body against a
|
|
37
|
+
"""Validate JSON body against a Dhi model."""
|
|
39
38
|
if not self.body:
|
|
40
39
|
return None
|
|
41
|
-
|
|
40
|
+
data = json.loads(self.body.decode('utf-8'))
|
|
41
|
+
return model_class.model_validate(data)
|
|
42
42
|
|
|
43
43
|
def text(self) -> str:
|
|
44
44
|
"""Get request body as text."""
|
|
@@ -60,39 +60,21 @@ class TurboRequest(Model):
|
|
|
60
60
|
Request = TurboRequest
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
class TurboResponse(
|
|
64
|
-
"""High-performance HTTP Response model powered by
|
|
63
|
+
class TurboResponse(BaseModel):
|
|
64
|
+
"""High-performance HTTP Response model powered by Dhi."""
|
|
65
65
|
|
|
66
66
|
status_code: int = Field(ge=100, le=599, default=200, description="HTTP status code")
|
|
67
67
|
headers: dict[str, str] = Field(default={}, description="HTTP headers")
|
|
68
68
|
content: Any = Field(default="", description="Response content")
|
|
69
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
70
|
@property
|
|
93
71
|
def body(self) -> bytes:
|
|
94
72
|
"""Get response body as bytes."""
|
|
95
|
-
if isinstance(self.content,
|
|
73
|
+
if isinstance(self.content, dict):
|
|
74
|
+
return json.dumps(self.content).encode('utf-8')
|
|
75
|
+
elif isinstance(self.content, (list, tuple)):
|
|
76
|
+
return json.dumps(self.content).encode('utf-8')
|
|
77
|
+
elif isinstance(self.content, str):
|
|
96
78
|
return self.content.encode('utf-8')
|
|
97
79
|
elif isinstance(self.content, bytes):
|
|
98
80
|
return self.content
|