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.

@@ -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