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.
Files changed (44) hide show
  1. tachyon_api/__init__.py +59 -0
  2. tachyon_api/app.py +699 -0
  3. tachyon_api/background.py +72 -0
  4. tachyon_api/cache.py +270 -0
  5. tachyon_api/cli/__init__.py +9 -0
  6. tachyon_api/cli/__main__.py +8 -0
  7. tachyon_api/cli/commands/__init__.py +5 -0
  8. tachyon_api/cli/commands/generate.py +190 -0
  9. tachyon_api/cli/commands/lint.py +186 -0
  10. tachyon_api/cli/commands/new.py +82 -0
  11. tachyon_api/cli/commands/openapi.py +128 -0
  12. tachyon_api/cli/main.py +69 -0
  13. tachyon_api/cli/templates/__init__.py +8 -0
  14. tachyon_api/cli/templates/project.py +194 -0
  15. tachyon_api/cli/templates/service.py +330 -0
  16. tachyon_api/core/__init__.py +12 -0
  17. tachyon_api/core/lifecycle.py +106 -0
  18. tachyon_api/core/websocket.py +92 -0
  19. tachyon_api/di.py +86 -0
  20. tachyon_api/exceptions.py +39 -0
  21. tachyon_api/files.py +14 -0
  22. tachyon_api/middlewares/__init__.py +4 -0
  23. tachyon_api/middlewares/core.py +40 -0
  24. tachyon_api/middlewares/cors.py +159 -0
  25. tachyon_api/middlewares/logger.py +123 -0
  26. tachyon_api/models.py +73 -0
  27. tachyon_api/openapi.py +419 -0
  28. tachyon_api/params.py +268 -0
  29. tachyon_api/processing/__init__.py +14 -0
  30. tachyon_api/processing/dependencies.py +172 -0
  31. tachyon_api/processing/parameters.py +484 -0
  32. tachyon_api/processing/response_processor.py +93 -0
  33. tachyon_api/responses.py +92 -0
  34. tachyon_api/router.py +161 -0
  35. tachyon_api/security.py +295 -0
  36. tachyon_api/testing.py +110 -0
  37. tachyon_api/utils/__init__.py +15 -0
  38. tachyon_api/utils/type_converter.py +113 -0
  39. tachyon_api/utils/type_utils.py +162 -0
  40. tachyon_api-0.9.0.dist-info/METADATA +291 -0
  41. tachyon_api-0.9.0.dist-info/RECORD +44 -0
  42. tachyon_api-0.9.0.dist-info/WHEEL +4 -0
  43. tachyon_api-0.9.0.dist-info/entry_points.txt +3 -0
  44. 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