tachyon-api 0.5.11__py3-none-any.whl → 0.6.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.

Potentially problematic release.


This version of tachyon-api might be problematic. Click here for more details.

tachyon_api/app.py DELETED
@@ -1,820 +0,0 @@
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
- import typing
15
-
16
- from starlette.applications import Starlette
17
- from starlette.responses import HTMLResponse, JSONResponse, Response
18
- from starlette.routing import Route
19
-
20
- from .di import Depends, _registry
21
- from .models import Struct
22
- from .openapi import (
23
- OpenAPIGenerator,
24
- OpenAPIConfig,
25
- create_openapi_config,
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
- from .responses import (
33
- TachyonJSONResponse,
34
- validation_error_response,
35
- response_validation_error_response,
36
- )
37
-
38
- from .utils import TypeConverter, TypeUtils
39
-
40
- from .responses import internal_server_error_response
41
-
42
- try:
43
- from .cache import set_cache_config
44
- except ImportError:
45
- set_cache_config = None # type: ignore
46
-
47
-
48
- class Tachyon:
49
- """
50
- Main Tachyon application class.
51
-
52
- Provides a web framework with automatic parameter validation, dependency injection,
53
- and type conversion. Built on top of Starlette for ASGI compatibility.
54
-
55
- Attributes:
56
- _router: Internal Starlette application instance
57
- routes: List of registered routes for introspection
58
- _instances_cache: Cache for dependency injection singleton instances
59
- openapi_config: Configuration for OpenAPI documentation
60
- openapi_generator: Generator for OpenAPI schema and documentation
61
- """
62
-
63
- def __init__(self, openapi_config: OpenAPIConfig = None, cache_config=None):
64
- """
65
- Initialize a new Tachyon application instance.
66
-
67
- Args:
68
- openapi_config: Optional OpenAPI configuration. If not provided,
69
- uses default configuration similar to FastAPI.
70
- cache_config: Optional cache configuration (tachyon_api.cache.CacheConfig).
71
- If provided, it will be set as the active cache configuration.
72
- """
73
- self._router = Starlette()
74
- self.routes = []
75
- self.middleware_stack = []
76
- self._instances_cache: Dict[Type, Any] = {}
77
-
78
- # Initialize OpenAPI configuration and generator
79
- self.openapi_config = openapi_config or create_openapi_config()
80
- self.openapi_generator = OpenAPIGenerator(self.openapi_config)
81
- self._docs_setup = False
82
-
83
- # Apply cache configuration if provided
84
- self.cache_config = cache_config
85
- if cache_config is not None and set_cache_config is not None:
86
- try:
87
- set_cache_config(cache_config)
88
- except Exception:
89
- # Do not break app initialization if cache setup fails
90
- pass
91
-
92
- # Dynamically create HTTP method decorators (get, post, put, delete, etc.)
93
- http_methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]
94
-
95
- for method in http_methods:
96
- setattr(
97
- self,
98
- method.lower(),
99
- partial(self._create_decorator, http_method=method),
100
- )
101
-
102
- def _resolve_dependency(self, cls: Type) -> Any:
103
- """
104
- Resolve a dependency and its sub-dependencies recursively.
105
-
106
- This method implements dependency injection with singleton pattern,
107
- automatically resolving constructor dependencies and caching instances.
108
-
109
- Args:
110
- cls: The class type to resolve and instantiate
111
-
112
- Returns:
113
- An instance of the requested class with all dependencies resolved
114
-
115
- Raises:
116
- TypeError: If the class cannot be instantiated or is not marked as injectable
117
-
118
- Note:
119
- - Uses singleton pattern - instances are cached and reused
120
- - Supports both @injectable decorated classes and simple classes
121
- - Recursively resolves constructor dependencies
122
- """
123
- # Return cached instance if available (singleton pattern)
124
- if cls in self._instances_cache:
125
- return self._instances_cache[cls]
126
-
127
- # For non-injectable classes, try to create without arguments
128
- if cls not in _registry:
129
- try:
130
- # Works for classes without __init__ or with no-arg __init__
131
- return cls()
132
- except TypeError:
133
- raise TypeError(
134
- f"Cannot resolve dependency '{cls.__name__}'. "
135
- f"Did you forget to mark it with @injectable?"
136
- )
137
-
138
- # For injectable classes, resolve constructor dependencies
139
- sig = inspect.signature(cls)
140
- dependencies = {}
141
-
142
- # Recursively resolve each constructor parameter
143
- for param in sig.parameters.values():
144
- if param.name != "self":
145
- dependencies[param.name] = self._resolve_dependency(param.annotation)
146
-
147
- # Create instance with resolved dependencies and cache it
148
- instance = cls(**dependencies)
149
- self._instances_cache[cls] = instance
150
- return instance
151
-
152
- def _create_decorator(self, path: str, *, http_method: str, **kwargs):
153
- """
154
- Create a decorator for the specified HTTP method.
155
-
156
- This factory method creates method-specific decorators (e.g., @app.get, @app.post)
157
- that register endpoint functions with the application.
158
-
159
- Args:
160
- path: URL path pattern (supports path parameters with {param} syntax)
161
- http_method: HTTP method name (GET, POST, PUT, DELETE, etc.)
162
-
163
- Returns:
164
- A decorator function that registers the endpoint
165
- """
166
-
167
- def decorator(endpoint_func: Callable):
168
- self._add_route(path, endpoint_func, http_method, **kwargs)
169
- return endpoint_func
170
-
171
- return decorator
172
-
173
- def _add_route(self, path: str, endpoint_func: Callable, method: str, **kwargs):
174
- """
175
- Register a route with the application and create an async handler.
176
-
177
- This is the core method that handles parameter injection, validation, and
178
- type conversion. It creates an async handler that processes requests and
179
- automatically injects dependencies, path parameters, query parameters, and
180
- request body data into the endpoint function.
181
-
182
- Args:
183
- path: URL path pattern (e.g., "/users/{user_id}")
184
- endpoint_func: The endpoint function to handle requests
185
- method: HTTP method (GET, POST, PUT, DELETE, etc.)
186
-
187
- Note:
188
- The created handler processes parameters in the following order:
189
- 1. Dependencies (explicit with Depends() or implicit via @injectable)
190
- 2. Body parameters (JSON request body validated against Struct models)
191
- 3. Query parameters (URL query string with type conversion)
192
- 4. Path parameters (both explicit with Path() and implicit from URL)
193
- """
194
-
195
- response_model = kwargs.get("response_model")
196
-
197
- async def handler(request):
198
- """
199
- Async request handler that processes parameters and calls the endpoint.
200
-
201
- This handler analyzes the endpoint function signature and automatically
202
- injects the appropriate values based on parameter annotations and defaults.
203
- """
204
- try:
205
- kwargs_to_inject = {}
206
- sig = inspect.signature(endpoint_func)
207
- query_params = request.query_params
208
- path_params = request.path_params
209
- _raw_body = None
210
-
211
- # Process each parameter in the endpoint function signature
212
- for param in sig.parameters.values():
213
- # Determine if this parameter is a dependency
214
- is_explicit_dependency = isinstance(param.default, Depends)
215
- is_implicit_dependency = (
216
- param.default is inspect.Parameter.empty
217
- and param.annotation in _registry
218
- )
219
-
220
- # Process dependencies (explicit and implicit)
221
- if is_explicit_dependency or is_implicit_dependency:
222
- target_class = param.annotation
223
- kwargs_to_inject[param.name] = self._resolve_dependency(
224
- target_class
225
- )
226
-
227
- # Process Body parameters (JSON request body)
228
- elif isinstance(param.default, Body):
229
- model_class = param.annotation
230
- if not issubclass(model_class, Struct):
231
- raise TypeError(
232
- "Body type must be an instance of Tachyon_api.models.Struct"
233
- )
234
-
235
- decoder = msgspec.json.Decoder(model_class)
236
- try:
237
- if _raw_body is None:
238
- _raw_body = await request.body()
239
- validated_data = decoder.decode(_raw_body)
240
- kwargs_to_inject[param.name] = validated_data
241
- except msgspec.ValidationError as e:
242
- # Attempt to build field errors map using e.path
243
- field_errors = None
244
- try:
245
- path = getattr(e, "path", None)
246
- if path:
247
- # Choose last string-ish path element as field name
248
- field_name = None
249
- for p in reversed(path):
250
- if isinstance(p, str):
251
- field_name = p
252
- break
253
- if field_name:
254
- field_errors = {field_name: [str(e)]}
255
- except Exception:
256
- field_errors = None
257
- return validation_error_response(
258
- str(e), errors=field_errors
259
- )
260
-
261
- # Process Query parameters (URL query string)
262
- elif isinstance(param.default, Query):
263
- query_info = param.default
264
- param_name = param.name
265
-
266
- # Determine typing for advanced cases
267
- ann = param.annotation
268
- origin = typing.get_origin(ann)
269
- args = typing.get_args(ann)
270
-
271
- # List[T] handling
272
- if origin in (list, typing.List):
273
- item_type = args[0] if args else str
274
- values = []
275
- # collect repeated params
276
- if hasattr(query_params, "getlist"):
277
- values = query_params.getlist(param_name)
278
- # if not repeated, check for CSV in single value
279
- if not values and param_name in query_params:
280
- raw = query_params[param_name]
281
- values = raw.split(",") if "," in raw else [raw]
282
- # flatten CSV in any element
283
- flat_values = []
284
- for v in values:
285
- if isinstance(v, str) and "," in v:
286
- flat_values.extend(v.split(","))
287
- else:
288
- flat_values.append(v)
289
- values = flat_values
290
- if not values:
291
- if query_info.default is not ...:
292
- kwargs_to_inject[param_name] = query_info.default
293
- continue
294
- return validation_error_response(
295
- f"Missing required query parameter: {param_name}"
296
- )
297
- # Unwrap Optional for item type
298
- base_item_type, item_is_opt = TypeUtils.unwrap_optional(
299
- item_type
300
- )
301
- converted_list = []
302
- for v in values:
303
- if item_is_opt and (v == "" or v.lower() == "null"):
304
- converted_list.append(None)
305
- continue
306
- converted_value = TypeConverter.convert_value(
307
- v, base_item_type, param_name, is_path_param=False
308
- )
309
- if isinstance(converted_value, JSONResponse):
310
- return converted_value
311
- converted_list.append(converted_value)
312
- kwargs_to_inject[param_name] = converted_list
313
- continue
314
-
315
- # Optional[T] handling for single value
316
- base_type, _is_opt = TypeUtils.unwrap_optional(ann)
317
-
318
- if param_name in query_params:
319
- value_str = query_params[param_name]
320
- converted_value = TypeConverter.convert_value(
321
- value_str, base_type, param_name, is_path_param=False
322
- )
323
- if isinstance(converted_value, JSONResponse):
324
- return converted_value
325
- kwargs_to_inject[param_name] = converted_value
326
-
327
- elif query_info.default is not ...:
328
- kwargs_to_inject[param.name] = query_info.default
329
- else:
330
- return validation_error_response(
331
- f"Missing required query parameter: {param_name}"
332
- )
333
-
334
- # Process explicit Path parameters (with Path() annotation)
335
- elif isinstance(param.default, Path):
336
- param_name = param.name
337
- if param_name in path_params:
338
- value_str = path_params[param_name]
339
- # Support List[T] in path params via CSV
340
- ann = param.annotation
341
- origin = typing.get_origin(ann)
342
- args = typing.get_args(ann)
343
- if origin in (list, typing.List):
344
- item_type = args[0] if args else str
345
- parts = value_str.split(",") if value_str else []
346
- # Unwrap Optional for item type
347
- base_item_type, item_is_opt = TypeUtils.unwrap_optional(
348
- item_type
349
- )
350
- converted_list = []
351
- for v in parts:
352
- if item_is_opt and (v == "" or v.lower() == "null"):
353
- converted_list.append(None)
354
- continue
355
- converted_value = TypeConverter.convert_value(
356
- v,
357
- base_item_type,
358
- param_name,
359
- is_path_param=True,
360
- )
361
- if isinstance(converted_value, JSONResponse):
362
- return converted_value
363
- converted_list.append(converted_value)
364
- kwargs_to_inject[param_name] = converted_list
365
- else:
366
- converted_value = TypeConverter.convert_value(
367
- value_str, ann, param_name, is_path_param=True
368
- )
369
- # Return 404 if conversion failed
370
- if isinstance(converted_value, JSONResponse):
371
- return converted_value
372
- kwargs_to_inject[param_name] = converted_value
373
- else:
374
- return JSONResponse(
375
- {"detail": "Not Found"}, status_code=404
376
- )
377
-
378
- # Process implicit Path parameters (URL path variables without Path())
379
- elif (
380
- param.default is inspect.Parameter.empty
381
- and param.name in path_params
382
- and not is_explicit_dependency
383
- and not is_implicit_dependency
384
- ):
385
- param_name = param.name
386
- value_str = path_params[param_name]
387
- # Support List[T] via CSV
388
- ann = param.annotation
389
- origin = typing.get_origin(ann)
390
- args = typing.get_args(ann)
391
- if origin in (list, typing.List):
392
- item_type = args[0] if args else str
393
- parts = value_str.split(",") if value_str else []
394
- # Unwrap Optional for item type
395
- base_item_type, item_is_opt = TypeUtils.unwrap_optional(
396
- item_type
397
- )
398
- converted_list = []
399
- for v in parts:
400
- if item_is_opt and (v == "" or v.lower() == "null"):
401
- converted_list.append(None)
402
- continue
403
- converted_value = TypeConverter.convert_value(
404
- v, base_item_type, param_name, is_path_param=True
405
- )
406
- if isinstance(converted_value, JSONResponse):
407
- return converted_value
408
- converted_list.append(converted_value)
409
- kwargs_to_inject[param_name] = converted_list
410
- else:
411
- converted_value = TypeConverter.convert_value(
412
- value_str, ann, param_name, is_path_param=True
413
- )
414
- # Return 404 if conversion failed
415
- if isinstance(converted_value, JSONResponse):
416
- return converted_value
417
- kwargs_to_inject[param_name] = converted_value
418
-
419
- # Call the endpoint function with injected parameters
420
- if asyncio.iscoroutinefunction(endpoint_func):
421
- payload = await endpoint_func(**kwargs_to_inject)
422
- else:
423
- payload = endpoint_func(**kwargs_to_inject)
424
-
425
- # If the endpoint already returned a Response object, return it directly
426
- if isinstance(payload, Response):
427
- return payload
428
-
429
- # Validate/convert response against response_model if provided
430
- if response_model is not None:
431
- try:
432
- payload = msgspec.convert(payload, response_model)
433
- except Exception as e:
434
- return response_validation_error_response(str(e))
435
-
436
- # Convert Struct objects to dictionaries for JSON serialization
437
- if isinstance(payload, Struct):
438
- payload = msgspec.to_builtins(payload)
439
- elif isinstance(payload, dict):
440
- # Convert any Struct values in the dictionary
441
- for key, value in payload.items():
442
- if isinstance(value, Struct):
443
- payload[key] = msgspec.to_builtins(value)
444
-
445
- return TachyonJSONResponse(payload)
446
- except Exception:
447
- # Fallback: prevent unhandled exceptions from leaking to the client
448
- return internal_server_error_response()
449
-
450
- # Register the route with Starlette
451
- route = Route(path, endpoint=handler, methods=[method])
452
- self._router.routes.append(route)
453
- self.routes.append(
454
- {"path": path, "method": method, "func": endpoint_func, **kwargs}
455
- )
456
-
457
- # Generate OpenAPI documentation for this route
458
- include_in_schema = kwargs.get("include_in_schema", True)
459
- if include_in_schema:
460
- self._generate_openapi_for_route(path, method, endpoint_func, **kwargs)
461
-
462
- def _generate_openapi_for_route(
463
- self, path: str, method: str, endpoint_func: Callable, **kwargs
464
- ):
465
- """
466
- Generate OpenAPI documentation for a specific route.
467
-
468
- This method analyzes the endpoint function signature and generates appropriate
469
- OpenAPI schema entries for parameters, request body, and responses.
470
-
471
- Args:
472
- path: URL path pattern
473
- method: HTTP method
474
- endpoint_func: The endpoint function
475
- **kwargs: Additional route metadata (summary, description, tags, etc.)
476
- """
477
- sig = inspect.signature(endpoint_func)
478
-
479
- # Ensure common error schemas exist in components
480
- self.openapi_generator.add_schema(
481
- "ValidationErrorResponse",
482
- {
483
- "type": "object",
484
- "properties": {
485
- "success": {"type": "boolean"},
486
- "error": {"type": "string"},
487
- "code": {"type": "string"},
488
- "errors": {
489
- "type": "object",
490
- "additionalProperties": {
491
- "type": "array",
492
- "items": {"type": "string"},
493
- },
494
- },
495
- },
496
- "required": ["success", "error", "code"],
497
- },
498
- )
499
- self.openapi_generator.add_schema(
500
- "ResponseValidationError",
501
- {
502
- "type": "object",
503
- "properties": {
504
- "success": {"type": "boolean"},
505
- "error": {"type": "string"},
506
- "detail": {"type": "string"},
507
- "code": {"type": "string"},
508
- },
509
- "required": ["success", "error", "code"],
510
- },
511
- )
512
-
513
- # Build the OpenAPI operation object
514
- operation = {
515
- "summary": kwargs.get(
516
- "summary", self._generate_summary_from_function(endpoint_func)
517
- ),
518
- "description": kwargs.get("description", endpoint_func.__doc__ or ""),
519
- "responses": {
520
- "200": {
521
- "description": "Successful Response",
522
- "content": {"application/json": {"schema": {"type": "object"}}},
523
- },
524
- "422": {
525
- "description": "Validation Error",
526
- "content": {
527
- "application/json": {
528
- "schema": {
529
- "$ref": "#/components/schemas/ValidationErrorResponse"
530
- }
531
- }
532
- },
533
- },
534
- "500": {
535
- "description": "Response Validation Error",
536
- "content": {
537
- "application/json": {
538
- "schema": {
539
- "$ref": "#/components/schemas/ResponseValidationError"
540
- }
541
- }
542
- },
543
- },
544
- },
545
- }
546
-
547
- # If a response_model is provided and is a Struct, use it for the 200 response schema
548
- response_model = kwargs.get("response_model")
549
- if response_model is not None and issubclass(response_model, Struct):
550
- from .openapi import build_components_for_struct
551
-
552
- comps = build_components_for_struct(response_model)
553
- for name, schema in comps.items():
554
- self.openapi_generator.add_schema(name, schema)
555
- operation["responses"]["200"]["content"]["application/json"]["schema"] = {
556
- "$ref": f"#/components/schemas/{response_model.__name__}"
557
- }
558
-
559
- # Add tags if provided
560
- if "tags" in kwargs:
561
- operation["tags"] = kwargs["tags"]
562
-
563
- # Process parameters from function signature
564
- parameters = []
565
- request_body_schema = None
566
-
567
- for param in sig.parameters.values():
568
- # Skip dependency parameters
569
- if isinstance(param.default, Depends) or (
570
- param.default is inspect.Parameter.empty
571
- and param.annotation in _registry
572
- ):
573
- continue
574
-
575
- # Process query parameters
576
- elif isinstance(param.default, Query):
577
- parameters.append(
578
- {
579
- "name": param.name,
580
- "in": "query",
581
- "required": param.default.default is ...,
582
- "schema": self._build_param_openapi_schema(param.annotation),
583
- "description": getattr(param.default, "description", ""),
584
- }
585
- )
586
-
587
- # Process path parameters
588
- elif isinstance(param.default, Path) or self._is_path_parameter(
589
- param.name, path
590
- ):
591
- parameters.append(
592
- {
593
- "name": param.name,
594
- "in": "path",
595
- "required": True,
596
- "schema": self._build_param_openapi_schema(param.annotation),
597
- "description": getattr(param.default, "description", "")
598
- if isinstance(param.default, Path)
599
- else "",
600
- }
601
- )
602
-
603
- # Process body parameters
604
- elif isinstance(param.default, Body):
605
- model_class = param.annotation
606
- if issubclass(model_class, Struct):
607
- from .openapi import build_components_for_struct
608
-
609
- comps = build_components_for_struct(model_class)
610
- for name, schema in comps.items():
611
- self.openapi_generator.add_schema(name, schema)
612
-
613
- request_body_schema = {
614
- "content": {
615
- "application/json": {
616
- "schema": {
617
- "$ref": f"#/components/schemas/{model_class.__name__}"
618
- }
619
- }
620
- },
621
- "required": True,
622
- }
623
-
624
- # Add parameters to operation if any exist
625
- if parameters:
626
- operation["parameters"] = parameters
627
-
628
- if request_body_schema:
629
- operation["requestBody"] = request_body_schema
630
-
631
- self.openapi_generator.add_path(path, method, operation)
632
-
633
- @staticmethod
634
- def _generate_summary_from_function(func: Callable) -> str:
635
- """Generate a human-readable summary from function name."""
636
- return func.__name__.replace("_", " ").title()
637
-
638
- @staticmethod
639
- def _is_path_parameter(param_name: str, path: str) -> bool:
640
- """Check if a parameter name corresponds to a path parameter in the URL."""
641
- return f"{{{param_name}}}" in path
642
-
643
- @staticmethod
644
- def _get_openapi_type(python_type: Type) -> str:
645
- """Convert Python type to OpenAPI schema type."""
646
- type_map: Dict[Type, str] = {
647
- int: "integer",
648
- str: "string",
649
- bool: "boolean",
650
- float: "number",
651
- }
652
- return type_map.get(python_type, "string")
653
-
654
- @staticmethod
655
- def _build_param_openapi_schema(python_type: Type) -> Dict[str, Any]:
656
- """Build OpenAPI schema for parameter types, supporting Optional[T] and List[T]."""
657
- origin = typing.get_origin(python_type)
658
- args = typing.get_args(python_type)
659
- nullable = False
660
- # Optional[T]
661
- if origin is Union and args:
662
- non_none = [a for a in args if a is not type(None)] # noqa: E721
663
- if len(non_none) == 1:
664
- python_type = non_none[0]
665
- nullable = True
666
- # List[T] (and List[Optional[T]])
667
- origin = typing.get_origin(python_type)
668
- args = typing.get_args(python_type)
669
- if origin in (list, typing.List):
670
- item_type = args[0] if args else str
671
- # Unwrap Optional in items for List[Optional[T]]
672
- item_origin = typing.get_origin(item_type)
673
- item_args = typing.get_args(item_type)
674
- item_nullable = False
675
- if item_origin is Union and item_args:
676
- item_non_none = [a for a in item_args if a is not type(None)] # noqa: E721
677
- if len(item_non_none) == 1:
678
- item_type = item_non_none[0]
679
- item_nullable = True
680
- schema = {
681
- "type": "array",
682
- "items": {"type": Tachyon._get_openapi_type(item_type)},
683
- }
684
- if item_nullable:
685
- schema["items"]["nullable"] = True
686
- else:
687
- schema = {"type": Tachyon._get_openapi_type(python_type)}
688
- if nullable:
689
- schema["nullable"] = True
690
- return schema
691
-
692
- def _setup_docs(self):
693
- """
694
- Setup OpenAPI documentation endpoints.
695
-
696
- This method registers the routes for serving OpenAPI JSON schema,
697
- Swagger UI, and ReDoc documentation interfaces.
698
- """
699
- if self._docs_setup:
700
- return
701
-
702
- self._docs_setup = True
703
-
704
- # OpenAPI JSON schema endpoint
705
- @self.get(self.openapi_config.openapi_url, include_in_schema=False)
706
- def get_openapi_schema():
707
- """Serve the OpenAPI JSON schema."""
708
- return self.openapi_generator.get_openapi_schema()
709
-
710
- # Scalar API Reference documentation endpoint (default for /docs)
711
- @self.get(self.openapi_config.docs_url, include_in_schema=False)
712
- def get_scalar_docs():
713
- """Serve the Scalar API Reference documentation interface."""
714
- html = self.openapi_generator.get_scalar_html(
715
- self.openapi_config.openapi_url, self.openapi_config.info.title
716
- )
717
- return HTMLResponse(html)
718
-
719
- # Swagger UI documentation endpoint (legacy support)
720
- @self.get("/swagger", include_in_schema=False)
721
- def get_swagger_ui():
722
- """Serve the Swagger UI documentation interface."""
723
- html = self.openapi_generator.get_swagger_ui_html(
724
- self.openapi_config.openapi_url, self.openapi_config.info.title
725
- )
726
- return HTMLResponse(html)
727
-
728
- # ReDoc documentation endpoint
729
- @self.get(self.openapi_config.redoc_url, include_in_schema=False)
730
- def get_redoc():
731
- """Serve the ReDoc documentation interface."""
732
- html = self.openapi_generator.get_redoc_html(
733
- self.openapi_config.openapi_url, self.openapi_config.info.title
734
- )
735
- return HTMLResponse(html)
736
-
737
- async def __call__(self, scope, receive, send):
738
- """
739
- ASGI application entry point.
740
-
741
- Delegates request handling to the internal Starlette application.
742
- This makes Tachyon compatible with ASGI servers like Uvicorn.
743
- """
744
- # Setup documentation endpoints on first request
745
- if not self._docs_setup:
746
- self._setup_docs()
747
- await self._router(scope, receive, send)
748
-
749
- def include_router(self, router, **kwargs):
750
- """
751
- Include a Router instance in the application.
752
-
753
- This method registers all routes from the router with the main application,
754
- applying the router's prefix, tags, and dependencies.
755
-
756
- Args:
757
- router: The Router instance to include
758
- **kwargs: Additional options (currently reserved for future use)
759
- """
760
- from .router import Router
761
-
762
- if not isinstance(router, Router):
763
- raise TypeError("Expected Router instance")
764
-
765
- # Register all routes from the router
766
- for route_info in router.routes:
767
- # Get the full path with prefix
768
- full_path = router.get_full_path(route_info["path"])
769
-
770
- # Create a copy of route info with the full path
771
- route_kwargs = route_info.copy()
772
- route_kwargs.pop("path", None)
773
- route_kwargs.pop("method", None)
774
- route_kwargs.pop("func", None)
775
-
776
- # Register the route with the main app
777
- self._add_route(
778
- full_path, route_info["func"], route_info["method"], **route_kwargs
779
- )
780
-
781
- def add_middleware(self, middleware_class, **options):
782
- """
783
- Adds a middleware to the application's stack.
784
-
785
- Middlewares are processed in the order they are added. They follow
786
- the ASGI middleware specification.
787
-
788
- Args:
789
- middleware_class: The middleware class.
790
- **options: Options to be passed to the middleware constructor.
791
- """
792
- # Use centralized helper to apply middleware to internal Starlette app
793
- apply_middleware_to_router(self._router, middleware_class, **options)
794
-
795
- if not hasattr(self, "middleware_stack"):
796
- self.middleware_stack = []
797
- self.middleware_stack.append({"func": middleware_class, "options": options})
798
-
799
- def middleware(self, middleware_type="http"):
800
- """
801
- Decorator for adding a middleware to the application.
802
- Similar to route decorators (@app.get, etc.)
803
-
804
- Args:
805
- middleware_type: Type of middleware ('http' by default)
806
-
807
- Returns:
808
- A decorator that registers the decorated function as middleware.
809
- """
810
-
811
- def decorator(middleware_func):
812
- # Create a middleware class from the decorated function
813
- DecoratedMiddleware = create_decorated_middleware_class(
814
- middleware_func, middleware_type
815
- )
816
- # Register the middleware using the existing method
817
- self.add_middleware(DecoratedMiddleware)
818
- return middleware_func
819
-
820
- return decorator