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/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
- from collections.abc import Callable
9
- from typing import Any
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
- # FastAPI-like decorators for better developer experience (inherits from Router)
69
- # The decorators are already available from the Router base class
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, 'content'):
175
- content = response.content
176
- if isinstance(content, str):
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 Satya integration.
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 satya import Field, Model
8
+ from dhi import BaseModel, Field
9
9
 
10
10
 
11
- class TurboRequest(Model):
12
- """High-performance HTTP Request model powered by Satya."""
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 using Satya's fast parsing."""
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 Satya model."""
37
+ """Validate JSON body against a Dhi model."""
39
38
  if not self.body:
40
39
  return None
41
- return model_class.model_validate_json_bytes(self.body, streaming=True)
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(Model):
64
- """High-performance HTTP Response model powered by Satya."""
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, str):
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