tachyon-api 0.5.5__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.
Potentially problematic release.
This version of tachyon-api might be problematic. Click here for more details.
- tachyon_api/__init__.py +31 -0
- tachyon_api/app.py +607 -0
- tachyon_api/di.py +59 -0
- tachyon_api/middlewares/__init__.py +5 -0
- tachyon_api/middlewares/core.py +40 -0
- tachyon_api/middlewares/cors.py +142 -0
- tachyon_api/middlewares/logger.py +119 -0
- tachyon_api/models.py +73 -0
- tachyon_api/openapi.py +362 -0
- tachyon_api/params.py +90 -0
- tachyon_api/responses.py +49 -0
- tachyon_api/router.py +129 -0
- tachyon_api-0.5.5.dist-info/LICENSE +17 -0
- tachyon_api-0.5.5.dist-info/METADATA +239 -0
- tachyon_api-0.5.5.dist-info/RECORD +16 -0
- tachyon_api-0.5.5.dist-info/WHEEL +4 -0
tachyon_api/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tachyon Web Framework
|
|
3
|
+
|
|
4
|
+
A lightweight, FastAPI-inspired web framework with built-in dependency injection,
|
|
5
|
+
automatic parameter validation, and high-performance JSON serialization.
|
|
6
|
+
|
|
7
|
+
Copyright (C) 2025 Juan Manuel Panozzo Zenere
|
|
8
|
+
|
|
9
|
+
This program is free software: you can redistribute it and/or modify
|
|
10
|
+
it under the terms of the GNU General Public License as published by
|
|
11
|
+
the Free Software Foundation, either version 3 of the License.
|
|
12
|
+
|
|
13
|
+
For more information, see the documentation and examples.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .app import Tachyon
|
|
17
|
+
from .models import Struct
|
|
18
|
+
from .params import Query, Body, Path
|
|
19
|
+
from .di import injectable, Depends
|
|
20
|
+
from .router import Router
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Tachyon",
|
|
24
|
+
"Struct",
|
|
25
|
+
"Query",
|
|
26
|
+
"Body",
|
|
27
|
+
"Path",
|
|
28
|
+
"injectable",
|
|
29
|
+
"Depends",
|
|
30
|
+
"Router",
|
|
31
|
+
]
|
tachyon_api/app.py
ADDED
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tachyon Web Framework - Main Application Module
|
|
3
|
+
|
|
4
|
+
This module contains the core Tachyon class that provides a lightweight,
|
|
5
|
+
FastAPI-inspired web framework with built-in dependency injection,
|
|
6
|
+
parameter validation, and automatic type conversion.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import inspect
|
|
11
|
+
import msgspec
|
|
12
|
+
from functools import partial
|
|
13
|
+
from typing import Any, Dict, Type, Union, Callable
|
|
14
|
+
|
|
15
|
+
from starlette.applications import Starlette
|
|
16
|
+
from starlette.responses import HTMLResponse, JSONResponse, Response
|
|
17
|
+
from starlette.routing import Route
|
|
18
|
+
|
|
19
|
+
from .di import Depends, _registry
|
|
20
|
+
from .models import Struct
|
|
21
|
+
from .openapi import (
|
|
22
|
+
OpenAPIGenerator,
|
|
23
|
+
OpenAPIConfig,
|
|
24
|
+
create_openapi_config,
|
|
25
|
+
_generate_schema_for_struct,
|
|
26
|
+
)
|
|
27
|
+
from .params import Body, Query, Path
|
|
28
|
+
from .middlewares.core import (
|
|
29
|
+
apply_middleware_to_router,
|
|
30
|
+
create_decorated_middleware_class,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Tachyon:
|
|
35
|
+
"""
|
|
36
|
+
Main Tachyon application class.
|
|
37
|
+
|
|
38
|
+
Provides a web framework with automatic parameter validation, dependency injection,
|
|
39
|
+
and type conversion. Built on top of Starlette for ASGI compatibility.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
_router: Internal Starlette application instance
|
|
43
|
+
routes: List of registered routes for introspection
|
|
44
|
+
_instances_cache: Cache for dependency injection singleton instances
|
|
45
|
+
openapi_config: Configuration for OpenAPI documentation
|
|
46
|
+
openapi_generator: Generator for OpenAPI schema and documentation
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, openapi_config: OpenAPIConfig = None):
|
|
50
|
+
"""
|
|
51
|
+
Initialize a new Tachyon application instance.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
openapi_config: Optional OpenAPI configuration. If not provided,
|
|
55
|
+
uses default configuration similar to FastAPI.
|
|
56
|
+
"""
|
|
57
|
+
self._router = Starlette()
|
|
58
|
+
self.routes = []
|
|
59
|
+
self.middleware_stack = []
|
|
60
|
+
self._instances_cache: Dict[Type, Any] = {}
|
|
61
|
+
|
|
62
|
+
# Initialize OpenAPI configuration and generator
|
|
63
|
+
self.openapi_config = openapi_config or create_openapi_config()
|
|
64
|
+
self.openapi_generator = OpenAPIGenerator(self.openapi_config)
|
|
65
|
+
self._docs_setup = False
|
|
66
|
+
|
|
67
|
+
# Dynamically create HTTP method decorators (get, post, put, delete, etc.)
|
|
68
|
+
http_methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]
|
|
69
|
+
|
|
70
|
+
for method in http_methods:
|
|
71
|
+
setattr(
|
|
72
|
+
self,
|
|
73
|
+
method.lower(),
|
|
74
|
+
partial(self._create_decorator, http_method=method),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def _convert_value(
|
|
79
|
+
value_str: str,
|
|
80
|
+
target_type: Type,
|
|
81
|
+
param_name: str,
|
|
82
|
+
is_path_param: bool = False, # noqa
|
|
83
|
+
) -> Union[Any, JSONResponse]:
|
|
84
|
+
"""
|
|
85
|
+
Convert a string value to the target type with appropriate error handling.
|
|
86
|
+
|
|
87
|
+
This method handles type conversion for query and path parameters,
|
|
88
|
+
including special handling for boolean values and proper error responses.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
value_str: The string value to convert
|
|
92
|
+
target_type: The target Python type to convert to
|
|
93
|
+
param_name: Name of the parameter (for error messages)
|
|
94
|
+
is_path_param: Whether this is a path parameter (affects error response)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
The converted value, or a JSONResponse with appropriate error code
|
|
98
|
+
|
|
99
|
+
Note:
|
|
100
|
+
- Boolean conversion accepts: "true", "1", "t", "yes" (case-insensitive)
|
|
101
|
+
- Path parameter errors return 404, query parameter errors return 422
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
if target_type is bool:
|
|
105
|
+
return value_str.lower() in ("true", "1", "t", "yes")
|
|
106
|
+
elif target_type is not str:
|
|
107
|
+
return target_type(value_str)
|
|
108
|
+
else:
|
|
109
|
+
return value_str
|
|
110
|
+
except (ValueError, TypeError):
|
|
111
|
+
if is_path_param:
|
|
112
|
+
return JSONResponse({"detail": "Not Found"}, status_code=404)
|
|
113
|
+
else:
|
|
114
|
+
type_name = "integer" if target_type is int else target_type.__name__
|
|
115
|
+
return JSONResponse(
|
|
116
|
+
{"detail": f"Invalid value for {type_name} conversion"},
|
|
117
|
+
status_code=422,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def _resolve_dependency(self, cls: Type) -> Any:
|
|
121
|
+
"""
|
|
122
|
+
Resolve a dependency and its sub-dependencies recursively.
|
|
123
|
+
|
|
124
|
+
This method implements dependency injection with singleton pattern,
|
|
125
|
+
automatically resolving constructor dependencies and caching instances.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
cls: The class type to resolve and instantiate
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
An instance of the requested class with all dependencies resolved
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
TypeError: If the class cannot be instantiated or is not marked as injectable
|
|
135
|
+
|
|
136
|
+
Note:
|
|
137
|
+
- Uses singleton pattern - instances are cached and reused
|
|
138
|
+
- Supports both @injectable decorated classes and simple classes
|
|
139
|
+
- Recursively resolves constructor dependencies
|
|
140
|
+
"""
|
|
141
|
+
# Return cached instance if available (singleton pattern)
|
|
142
|
+
if cls in self._instances_cache:
|
|
143
|
+
return self._instances_cache[cls]
|
|
144
|
+
|
|
145
|
+
# For non-injectable classes, try to create without arguments
|
|
146
|
+
if cls not in _registry:
|
|
147
|
+
try:
|
|
148
|
+
# Works for classes without __init__ or with no-arg __init__
|
|
149
|
+
return cls()
|
|
150
|
+
except TypeError:
|
|
151
|
+
raise TypeError(
|
|
152
|
+
f"Cannot resolve dependency '{cls.__name__}'. "
|
|
153
|
+
f"Did you forget to mark it with @injectable?"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# For injectable classes, resolve constructor dependencies
|
|
157
|
+
sig = inspect.signature(cls)
|
|
158
|
+
dependencies = {}
|
|
159
|
+
|
|
160
|
+
# Recursively resolve each constructor parameter
|
|
161
|
+
for param in sig.parameters.values():
|
|
162
|
+
if param.name != "self":
|
|
163
|
+
dependencies[param.name] = self._resolve_dependency(param.annotation)
|
|
164
|
+
|
|
165
|
+
# Create instance with resolved dependencies and cache it
|
|
166
|
+
instance = cls(**dependencies)
|
|
167
|
+
self._instances_cache[cls] = instance
|
|
168
|
+
return instance
|
|
169
|
+
|
|
170
|
+
def _create_decorator(self, path: str, *, http_method: str, **kwargs):
|
|
171
|
+
"""
|
|
172
|
+
Create a decorator for the specified HTTP method.
|
|
173
|
+
|
|
174
|
+
This factory method creates method-specific decorators (e.g., @app.get, @app.post)
|
|
175
|
+
that register endpoint functions with the application.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
path: URL path pattern (supports path parameters with {param} syntax)
|
|
179
|
+
http_method: HTTP method name (GET, POST, PUT, DELETE, etc.)
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
A decorator function that registers the endpoint
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
def decorator(endpoint_func: Callable):
|
|
186
|
+
self._add_route(path, endpoint_func, http_method, **kwargs)
|
|
187
|
+
return endpoint_func
|
|
188
|
+
|
|
189
|
+
return decorator
|
|
190
|
+
|
|
191
|
+
def _add_route(self, path: str, endpoint_func: Callable, method: str, **kwargs):
|
|
192
|
+
"""
|
|
193
|
+
Register a route with the application and create an async handler.
|
|
194
|
+
|
|
195
|
+
This is the core method that handles parameter injection, validation, and
|
|
196
|
+
type conversion. It creates an async handler that processes requests and
|
|
197
|
+
automatically injects dependencies, path parameters, query parameters, and
|
|
198
|
+
request body data into the endpoint function.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
path: URL path pattern (e.g., "/users/{user_id}")
|
|
202
|
+
endpoint_func: The endpoint function to handle requests
|
|
203
|
+
method: HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
204
|
+
|
|
205
|
+
Note:
|
|
206
|
+
The created handler processes parameters in the following order:
|
|
207
|
+
1. Dependencies (explicit with Depends() or implicit via @injectable)
|
|
208
|
+
2. Body parameters (JSON request body validated against Struct models)
|
|
209
|
+
3. Query parameters (URL query string with type conversion)
|
|
210
|
+
4. Path parameters (both explicit with Path() and implicit from URL)
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
async def handler(request):
|
|
214
|
+
"""
|
|
215
|
+
Async request handler that processes parameters and calls the endpoint.
|
|
216
|
+
|
|
217
|
+
This handler analyzes the endpoint function signature and automatically
|
|
218
|
+
injects the appropriate values based on parameter annotations and defaults.
|
|
219
|
+
"""
|
|
220
|
+
kwargs_to_inject = {}
|
|
221
|
+
sig = inspect.signature(endpoint_func)
|
|
222
|
+
query_params = request.query_params
|
|
223
|
+
path_params = request.path_params
|
|
224
|
+
_raw_body = None
|
|
225
|
+
|
|
226
|
+
# Process each parameter in the endpoint function signature
|
|
227
|
+
for param in sig.parameters.values():
|
|
228
|
+
# Determine if this parameter is a dependency
|
|
229
|
+
is_explicit_dependency = isinstance(param.default, Depends)
|
|
230
|
+
is_implicit_dependency = (
|
|
231
|
+
param.default is inspect.Parameter.empty
|
|
232
|
+
and param.annotation in _registry
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Process dependencies (explicit and implicit)
|
|
236
|
+
if is_explicit_dependency or is_implicit_dependency:
|
|
237
|
+
target_class = param.annotation
|
|
238
|
+
kwargs_to_inject[param.name] = self._resolve_dependency(
|
|
239
|
+
target_class
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Process Body parameters (JSON request body)
|
|
243
|
+
elif isinstance(param.default, Body):
|
|
244
|
+
model_class = param.annotation
|
|
245
|
+
if not issubclass(model_class, Struct):
|
|
246
|
+
raise TypeError(
|
|
247
|
+
"Body type must be an instance of Tachyon_api.models.Struct"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
decoder = msgspec.json.Decoder(model_class)
|
|
251
|
+
try:
|
|
252
|
+
if _raw_body is None:
|
|
253
|
+
_raw_body = await request.body()
|
|
254
|
+
validated_data = decoder.decode(_raw_body)
|
|
255
|
+
kwargs_to_inject[param.name] = validated_data
|
|
256
|
+
except msgspec.ValidationError as e:
|
|
257
|
+
return JSONResponse({"detail": str(e)}, status_code=422)
|
|
258
|
+
|
|
259
|
+
# Process Query parameters (URL query string)
|
|
260
|
+
elif isinstance(param.default, Query):
|
|
261
|
+
query_info = param.default
|
|
262
|
+
param_name = param.name
|
|
263
|
+
|
|
264
|
+
if param_name in query_params:
|
|
265
|
+
value_str = query_params[param_name]
|
|
266
|
+
converted_value = self._convert_value(
|
|
267
|
+
value_str, param.annotation, param_name, is_path_param=False
|
|
268
|
+
)
|
|
269
|
+
# Return error response if conversion failed
|
|
270
|
+
if isinstance(converted_value, JSONResponse):
|
|
271
|
+
return converted_value
|
|
272
|
+
kwargs_to_inject[param_name] = converted_value
|
|
273
|
+
|
|
274
|
+
elif query_info.default is not ...:
|
|
275
|
+
# Use default value if parameter is optional
|
|
276
|
+
kwargs_to_inject[param.name] = query_info.default
|
|
277
|
+
else:
|
|
278
|
+
# Return error if required parameter is missing
|
|
279
|
+
return JSONResponse(
|
|
280
|
+
{
|
|
281
|
+
"detail": f"Missing required query parameter: {param_name}"
|
|
282
|
+
},
|
|
283
|
+
status_code=422,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Process explicit Path parameters (with Path() annotation)
|
|
287
|
+
elif isinstance(param.default, Path):
|
|
288
|
+
param_name = param.name
|
|
289
|
+
if param_name in path_params:
|
|
290
|
+
value_str = path_params[param_name]
|
|
291
|
+
converted_value = self._convert_value(
|
|
292
|
+
value_str, param.annotation, param_name, is_path_param=True
|
|
293
|
+
)
|
|
294
|
+
# Return 404 if conversion failed
|
|
295
|
+
if isinstance(converted_value, JSONResponse):
|
|
296
|
+
return converted_value
|
|
297
|
+
kwargs_to_inject[param_name] = converted_value
|
|
298
|
+
else:
|
|
299
|
+
return JSONResponse({"detail": "Not Found"}, status_code=404)
|
|
300
|
+
|
|
301
|
+
# Process implicit Path parameters (URL path variables without Path())
|
|
302
|
+
elif (
|
|
303
|
+
param.default is inspect.Parameter.empty
|
|
304
|
+
and param.name in path_params
|
|
305
|
+
and not is_explicit_dependency
|
|
306
|
+
and not is_implicit_dependency
|
|
307
|
+
):
|
|
308
|
+
param_name = param.name
|
|
309
|
+
value_str = path_params[param_name]
|
|
310
|
+
converted_value = self._convert_value(
|
|
311
|
+
value_str, param.annotation, param_name, is_path_param=True
|
|
312
|
+
)
|
|
313
|
+
# Return 404 if conversion failed
|
|
314
|
+
if isinstance(converted_value, JSONResponse):
|
|
315
|
+
return converted_value
|
|
316
|
+
kwargs_to_inject[param_name] = converted_value
|
|
317
|
+
|
|
318
|
+
# Call the endpoint function with injected parameters
|
|
319
|
+
if asyncio.iscoroutinefunction(endpoint_func):
|
|
320
|
+
payload = await endpoint_func(**kwargs_to_inject)
|
|
321
|
+
else:
|
|
322
|
+
payload = endpoint_func(**kwargs_to_inject)
|
|
323
|
+
|
|
324
|
+
# If the endpoint already returned a Response object, return it directly
|
|
325
|
+
if isinstance(payload, Response):
|
|
326
|
+
return payload
|
|
327
|
+
|
|
328
|
+
# Convert Struct objects to dictionaries for JSON serialization
|
|
329
|
+
if isinstance(payload, Struct):
|
|
330
|
+
payload = msgspec.to_builtins(payload)
|
|
331
|
+
elif isinstance(payload, dict):
|
|
332
|
+
# Convert any Struct values in the dictionary
|
|
333
|
+
for key, value in payload.items():
|
|
334
|
+
if isinstance(value, Struct):
|
|
335
|
+
payload[key] = msgspec.to_builtins(value)
|
|
336
|
+
|
|
337
|
+
return JSONResponse(payload)
|
|
338
|
+
|
|
339
|
+
# Register the route with Starlette
|
|
340
|
+
route = Route(path, endpoint=handler, methods=[method])
|
|
341
|
+
self._router.routes.append(route)
|
|
342
|
+
self.routes.append(
|
|
343
|
+
{"path": path, "method": method, "func": endpoint_func, **kwargs}
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Generate OpenAPI documentation for this route
|
|
347
|
+
include_in_schema = kwargs.get("include_in_schema", True)
|
|
348
|
+
if include_in_schema:
|
|
349
|
+
self._generate_openapi_for_route(path, method, endpoint_func, **kwargs)
|
|
350
|
+
|
|
351
|
+
def _generate_openapi_for_route(
|
|
352
|
+
self, path: str, method: str, endpoint_func: Callable, **kwargs
|
|
353
|
+
):
|
|
354
|
+
"""
|
|
355
|
+
Generate OpenAPI documentation for a specific route.
|
|
356
|
+
|
|
357
|
+
This method analyzes the endpoint function signature and generates appropriate
|
|
358
|
+
OpenAPI schema entries for parameters, request body, and responses.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
path: URL path pattern
|
|
362
|
+
method: HTTP method
|
|
363
|
+
endpoint_func: The endpoint function
|
|
364
|
+
**kwargs: Additional route metadata (summary, description, tags, etc.)
|
|
365
|
+
"""
|
|
366
|
+
sig = inspect.signature(endpoint_func)
|
|
367
|
+
|
|
368
|
+
# Build the OpenAPI operation object
|
|
369
|
+
operation = {
|
|
370
|
+
"summary": kwargs.get(
|
|
371
|
+
"summary", self._generate_summary_from_function(endpoint_func)
|
|
372
|
+
),
|
|
373
|
+
"description": kwargs.get("description", endpoint_func.__doc__ or ""),
|
|
374
|
+
"responses": {
|
|
375
|
+
"200": {
|
|
376
|
+
"description": "Successful Response",
|
|
377
|
+
"content": {"application/json": {"schema": {"type": "object"}}},
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
# Add tags if provided
|
|
383
|
+
if "tags" in kwargs:
|
|
384
|
+
operation["tags"] = kwargs["tags"]
|
|
385
|
+
|
|
386
|
+
# Process parameters from function signature
|
|
387
|
+
parameters = []
|
|
388
|
+
request_body_schema = None
|
|
389
|
+
|
|
390
|
+
for param in sig.parameters.values():
|
|
391
|
+
# Skip dependency parameters
|
|
392
|
+
if isinstance(param.default, Depends) or (
|
|
393
|
+
param.default is inspect.Parameter.empty
|
|
394
|
+
and param.annotation in _registry
|
|
395
|
+
):
|
|
396
|
+
continue
|
|
397
|
+
|
|
398
|
+
# Process query parameters
|
|
399
|
+
elif isinstance(param.default, Query):
|
|
400
|
+
parameters.append(
|
|
401
|
+
{
|
|
402
|
+
"name": param.name,
|
|
403
|
+
"in": "query",
|
|
404
|
+
"required": param.default.default is ...,
|
|
405
|
+
"schema": {"type": self._get_openapi_type(param.annotation)},
|
|
406
|
+
"description": getattr(param.default, "description", ""),
|
|
407
|
+
}
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Process path parameters
|
|
411
|
+
elif isinstance(param.default, Path) or self._is_path_parameter(
|
|
412
|
+
param.name, path
|
|
413
|
+
):
|
|
414
|
+
parameters.append(
|
|
415
|
+
{
|
|
416
|
+
"name": param.name,
|
|
417
|
+
"in": "path",
|
|
418
|
+
"required": True,
|
|
419
|
+
"schema": {"type": self._get_openapi_type(param.annotation)},
|
|
420
|
+
"description": getattr(param.default, "description", "")
|
|
421
|
+
if isinstance(param.default, Path)
|
|
422
|
+
else "",
|
|
423
|
+
}
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Process body parameters
|
|
427
|
+
elif isinstance(param.default, Body):
|
|
428
|
+
model_class = param.annotation
|
|
429
|
+
if issubclass(model_class, Struct):
|
|
430
|
+
schema_name = model_class.__name__
|
|
431
|
+
# Add schema to components
|
|
432
|
+
self.openapi_generator.add_schema(
|
|
433
|
+
schema_name, _generate_schema_for_struct(model_class)
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
request_body_schema = {
|
|
437
|
+
"content": {
|
|
438
|
+
"application/json": {
|
|
439
|
+
"schema": {
|
|
440
|
+
"$ref": f"#/components/schemas/{schema_name}"
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
"required": True,
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
# Add parameters to operation if any exist
|
|
448
|
+
if parameters:
|
|
449
|
+
operation["parameters"] = parameters
|
|
450
|
+
|
|
451
|
+
# Add request body if it exists
|
|
452
|
+
if request_body_schema:
|
|
453
|
+
operation["requestBody"] = request_body_schema
|
|
454
|
+
|
|
455
|
+
# Add the operation to the OpenAPI schema
|
|
456
|
+
self.openapi_generator.add_path(path, method, operation)
|
|
457
|
+
|
|
458
|
+
@staticmethod
|
|
459
|
+
def _generate_summary_from_function(func: Callable) -> str:
|
|
460
|
+
"""Generate a human-readable summary from function name."""
|
|
461
|
+
return func.__name__.replace("_", " ").title()
|
|
462
|
+
|
|
463
|
+
@staticmethod
|
|
464
|
+
def _is_path_parameter(param_name: str, path: str) -> bool:
|
|
465
|
+
"""Check if a parameter name corresponds to a path parameter in the URL."""
|
|
466
|
+
return f"{{{param_name}}}" in path
|
|
467
|
+
|
|
468
|
+
@staticmethod
|
|
469
|
+
def _get_openapi_type(python_type: Type) -> str:
|
|
470
|
+
"""Convert Python type to OpenAPI schema type."""
|
|
471
|
+
type_map: Dict[Type, str] = {
|
|
472
|
+
int: "integer",
|
|
473
|
+
str: "string",
|
|
474
|
+
bool: "boolean",
|
|
475
|
+
float: "number",
|
|
476
|
+
}
|
|
477
|
+
return type_map.get(python_type, "string")
|
|
478
|
+
|
|
479
|
+
def _setup_docs(self):
|
|
480
|
+
"""
|
|
481
|
+
Setup OpenAPI documentation endpoints.
|
|
482
|
+
|
|
483
|
+
This method registers the routes for serving OpenAPI JSON schema,
|
|
484
|
+
Swagger UI, and ReDoc documentation interfaces.
|
|
485
|
+
"""
|
|
486
|
+
if self._docs_setup:
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
self._docs_setup = True
|
|
490
|
+
|
|
491
|
+
# OpenAPI JSON schema endpoint
|
|
492
|
+
@self.get(self.openapi_config.openapi_url, include_in_schema=False)
|
|
493
|
+
def get_openapi_schema():
|
|
494
|
+
"""Serve the OpenAPI JSON schema."""
|
|
495
|
+
return self.openapi_generator.get_openapi_schema()
|
|
496
|
+
|
|
497
|
+
# Scalar API Reference documentation endpoint (default for /docs)
|
|
498
|
+
@self.get(self.openapi_config.docs_url, include_in_schema=False)
|
|
499
|
+
def get_scalar_docs():
|
|
500
|
+
"""Serve the Scalar API Reference documentation interface."""
|
|
501
|
+
html = self.openapi_generator.get_scalar_html(
|
|
502
|
+
self.openapi_config.openapi_url, self.openapi_config.info.title
|
|
503
|
+
)
|
|
504
|
+
return HTMLResponse(html)
|
|
505
|
+
|
|
506
|
+
# Swagger UI documentation endpoint (legacy support)
|
|
507
|
+
@self.get("/swagger", include_in_schema=False)
|
|
508
|
+
def get_swagger_ui():
|
|
509
|
+
"""Serve the Swagger UI documentation interface."""
|
|
510
|
+
html = self.openapi_generator.get_swagger_ui_html(
|
|
511
|
+
self.openapi_config.openapi_url, self.openapi_config.info.title
|
|
512
|
+
)
|
|
513
|
+
return HTMLResponse(html)
|
|
514
|
+
|
|
515
|
+
# ReDoc documentation endpoint
|
|
516
|
+
@self.get(self.openapi_config.redoc_url, include_in_schema=False)
|
|
517
|
+
def get_redoc():
|
|
518
|
+
"""Serve the ReDoc documentation interface."""
|
|
519
|
+
html = self.openapi_generator.get_redoc_html(
|
|
520
|
+
self.openapi_config.openapi_url, self.openapi_config.info.title
|
|
521
|
+
)
|
|
522
|
+
return HTMLResponse(html)
|
|
523
|
+
|
|
524
|
+
async def __call__(self, scope, receive, send):
|
|
525
|
+
"""
|
|
526
|
+
ASGI application entry point.
|
|
527
|
+
|
|
528
|
+
Delegates request handling to the internal Starlette application.
|
|
529
|
+
This makes Tachyon compatible with ASGI servers like Uvicorn.
|
|
530
|
+
"""
|
|
531
|
+
# Setup documentation endpoints on first request
|
|
532
|
+
if not self._docs_setup:
|
|
533
|
+
self._setup_docs()
|
|
534
|
+
await self._router(scope, receive, send)
|
|
535
|
+
|
|
536
|
+
def include_router(self, router, **kwargs):
|
|
537
|
+
"""
|
|
538
|
+
Include a Router instance in the application.
|
|
539
|
+
|
|
540
|
+
This method registers all routes from the router with the main application,
|
|
541
|
+
applying the router's prefix, tags, and dependencies.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
router: The Router instance to include
|
|
545
|
+
**kwargs: Additional options (currently reserved for future use)
|
|
546
|
+
"""
|
|
547
|
+
from .router import Router
|
|
548
|
+
|
|
549
|
+
if not isinstance(router, Router):
|
|
550
|
+
raise TypeError("Expected Router instance")
|
|
551
|
+
|
|
552
|
+
# Register all routes from the router
|
|
553
|
+
for route_info in router.routes:
|
|
554
|
+
# Get the full path with prefix
|
|
555
|
+
full_path = router.get_full_path(route_info["path"])
|
|
556
|
+
|
|
557
|
+
# Create a copy of route info with the full path
|
|
558
|
+
route_kwargs = route_info.copy()
|
|
559
|
+
route_kwargs.pop("path", None)
|
|
560
|
+
route_kwargs.pop("method", None)
|
|
561
|
+
route_kwargs.pop("func", None)
|
|
562
|
+
|
|
563
|
+
# Register the route with the main app
|
|
564
|
+
self._add_route(
|
|
565
|
+
full_path, route_info["func"], route_info["method"], **route_kwargs
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
def add_middleware(self, middleware_class, **options):
|
|
569
|
+
"""
|
|
570
|
+
Adds a middleware to the application's stack.
|
|
571
|
+
|
|
572
|
+
Middlewares are processed in the order they are added. They follow
|
|
573
|
+
the ASGI middleware specification.
|
|
574
|
+
|
|
575
|
+
Args:
|
|
576
|
+
middleware_class: The middleware class.
|
|
577
|
+
**options: Options to be passed to the middleware constructor.
|
|
578
|
+
"""
|
|
579
|
+
# Usar helper centralizado para aplicar el middleware sobre Starlette
|
|
580
|
+
apply_middleware_to_router(self._router, middleware_class, **options)
|
|
581
|
+
|
|
582
|
+
if not hasattr(self, "middleware_stack"):
|
|
583
|
+
self.middleware_stack = []
|
|
584
|
+
self.middleware_stack.append({"func": middleware_class, "options": options})
|
|
585
|
+
|
|
586
|
+
def middleware(self, middleware_type="http"):
|
|
587
|
+
"""
|
|
588
|
+
Decorator for adding a middleware to the application.
|
|
589
|
+
Similar to route decorators (@app.get, etc.)
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
middleware_type: Type of middleware ('http' by default)
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
A decorator that registers the decorated function as middleware.
|
|
596
|
+
"""
|
|
597
|
+
|
|
598
|
+
def decorator(middleware_func):
|
|
599
|
+
# Crear una clase de middleware a partir de la función decorada
|
|
600
|
+
DecoratedMiddleware = create_decorated_middleware_class(
|
|
601
|
+
middleware_func, middleware_type
|
|
602
|
+
)
|
|
603
|
+
# Registrar el middleware usando el método existente
|
|
604
|
+
self.add_middleware(DecoratedMiddleware)
|
|
605
|
+
return middleware_func
|
|
606
|
+
|
|
607
|
+
return decorator
|
tachyon_api/di.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tachyon Web Framework - Dependency Injection Module
|
|
3
|
+
|
|
4
|
+
This module provides a lightweight dependency injection system that supports
|
|
5
|
+
both explicit and implicit dependency resolution with singleton pattern.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Set, Type, TypeVar
|
|
9
|
+
|
|
10
|
+
# Global registry of injectable classes
|
|
11
|
+
_registry: Set[Type] = set()
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Depends:
|
|
17
|
+
"""
|
|
18
|
+
Marker class for explicit dependency injection.
|
|
19
|
+
|
|
20
|
+
Use this as a default parameter value to explicitly mark a parameter
|
|
21
|
+
as a dependency that should be resolved and injected automatically.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
@app.get("/users")
|
|
25
|
+
def get_users(service: UserService = Depends()):
|
|
26
|
+
return service.list_all()
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
"""Initialize a dependency marker."""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def injectable(cls: Type[T]) -> Type[T]:
|
|
35
|
+
"""
|
|
36
|
+
Decorator to mark a class as injectable for dependency injection.
|
|
37
|
+
|
|
38
|
+
Classes marked with this decorator can be automatically resolved and
|
|
39
|
+
injected into endpoint functions and other injectable classes.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
cls: The class to mark as injectable
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
The same class, now registered for dependency injection
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
@injectable
|
|
49
|
+
class UserRepository:
|
|
50
|
+
def __init__(self, db: Database):
|
|
51
|
+
self.db = db
|
|
52
|
+
|
|
53
|
+
@injectable
|
|
54
|
+
class UserService:
|
|
55
|
+
def __init__(self, repo: UserRepository):
|
|
56
|
+
self.repo = repo
|
|
57
|
+
"""
|
|
58
|
+
_registry.add(cls)
|
|
59
|
+
return cls
|