tachyon-api 0.9.0__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.
- tachyon_api/__init__.py +59 -0
- tachyon_api/app.py +699 -0
- tachyon_api/background.py +72 -0
- tachyon_api/cache.py +270 -0
- tachyon_api/cli/__init__.py +9 -0
- tachyon_api/cli/__main__.py +8 -0
- tachyon_api/cli/commands/__init__.py +5 -0
- tachyon_api/cli/commands/generate.py +190 -0
- tachyon_api/cli/commands/lint.py +186 -0
- tachyon_api/cli/commands/new.py +82 -0
- tachyon_api/cli/commands/openapi.py +128 -0
- tachyon_api/cli/main.py +69 -0
- tachyon_api/cli/templates/__init__.py +8 -0
- tachyon_api/cli/templates/project.py +194 -0
- tachyon_api/cli/templates/service.py +330 -0
- tachyon_api/core/__init__.py +12 -0
- tachyon_api/core/lifecycle.py +106 -0
- tachyon_api/core/websocket.py +92 -0
- tachyon_api/di.py +86 -0
- tachyon_api/exceptions.py +39 -0
- tachyon_api/files.py +14 -0
- tachyon_api/middlewares/__init__.py +4 -0
- tachyon_api/middlewares/core.py +40 -0
- tachyon_api/middlewares/cors.py +159 -0
- tachyon_api/middlewares/logger.py +123 -0
- tachyon_api/models.py +73 -0
- tachyon_api/openapi.py +419 -0
- tachyon_api/params.py +268 -0
- tachyon_api/processing/__init__.py +14 -0
- tachyon_api/processing/dependencies.py +172 -0
- tachyon_api/processing/parameters.py +484 -0
- tachyon_api/processing/response_processor.py +93 -0
- tachyon_api/responses.py +92 -0
- tachyon_api/router.py +161 -0
- tachyon_api/security.py +295 -0
- tachyon_api/testing.py +110 -0
- tachyon_api/utils/__init__.py +15 -0
- tachyon_api/utils/type_converter.py +113 -0
- tachyon_api/utils/type_utils.py +162 -0
- tachyon_api-0.9.0.dist-info/METADATA +291 -0
- tachyon_api-0.9.0.dist-info/RECORD +44 -0
- tachyon_api-0.9.0.dist-info/WHEEL +4 -0
- tachyon_api-0.9.0.dist-info/entry_points.txt +3 -0
- tachyon_api-0.9.0.dist-info/licenses/LICENSE +17 -0
tachyon_api/app.py
ADDED
|
@@ -0,0 +1,699 @@
|
|
|
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
|
+
from functools import partial
|
|
12
|
+
from typing import Any, Dict, Type, Callable, Optional
|
|
13
|
+
|
|
14
|
+
from starlette.applications import Starlette
|
|
15
|
+
from starlette.requests import Request
|
|
16
|
+
from starlette.responses import JSONResponse
|
|
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
|
+
)
|
|
26
|
+
from .params import Body, Query, Path, Header, Cookie
|
|
27
|
+
from .exceptions import HTTPException
|
|
28
|
+
from .middlewares.core import (
|
|
29
|
+
apply_middleware_to_router,
|
|
30
|
+
create_decorated_middleware_class,
|
|
31
|
+
)
|
|
32
|
+
from .responses import (
|
|
33
|
+
HTMLResponse,
|
|
34
|
+
internal_server_error_response,
|
|
35
|
+
)
|
|
36
|
+
from .utils import TypeUtils
|
|
37
|
+
from .core.lifecycle import LifecycleManager
|
|
38
|
+
from .core.websocket import WebSocketManager
|
|
39
|
+
from .processing.parameters import ParameterProcessor
|
|
40
|
+
from .processing.dependencies import DependencyResolver
|
|
41
|
+
from .processing.response_processor import ResponseProcessor
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
from .cache import set_cache_config
|
|
45
|
+
except ImportError:
|
|
46
|
+
set_cache_config = None # type: ignore
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Tachyon:
|
|
50
|
+
"""
|
|
51
|
+
Main Tachyon application class.
|
|
52
|
+
|
|
53
|
+
Provides a web framework with automatic parameter validation, dependency injection,
|
|
54
|
+
and type conversion. Built on top of Starlette for ASGI compatibility.
|
|
55
|
+
|
|
56
|
+
Attributes:
|
|
57
|
+
_router: Internal Starlette application instance
|
|
58
|
+
routes: List of registered routes for introspection
|
|
59
|
+
_instances_cache: Cache for dependency injection singleton instances
|
|
60
|
+
openapi_config: Configuration for OpenAPI documentation
|
|
61
|
+
openapi_generator: Generator for OpenAPI schema and documentation
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
openapi_config: OpenAPIConfig = None,
|
|
67
|
+
cache_config=None,
|
|
68
|
+
lifespan: Optional[Callable] = None,
|
|
69
|
+
):
|
|
70
|
+
"""
|
|
71
|
+
Initialize a new Tachyon application instance.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
openapi_config: Optional OpenAPI configuration. If not provided,
|
|
75
|
+
uses default configuration similar to FastAPI.
|
|
76
|
+
cache_config: Optional cache configuration (tachyon_api.cache.CacheConfig).
|
|
77
|
+
If provided, it will be set as the active cache configuration.
|
|
78
|
+
lifespan: Optional async context manager for startup/shutdown events.
|
|
79
|
+
Similar to FastAPI's lifespan parameter.
|
|
80
|
+
"""
|
|
81
|
+
# Lifecycle manager for startup/shutdown events
|
|
82
|
+
self._lifecycle_manager = LifecycleManager(lifespan)
|
|
83
|
+
|
|
84
|
+
# Exception handlers registry (exception_type -> handler_function)
|
|
85
|
+
self._exception_handlers: Dict[Type[Exception], Callable] = {}
|
|
86
|
+
|
|
87
|
+
# Create combined lifespan that handles both custom lifespan and on_event handlers
|
|
88
|
+
self._router = Starlette(lifespan=self._lifecycle_manager.create_combined_lifespan())
|
|
89
|
+
|
|
90
|
+
# WebSocket manager
|
|
91
|
+
self._websocket_manager = WebSocketManager(self._router)
|
|
92
|
+
|
|
93
|
+
# Parameter processor
|
|
94
|
+
self._parameter_processor = ParameterProcessor(self)
|
|
95
|
+
|
|
96
|
+
# Dependency resolver
|
|
97
|
+
self._dependency_resolver = DependencyResolver(self)
|
|
98
|
+
self.routes = []
|
|
99
|
+
self.middleware_stack = []
|
|
100
|
+
self._instances_cache: Dict[Type, Any] = {}
|
|
101
|
+
|
|
102
|
+
# Expose state object for storing app-wide state (like FastAPI)
|
|
103
|
+
self.state = self._router.state
|
|
104
|
+
|
|
105
|
+
# Dependency overrides for testing (like FastAPI)
|
|
106
|
+
self.dependency_overrides: Dict[Any, Any] = {}
|
|
107
|
+
|
|
108
|
+
# Initialize OpenAPI configuration and generator
|
|
109
|
+
self.openapi_config = openapi_config or create_openapi_config()
|
|
110
|
+
self.openapi_generator = OpenAPIGenerator(self.openapi_config)
|
|
111
|
+
self._docs_setup = False
|
|
112
|
+
|
|
113
|
+
# Apply cache configuration if provided
|
|
114
|
+
self.cache_config = cache_config
|
|
115
|
+
if cache_config is not None and set_cache_config is not None:
|
|
116
|
+
try:
|
|
117
|
+
set_cache_config(cache_config)
|
|
118
|
+
except Exception:
|
|
119
|
+
# Do not break app initialization if cache setup fails
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
# Dynamically create HTTP method decorators (get, post, put, delete, etc.)
|
|
123
|
+
http_methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]
|
|
124
|
+
|
|
125
|
+
for method in http_methods:
|
|
126
|
+
setattr(
|
|
127
|
+
self,
|
|
128
|
+
method.lower(),
|
|
129
|
+
partial(self._create_decorator, http_method=method),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def on_event(self, event_type: str):
|
|
133
|
+
"""
|
|
134
|
+
Decorator to register startup or shutdown event handlers.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
event_type: Either 'startup' or 'shutdown'
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
A decorator that registers the handler function
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
@app.on_event('startup')
|
|
144
|
+
async def on_startup():
|
|
145
|
+
print('Starting up...')
|
|
146
|
+
|
|
147
|
+
@app.on_event('shutdown')
|
|
148
|
+
def on_shutdown():
|
|
149
|
+
print('Shutting down...')
|
|
150
|
+
"""
|
|
151
|
+
return self._lifecycle_manager.on_event_decorator(event_type)
|
|
152
|
+
|
|
153
|
+
def exception_handler(self, exc_class: Type[Exception]):
|
|
154
|
+
"""
|
|
155
|
+
Decorator to register a custom exception handler.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
exc_class: The exception class to handle
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
A decorator that registers the handler function
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
@app.exception_handler(ValueError)
|
|
165
|
+
async def handle_value_error(request, exc):
|
|
166
|
+
return JSONResponse(
|
|
167
|
+
status_code=400,
|
|
168
|
+
content={"error": str(exc)}
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
@app.exception_handler(HTTPException)
|
|
172
|
+
async def custom_http_handler(request, exc):
|
|
173
|
+
return JSONResponse(
|
|
174
|
+
status_code=exc.status_code,
|
|
175
|
+
content={"error": exc.detail, "custom": True}
|
|
176
|
+
)
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def decorator(func: Callable):
|
|
180
|
+
self._exception_handlers[exc_class] = func
|
|
181
|
+
return func
|
|
182
|
+
|
|
183
|
+
return decorator
|
|
184
|
+
|
|
185
|
+
def websocket(self, path: str):
|
|
186
|
+
"""
|
|
187
|
+
Decorator to register a WebSocket endpoint.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
path: URL path pattern for the WebSocket endpoint
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
A decorator that registers the WebSocket handler
|
|
194
|
+
|
|
195
|
+
Example:
|
|
196
|
+
@app.websocket("/ws")
|
|
197
|
+
async def websocket_endpoint(websocket):
|
|
198
|
+
await websocket.accept()
|
|
199
|
+
data = await websocket.receive_text()
|
|
200
|
+
await websocket.send_text(f"Echo: {data}")
|
|
201
|
+
await websocket.close()
|
|
202
|
+
|
|
203
|
+
@app.websocket("/ws/{room_id}")
|
|
204
|
+
async def room_endpoint(websocket, room_id: str):
|
|
205
|
+
await websocket.accept()
|
|
206
|
+
await websocket.send_text(f"Welcome to {room_id}")
|
|
207
|
+
await websocket.close()
|
|
208
|
+
"""
|
|
209
|
+
return self._websocket_manager.websocket_decorator(path)
|
|
210
|
+
|
|
211
|
+
def _resolve_dependency(self, cls: Type) -> Any:
|
|
212
|
+
"""Delegate to DependencyResolver."""
|
|
213
|
+
return self._dependency_resolver.resolve_dependency(cls)
|
|
214
|
+
|
|
215
|
+
async def _resolve_callable_dependency(
|
|
216
|
+
self, dependency: Callable, cache: Dict, request: Request
|
|
217
|
+
) -> Any:
|
|
218
|
+
"""Delegate to DependencyResolver."""
|
|
219
|
+
return await self._dependency_resolver.resolve_callable_dependency(
|
|
220
|
+
dependency, cache, request
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def _create_decorator(self, path: str, *, http_method: str, **kwargs):
|
|
224
|
+
"""
|
|
225
|
+
Create a decorator for the specified HTTP method.
|
|
226
|
+
|
|
227
|
+
This factory method creates method-specific decorators (e.g., @app.get, @app.post)
|
|
228
|
+
that register endpoint functions with the application.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
path: URL path pattern (supports path parameters with {param} syntax)
|
|
232
|
+
http_method: HTTP method name (GET, POST, PUT, DELETE, etc.)
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
A decorator function that registers the endpoint
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
def decorator(endpoint_func: Callable):
|
|
239
|
+
self._add_route(path, endpoint_func, http_method, **kwargs)
|
|
240
|
+
return endpoint_func
|
|
241
|
+
|
|
242
|
+
return decorator
|
|
243
|
+
|
|
244
|
+
def _add_route(self, path: str, endpoint_func: Callable, method: str, **kwargs):
|
|
245
|
+
"""
|
|
246
|
+
Register a route with the application and create an async handler.
|
|
247
|
+
|
|
248
|
+
This is the core method that handles parameter injection, validation, and
|
|
249
|
+
type conversion. It creates an async handler that processes requests and
|
|
250
|
+
automatically injects dependencies, path parameters, query parameters, and
|
|
251
|
+
request body data into the endpoint function.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
path: URL path pattern (e.g., "/users/{user_id}")
|
|
255
|
+
endpoint_func: The endpoint function to handle requests
|
|
256
|
+
method: HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
257
|
+
|
|
258
|
+
Note:
|
|
259
|
+
The created handler processes parameters in the following order:
|
|
260
|
+
1. Dependencies (explicit with Depends() or implicit via @injectable)
|
|
261
|
+
2. Body parameters (JSON request body validated against Struct models)
|
|
262
|
+
3. Query parameters (URL query string with type conversion)
|
|
263
|
+
4. Path parameters (both explicit with Path() and implicit from URL)
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
response_model = kwargs.get("response_model")
|
|
267
|
+
|
|
268
|
+
async def handler(request):
|
|
269
|
+
"""
|
|
270
|
+
Async request handler that processes parameters and calls the endpoint.
|
|
271
|
+
|
|
272
|
+
This handler analyzes the endpoint function signature and automatically
|
|
273
|
+
injects the appropriate values based on parameter annotations and defaults.
|
|
274
|
+
"""
|
|
275
|
+
try:
|
|
276
|
+
# Process all parameters using ParameterProcessor
|
|
277
|
+
dependency_cache = {}
|
|
278
|
+
kwargs_to_inject, error_response, _background_tasks = await self._parameter_processor.process_parameters(
|
|
279
|
+
endpoint_func, request, dependency_cache
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Return early if parameter processing failed
|
|
283
|
+
if error_response is not None:
|
|
284
|
+
return error_response
|
|
285
|
+
|
|
286
|
+
# Call the endpoint function with injected parameters
|
|
287
|
+
payload = await ResponseProcessor.call_endpoint(
|
|
288
|
+
endpoint_func, kwargs_to_inject
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# Process response (validate, serialize, run background tasks)
|
|
292
|
+
return await ResponseProcessor.process_response(
|
|
293
|
+
payload, response_model, _background_tasks
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
except HTTPException as exc:
|
|
297
|
+
# Handle HTTPException - check for custom handler first
|
|
298
|
+
handler = self._exception_handlers.get(HTTPException)
|
|
299
|
+
if handler is not None:
|
|
300
|
+
if asyncio.iscoroutinefunction(handler):
|
|
301
|
+
return await handler(request, exc)
|
|
302
|
+
else:
|
|
303
|
+
return handler(request, exc)
|
|
304
|
+
# Default HTTPException handling
|
|
305
|
+
response = JSONResponse(
|
|
306
|
+
{"detail": exc.detail}, status_code=exc.status_code
|
|
307
|
+
)
|
|
308
|
+
if exc.headers:
|
|
309
|
+
for key, value in exc.headers.items():
|
|
310
|
+
response.headers[key] = value
|
|
311
|
+
return response
|
|
312
|
+
|
|
313
|
+
except Exception as exc:
|
|
314
|
+
# Check for custom exception handler
|
|
315
|
+
for exc_class, handler in self._exception_handlers.items():
|
|
316
|
+
if isinstance(exc, exc_class):
|
|
317
|
+
if asyncio.iscoroutinefunction(handler):
|
|
318
|
+
return await handler(request, exc)
|
|
319
|
+
else:
|
|
320
|
+
return handler(request, exc)
|
|
321
|
+
# Fallback: prevent unhandled exceptions from leaking to the client
|
|
322
|
+
return internal_server_error_response()
|
|
323
|
+
|
|
324
|
+
# Register the route with Starlette
|
|
325
|
+
route = Route(path, endpoint=handler, methods=[method])
|
|
326
|
+
self._router.routes.append(route)
|
|
327
|
+
self.routes.append(
|
|
328
|
+
{"path": path, "method": method, "func": endpoint_func, **kwargs}
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Generate OpenAPI documentation for this route
|
|
332
|
+
include_in_schema = kwargs.get("include_in_schema", True)
|
|
333
|
+
if include_in_schema:
|
|
334
|
+
self._generate_openapi_for_route(path, method, endpoint_func, **kwargs)
|
|
335
|
+
|
|
336
|
+
def _generate_openapi_for_route(
|
|
337
|
+
self, path: str, method: str, endpoint_func: Callable, **kwargs
|
|
338
|
+
):
|
|
339
|
+
"""
|
|
340
|
+
Generate OpenAPI documentation for a specific route.
|
|
341
|
+
|
|
342
|
+
This method analyzes the endpoint function signature and generates appropriate
|
|
343
|
+
OpenAPI schema entries for parameters, request body, and responses.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
path: URL path pattern
|
|
347
|
+
method: HTTP method
|
|
348
|
+
endpoint_func: The endpoint function
|
|
349
|
+
**kwargs: Additional route metadata (summary, description, tags, etc.)
|
|
350
|
+
"""
|
|
351
|
+
sig = inspect.signature(endpoint_func)
|
|
352
|
+
|
|
353
|
+
# Ensure common error schemas exist in components
|
|
354
|
+
self.openapi_generator.add_schema(
|
|
355
|
+
"ValidationErrorResponse",
|
|
356
|
+
{
|
|
357
|
+
"type": "object",
|
|
358
|
+
"properties": {
|
|
359
|
+
"success": {"type": "boolean"},
|
|
360
|
+
"error": {"type": "string"},
|
|
361
|
+
"code": {"type": "string"},
|
|
362
|
+
"errors": {
|
|
363
|
+
"type": "object",
|
|
364
|
+
"additionalProperties": {
|
|
365
|
+
"type": "array",
|
|
366
|
+
"items": {"type": "string"},
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
"required": ["success", "error", "code"],
|
|
371
|
+
},
|
|
372
|
+
)
|
|
373
|
+
self.openapi_generator.add_schema(
|
|
374
|
+
"ResponseValidationError",
|
|
375
|
+
{
|
|
376
|
+
"type": "object",
|
|
377
|
+
"properties": {
|
|
378
|
+
"success": {"type": "boolean"},
|
|
379
|
+
"error": {"type": "string"},
|
|
380
|
+
"detail": {"type": "string"},
|
|
381
|
+
"code": {"type": "string"},
|
|
382
|
+
},
|
|
383
|
+
"required": ["success", "error", "code"],
|
|
384
|
+
},
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Build the OpenAPI operation object
|
|
388
|
+
operation = {
|
|
389
|
+
"summary": kwargs.get(
|
|
390
|
+
"summary", self._generate_summary_from_function(endpoint_func)
|
|
391
|
+
),
|
|
392
|
+
"description": kwargs.get("description", endpoint_func.__doc__ or ""),
|
|
393
|
+
"responses": {
|
|
394
|
+
"200": {
|
|
395
|
+
"description": "Successful Response",
|
|
396
|
+
"content": {"application/json": {"schema": {"type": "object"}}},
|
|
397
|
+
},
|
|
398
|
+
"422": {
|
|
399
|
+
"description": "Validation Error",
|
|
400
|
+
"content": {
|
|
401
|
+
"application/json": {
|
|
402
|
+
"schema": {
|
|
403
|
+
"$ref": "#/components/schemas/ValidationErrorResponse"
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
"500": {
|
|
409
|
+
"description": "Response Validation Error",
|
|
410
|
+
"content": {
|
|
411
|
+
"application/json": {
|
|
412
|
+
"schema": {
|
|
413
|
+
"$ref": "#/components/schemas/ResponseValidationError"
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
# If a response_model is provided and is a Struct, use it for the 200 response schema
|
|
422
|
+
response_model = kwargs.get("response_model")
|
|
423
|
+
if response_model is not None and issubclass(response_model, Struct):
|
|
424
|
+
from .openapi import build_components_for_struct
|
|
425
|
+
|
|
426
|
+
comps = build_components_for_struct(response_model)
|
|
427
|
+
for name, schema in comps.items():
|
|
428
|
+
self.openapi_generator.add_schema(name, schema)
|
|
429
|
+
operation["responses"]["200"]["content"]["application/json"]["schema"] = {
|
|
430
|
+
"$ref": f"#/components/schemas/{response_model.__name__}"
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
# Add tags if provided
|
|
434
|
+
if "tags" in kwargs:
|
|
435
|
+
operation["tags"] = kwargs["tags"]
|
|
436
|
+
|
|
437
|
+
# Process parameters from function signature
|
|
438
|
+
parameters = []
|
|
439
|
+
request_body_schema = None
|
|
440
|
+
|
|
441
|
+
for param in sig.parameters.values():
|
|
442
|
+
# Skip dependency parameters
|
|
443
|
+
if isinstance(param.default, Depends) or (
|
|
444
|
+
param.default is inspect.Parameter.empty
|
|
445
|
+
and param.annotation in _registry
|
|
446
|
+
):
|
|
447
|
+
continue
|
|
448
|
+
|
|
449
|
+
# Process query parameters
|
|
450
|
+
elif isinstance(param.default, Query):
|
|
451
|
+
parameters.append(
|
|
452
|
+
{
|
|
453
|
+
"name": param.name,
|
|
454
|
+
"in": "query",
|
|
455
|
+
"required": param.default.default is ...,
|
|
456
|
+
"schema": self._build_param_openapi_schema(param.annotation),
|
|
457
|
+
"description": getattr(param.default, "description", ""),
|
|
458
|
+
}
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# Process header parameters
|
|
462
|
+
elif isinstance(param.default, Header):
|
|
463
|
+
parameters.append(
|
|
464
|
+
{
|
|
465
|
+
"name": param.name,
|
|
466
|
+
"in": "header",
|
|
467
|
+
"required": param.default.default is ...,
|
|
468
|
+
"schema": self._build_param_openapi_schema(param.annotation),
|
|
469
|
+
"description": getattr(param.default, "description", ""),
|
|
470
|
+
}
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# Process cookie parameters
|
|
474
|
+
elif isinstance(param.default, Cookie):
|
|
475
|
+
parameters.append(
|
|
476
|
+
{
|
|
477
|
+
"name": param.name,
|
|
478
|
+
"in": "cookie",
|
|
479
|
+
"required": param.default.default is ...,
|
|
480
|
+
"schema": self._build_param_openapi_schema(param.annotation),
|
|
481
|
+
"description": getattr(param.default, "description", ""),
|
|
482
|
+
}
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Process path parameters
|
|
486
|
+
elif isinstance(param.default, Path) or self._is_path_parameter(
|
|
487
|
+
param.name, path
|
|
488
|
+
):
|
|
489
|
+
parameters.append(
|
|
490
|
+
{
|
|
491
|
+
"name": param.name,
|
|
492
|
+
"in": "path",
|
|
493
|
+
"required": True,
|
|
494
|
+
"schema": self._build_param_openapi_schema(param.annotation),
|
|
495
|
+
"description": getattr(param.default, "description", "")
|
|
496
|
+
if isinstance(param.default, Path)
|
|
497
|
+
else "",
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Process body parameters
|
|
502
|
+
elif isinstance(param.default, Body):
|
|
503
|
+
model_class = param.annotation
|
|
504
|
+
if issubclass(model_class, Struct):
|
|
505
|
+
from .openapi import build_components_for_struct
|
|
506
|
+
|
|
507
|
+
comps = build_components_for_struct(model_class)
|
|
508
|
+
for name, schema in comps.items():
|
|
509
|
+
self.openapi_generator.add_schema(name, schema)
|
|
510
|
+
|
|
511
|
+
request_body_schema = {
|
|
512
|
+
"content": {
|
|
513
|
+
"application/json": {
|
|
514
|
+
"schema": {
|
|
515
|
+
"$ref": f"#/components/schemas/{model_class.__name__}"
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
"required": True,
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
# Add parameters to operation if any exist
|
|
523
|
+
if parameters:
|
|
524
|
+
operation["parameters"] = parameters
|
|
525
|
+
|
|
526
|
+
if request_body_schema:
|
|
527
|
+
operation["requestBody"] = request_body_schema
|
|
528
|
+
|
|
529
|
+
self.openapi_generator.add_path(path, method, operation)
|
|
530
|
+
|
|
531
|
+
@staticmethod
|
|
532
|
+
def _generate_summary_from_function(func: Callable) -> str:
|
|
533
|
+
"""Generate a human-readable summary from function name."""
|
|
534
|
+
return func.__name__.replace("_", " ").title()
|
|
535
|
+
|
|
536
|
+
@staticmethod
|
|
537
|
+
def _is_path_parameter(param_name: str, path: str) -> bool:
|
|
538
|
+
"""Check if a parameter name corresponds to a path parameter in the URL."""
|
|
539
|
+
return f"{{{param_name}}}" in path
|
|
540
|
+
|
|
541
|
+
@staticmethod
|
|
542
|
+
def _build_param_openapi_schema(python_type: Type) -> Dict[str, Any]:
|
|
543
|
+
"""Build OpenAPI schema for parameter types, supporting Optional[T] and List[T]."""
|
|
544
|
+
# Use centralized TypeUtils for type checking
|
|
545
|
+
inner_type, nullable = TypeUtils.unwrap_optional(python_type)
|
|
546
|
+
|
|
547
|
+
# Check if it's a List type
|
|
548
|
+
is_list, item_type = TypeUtils.is_list_type(inner_type)
|
|
549
|
+
if is_list:
|
|
550
|
+
# Check if item type is Optional
|
|
551
|
+
base_item_type, item_nullable = TypeUtils.unwrap_optional(item_type)
|
|
552
|
+
schema = {
|
|
553
|
+
"type": "array",
|
|
554
|
+
"items": {"type": TypeUtils.get_openapi_type(base_item_type)},
|
|
555
|
+
}
|
|
556
|
+
if item_nullable:
|
|
557
|
+
schema["items"]["nullable"] = True
|
|
558
|
+
else:
|
|
559
|
+
schema = {"type": TypeUtils.get_openapi_type(inner_type)}
|
|
560
|
+
|
|
561
|
+
if nullable:
|
|
562
|
+
schema["nullable"] = True
|
|
563
|
+
return schema
|
|
564
|
+
|
|
565
|
+
def _setup_docs(self):
|
|
566
|
+
"""
|
|
567
|
+
Setup OpenAPI documentation endpoints.
|
|
568
|
+
|
|
569
|
+
This method registers the routes for serving OpenAPI JSON schema,
|
|
570
|
+
Swagger UI, and ReDoc documentation interfaces.
|
|
571
|
+
"""
|
|
572
|
+
if self._docs_setup:
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
self._docs_setup = True
|
|
576
|
+
|
|
577
|
+
# OpenAPI JSON schema endpoint
|
|
578
|
+
@self.get(self.openapi_config.openapi_url, include_in_schema=False)
|
|
579
|
+
def get_openapi_schema():
|
|
580
|
+
"""Serve the OpenAPI JSON schema."""
|
|
581
|
+
return self.openapi_generator.get_openapi_schema()
|
|
582
|
+
|
|
583
|
+
# Scalar API Reference documentation endpoint (default for /docs)
|
|
584
|
+
@self.get(self.openapi_config.docs_url, include_in_schema=False)
|
|
585
|
+
def get_scalar_docs():
|
|
586
|
+
"""Serve the Scalar API Reference documentation interface."""
|
|
587
|
+
html = self.openapi_generator.get_scalar_html(
|
|
588
|
+
self.openapi_config.openapi_url, self.openapi_config.info.title
|
|
589
|
+
)
|
|
590
|
+
return HTMLResponse(html)
|
|
591
|
+
|
|
592
|
+
# Swagger UI documentation endpoint (legacy support)
|
|
593
|
+
@self.get("/swagger", include_in_schema=False)
|
|
594
|
+
def get_swagger_ui():
|
|
595
|
+
"""Serve the Swagger UI documentation interface."""
|
|
596
|
+
html = self.openapi_generator.get_swagger_ui_html(
|
|
597
|
+
self.openapi_config.openapi_url, self.openapi_config.info.title
|
|
598
|
+
)
|
|
599
|
+
return HTMLResponse(html)
|
|
600
|
+
|
|
601
|
+
# ReDoc documentation endpoint
|
|
602
|
+
@self.get(self.openapi_config.redoc_url, include_in_schema=False)
|
|
603
|
+
def get_redoc():
|
|
604
|
+
"""Serve the ReDoc documentation interface."""
|
|
605
|
+
html = self.openapi_generator.get_redoc_html(
|
|
606
|
+
self.openapi_config.openapi_url, self.openapi_config.info.title
|
|
607
|
+
)
|
|
608
|
+
return HTMLResponse(html)
|
|
609
|
+
|
|
610
|
+
async def __call__(self, scope, receive, send):
|
|
611
|
+
"""
|
|
612
|
+
ASGI application entry point.
|
|
613
|
+
|
|
614
|
+
Delegates request handling to the internal Starlette application.
|
|
615
|
+
This makes Tachyon compatible with ASGI servers like Uvicorn.
|
|
616
|
+
"""
|
|
617
|
+
# Setup documentation endpoints on first request
|
|
618
|
+
if not self._docs_setup:
|
|
619
|
+
self._setup_docs()
|
|
620
|
+
await self._router(scope, receive, send)
|
|
621
|
+
|
|
622
|
+
def include_router(self, router, **kwargs):
|
|
623
|
+
"""
|
|
624
|
+
Include a Router instance in the application.
|
|
625
|
+
|
|
626
|
+
This method registers all routes from the router with the main application,
|
|
627
|
+
applying the router's prefix, tags, and dependencies.
|
|
628
|
+
|
|
629
|
+
Args:
|
|
630
|
+
router: The Router instance to include
|
|
631
|
+
**kwargs: Additional options (currently reserved for future use)
|
|
632
|
+
"""
|
|
633
|
+
from .router import Router
|
|
634
|
+
|
|
635
|
+
if not isinstance(router, Router):
|
|
636
|
+
raise TypeError("Expected Router instance")
|
|
637
|
+
|
|
638
|
+
# Register all routes from the router
|
|
639
|
+
for route_info in router.routes:
|
|
640
|
+
# Get the full path with prefix
|
|
641
|
+
full_path = router.get_full_path(route_info["path"])
|
|
642
|
+
|
|
643
|
+
# Check if it's a WebSocket route
|
|
644
|
+
if route_info.get("is_websocket"):
|
|
645
|
+
self._websocket_manager.add_websocket_route(full_path, route_info["func"])
|
|
646
|
+
continue
|
|
647
|
+
|
|
648
|
+
# Create a copy of route info with the full path
|
|
649
|
+
route_kwargs = route_info.copy()
|
|
650
|
+
route_kwargs.pop("path", None)
|
|
651
|
+
route_kwargs.pop("method", None)
|
|
652
|
+
route_kwargs.pop("func", None)
|
|
653
|
+
route_kwargs.pop("is_websocket", None)
|
|
654
|
+
|
|
655
|
+
# Register the route with the main app
|
|
656
|
+
self._add_route(
|
|
657
|
+
full_path, route_info["func"], route_info["method"], **route_kwargs
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
def add_middleware(self, middleware_class, **options):
|
|
661
|
+
"""
|
|
662
|
+
Adds a middleware to the application's stack.
|
|
663
|
+
|
|
664
|
+
Middlewares are processed in the order they are added. They follow
|
|
665
|
+
the ASGI middleware specification.
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
middleware_class: The middleware class.
|
|
669
|
+
**options: Options to be passed to the middleware constructor.
|
|
670
|
+
"""
|
|
671
|
+
# Use centralized helper to apply middleware to internal Starlette app
|
|
672
|
+
apply_middleware_to_router(self._router, middleware_class, **options)
|
|
673
|
+
|
|
674
|
+
if not hasattr(self, "middleware_stack"):
|
|
675
|
+
self.middleware_stack = []
|
|
676
|
+
self.middleware_stack.append({"func": middleware_class, "options": options})
|
|
677
|
+
|
|
678
|
+
def middleware(self, middleware_type="http"):
|
|
679
|
+
"""
|
|
680
|
+
Decorator for adding a middleware to the application.
|
|
681
|
+
Similar to route decorators (@app.get, etc.)
|
|
682
|
+
|
|
683
|
+
Args:
|
|
684
|
+
middleware_type: Type of middleware ('http' by default)
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
A decorator that registers the decorated function as middleware.
|
|
688
|
+
"""
|
|
689
|
+
|
|
690
|
+
def decorator(middleware_func):
|
|
691
|
+
# Create a middleware class from the decorated function
|
|
692
|
+
DecoratedMiddleware = create_decorated_middleware_class(
|
|
693
|
+
middleware_func, middleware_type
|
|
694
|
+
)
|
|
695
|
+
# Register the middleware using the existing method
|
|
696
|
+
self.add_middleware(DecoratedMiddleware)
|
|
697
|
+
return middleware_func
|
|
698
|
+
|
|
699
|
+
return decorator
|