vayuapi 0.1.2__tar.gz → 0.1.4__tar.gz

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 (58) hide show
  1. {vayuapi-0.1.2 → vayuapi-0.1.4}/PKG-INFO +16 -6
  2. {vayuapi-0.1.2 → vayuapi-0.1.4}/pyproject.toml +23 -4
  3. {vayuapi-0.1.2 → vayuapi-0.1.4}/setup.py +2 -2
  4. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/__init__.py +23 -2
  5. vayuapi-0.1.4/vayuapi/di/__init__.py +23 -0
  6. vayuapi-0.1.4/vayuapi/di/container.py +311 -0
  7. vayuapi-0.1.4/vayuapi/di/injectable.py +136 -0
  8. vayuapi-0.1.4/vayuapi/di/provider.py +56 -0
  9. vayuapi-0.1.4/vayuapi/grpc/__init__.py +16 -0
  10. vayuapi-0.1.4/vayuapi/grpc/client.py +129 -0
  11. vayuapi-0.1.4/vayuapi/grpc/decorators.py +161 -0
  12. vayuapi-0.1.4/vayuapi/grpc/server.py +244 -0
  13. vayuapi-0.1.4/vayuapi/tasks/__init__.py +18 -0
  14. vayuapi-0.1.4/vayuapi/tasks/arq_app.py +352 -0
  15. vayuapi-0.1.4/vayuapi/tasks/celery_app.py +239 -0
  16. vayuapi-0.1.4/vayuapi/tasks/decorators.py +184 -0
  17. vayuapi-0.1.4/vayuapi/tasks/worker.py +103 -0
  18. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi.egg-info/SOURCES.txt +13 -0
  19. {vayuapi-0.1.2 → vayuapi-0.1.4}/LICENSE +0 -0
  20. {vayuapi-0.1.2 → vayuapi-0.1.4}/MANIFEST.in +0 -0
  21. {vayuapi-0.1.2 → vayuapi-0.1.4}/README.md +0 -0
  22. {vayuapi-0.1.2 → vayuapi-0.1.4}/requirements.txt +0 -0
  23. {vayuapi-0.1.2 → vayuapi-0.1.4}/setup.cfg +0 -0
  24. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/admin/__init__.py +0 -0
  25. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/admin/enhanced_panel.py +0 -0
  26. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/admin/panel.py +0 -0
  27. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/ai/__init__.py +0 -0
  28. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/ai/langchain.py +0 -0
  29. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/ai/pydantic_ai.py +0 -0
  30. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/__init__.py +0 -0
  31. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/application.py +0 -0
  32. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/concurrency.py +0 -0
  33. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/decorators.py +0 -0
  34. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/dependencies.py +0 -0
  35. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/docs.py +0 -0
  36. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/exceptions.py +0 -0
  37. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/middleware.py +0 -0
  38. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/params.py +0 -0
  39. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/responses.py +0 -0
  40. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/routing.py +0 -0
  41. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/staticfiles.py +0 -0
  42. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/templating.py +0 -0
  43. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/uploads.py +0 -0
  44. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/websocket.py +0 -0
  45. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/middleware/__init__.py +0 -0
  46. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/orm/__init__.py +0 -0
  47. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/orm/async_orm.py +0 -0
  48. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/orm/database.py +0 -0
  49. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/orm/django_orm.py +0 -0
  50. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/scheduler/__init__.py +0 -0
  51. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/scheduler/tasks.py +0 -0
  52. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/security/__init__.py +0 -0
  53. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/security/auth.py +0 -0
  54. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/security/encryption.py +0 -0
  55. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/security/jwt.py +0 -0
  56. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/status.py +0 -0
  57. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/utils/__init__.py +0 -0
  58. {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/utils/helpers.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vayuapi
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: The fastest Python async API framework for rapid development with AI/ML support
5
5
  Home-page: https://github.com/vayuapi/vayuapi
6
6
  Author: VayuAPI Team
7
- Author-email: VayuAPI Team <team@vayuapi.dev>
8
- Maintainer-email: VayuAPI Team <team@vayuapi.dev>
7
+ Author-email: VayuAPI Team <codeswithalok@gmail.com>
8
+ Maintainer-email: VayuAPI Team <codeswithalok@gmail.com>
9
9
  License: MIT
10
10
  Project-URL: Homepage, https://vayuapi.amrits.in
11
11
  Project-URL: Documentation, https://vayuapi.amrits.in/pages/documentation.html
@@ -88,8 +88,18 @@ Requires-Dist: sentence-transformers>=2.3.0; extra == "rag"
88
88
  Requires-Dist: tiktoken>=0.6.0; extra == "rag"
89
89
  Provides-Extra: scheduler
90
90
  Requires-Dist: apscheduler>=3.10.0; extra == "scheduler"
91
- Requires-Dist: celery>=5.3.0; extra == "scheduler"
92
- Requires-Dist: redis>=5.0.0; extra == "scheduler"
91
+ Provides-Extra: celery
92
+ Requires-Dist: celery>=5.3.0; extra == "celery"
93
+ Requires-Dist: redis>=5.0.0; extra == "celery"
94
+ Requires-Dist: kombu>=5.3.0; extra == "celery"
95
+ Provides-Extra: arq
96
+ Requires-Dist: arq>=0.25.0; extra == "arq"
97
+ Requires-Dist: redis>=5.0.0; extra == "arq"
98
+ Provides-Extra: grpc
99
+ Requires-Dist: grpcio>=1.60.0; extra == "grpc"
100
+ Requires-Dist: grpcio-tools>=1.60.0; extra == "grpc"
101
+ Requires-Dist: grpcio-reflection>=1.60.0; extra == "grpc"
102
+ Requires-Dist: protobuf>=4.25.0; extra == "grpc"
93
103
  Provides-Extra: security
94
104
  Requires-Dist: cryptography>=41.0.0; extra == "security"
95
105
  Requires-Dist: pyjwt>=2.8.0; extra == "security"
@@ -104,7 +114,7 @@ Requires-Dist: pytest-asyncio>=0.23.0; extra == "test"
104
114
  Requires-Dist: pytest-cov>=4.1.0; extra == "test"
105
115
  Requires-Dist: httpx>=0.26.0; extra == "test"
106
116
  Provides-Extra: all
107
- Requires-Dist: vayuapi[ai,django,http,nosql,orm,rag,scheduler,security,sqlalchemy,vector]; extra == "all"
117
+ Requires-Dist: vayuapi[ai,arq,celery,django,grpc,http,nosql,orm,rag,scheduler,security,sqlalchemy,vector]; extra == "all"
108
118
  Provides-Extra: dev
109
119
  Requires-Dist: vayuapi[test]; extra == "dev"
110
120
  Requires-Dist: black>=24.0.0; extra == "dev"
@@ -6,7 +6,7 @@ include = ["vayuapi*"]
6
6
 
7
7
  [project]
8
8
  name = "vayuapi"
9
- version = "0.1.2"
9
+ version = "0.1.4"
10
10
  description = "The fastest Python async API framework for rapid development with AI/ML support"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.11,<3.16"
@@ -17,10 +17,10 @@ keywords = [
17
17
  "ai", "ml", "langchain", "rag", "pydantic", "admin", "security"
18
18
  ]
19
19
  authors = [
20
- {name = "VayuAPI Team", email = "team@vayuapi.dev"}
20
+ {name = "VayuAPI Team", email = "codeswithalok@gmail.com"}
21
21
  ]
22
22
  maintainers = [
23
- {name = "VayuAPI Team", email = "team@vayuapi.dev"}
23
+ {name = "VayuAPI Team", email = "codeswithalok@gmail.com"}
24
24
  ]
25
25
  classifiers = [
26
26
  "Development Status :: 4 - Beta",
@@ -124,8 +124,27 @@ rag = [
124
124
  # Task scheduling
125
125
  scheduler = [
126
126
  "apscheduler>=3.10.0",
127
+ ]
128
+
129
+ # Celery distributed task queue
130
+ celery = [
127
131
  "celery>=5.3.0",
128
132
  "redis>=5.0.0",
133
+ "kombu>=5.3.0",
134
+ ]
135
+
136
+ # ARQ async task queue
137
+ arq = [
138
+ "arq>=0.25.0",
139
+ "redis>=5.0.0",
140
+ ]
141
+
142
+ # gRPC support
143
+ grpc = [
144
+ "grpcio>=1.60.0",
145
+ "grpcio-tools>=1.60.0",
146
+ "grpcio-reflection>=1.60.0",
147
+ "protobuf>=4.25.0",
129
148
  ]
130
149
 
131
150
  # Security
@@ -152,7 +171,7 @@ test = [
152
171
 
153
172
  # All dependencies
154
173
  all = [
155
- "vayuapi[django,orm,sqlalchemy,nosql,vector,ai,rag,scheduler,security,http]",
174
+ "vayuapi[django,orm,sqlalchemy,nosql,vector,ai,rag,scheduler,celery,arq,grpc,security,http]",
156
175
  ]
157
176
 
158
177
  # Development dependencies (testing + linting only)
@@ -7,9 +7,9 @@ with open("README.md", "r", encoding="utf-8") as fh:
7
7
 
8
8
  setup(
9
9
  name="vayuapi",
10
- version="0.1.2",
10
+ version="0.1.4",
11
11
  author="VayuAPI Team",
12
- author_email="alokk.gzp@gmail.com",
12
+ author_email="codeswithalok@gmail.com",
13
13
  description="The fastest Python async API framework for rapid development with AI/ML support",
14
14
  long_description=long_description,
15
15
  long_description_content_type="text/markdown",
@@ -26,9 +26,21 @@ from vayuapi.core.params import (
26
26
  File,
27
27
  )
28
28
 
29
- # Dependency injection
29
+ # Dependency injection (basic Depends/Security)
30
30
  from vayuapi.core.dependencies import Depends, Security
31
31
 
32
+ # IoC / DI Container
33
+ from vayuapi.di import (
34
+ DIContainer,
35
+ ServiceLifetime,
36
+ ServiceDescriptor,
37
+ ServiceScope,
38
+ ServiceProvider,
39
+ injectable,
40
+ inject,
41
+ )
42
+ from vayuapi.di.injectable import register_injectables
43
+
32
44
  # File uploads
33
45
  from vayuapi.core.uploads import UploadFile
34
46
 
@@ -107,9 +119,18 @@ __all__ = [
107
119
  "Body",
108
120
  "Form",
109
121
  "File",
110
- # Dependencies
122
+ # Dependency injection (basic)
111
123
  "Depends",
112
124
  "Security",
125
+ # IoC DI Container
126
+ "DIContainer",
127
+ "ServiceLifetime",
128
+ "ServiceDescriptor",
129
+ "ServiceScope",
130
+ "ServiceProvider",
131
+ "injectable",
132
+ "inject",
133
+ "register_injectables",
113
134
  # File uploads
114
135
  "UploadFile",
115
136
  # Concurrency & Low Overhead
@@ -0,0 +1,23 @@
1
+ """
2
+ VayuAPI Dependency Injection Container
3
+ Full IoC container with Singleton, Transient, and Scoped lifetimes.
4
+ """
5
+
6
+ from vayuapi.di.container import (
7
+ DIContainer,
8
+ ServiceLifetime,
9
+ ServiceDescriptor,
10
+ ServiceScope,
11
+ )
12
+ from vayuapi.di.injectable import injectable, inject
13
+ from vayuapi.di.provider import ServiceProvider
14
+
15
+ __all__ = [
16
+ "DIContainer",
17
+ "ServiceLifetime",
18
+ "ServiceDescriptor",
19
+ "ServiceScope",
20
+ "ServiceProvider",
21
+ "injectable",
22
+ "inject",
23
+ ]
@@ -0,0 +1,311 @@
1
+ """
2
+ DI Container — IoC container with Singleton, Transient, and Scoped lifetimes.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ import inspect
9
+ import typing
10
+ from enum import Enum
11
+ from typing import Any, Callable, Dict, Generic, List, Optional, Type, TypeVar
12
+
13
+ T = TypeVar("T")
14
+
15
+
16
+ class ServiceLifetime(Enum):
17
+ """Service lifetime options."""
18
+ SINGLETON = "singleton" # One instance for the entire application
19
+ TRANSIENT = "transient" # New instance every time
20
+ SCOPED = "scoped" # One instance per scope (e.g. per request)
21
+
22
+
23
+ class ServiceDescriptor:
24
+ """Describes how to create a service."""
25
+
26
+ def __init__(
27
+ self,
28
+ service_type: Type,
29
+ lifetime: ServiceLifetime,
30
+ implementation_type: Optional[Type] = None,
31
+ factory: Optional[Callable] = None,
32
+ instance: Optional[Any] = None,
33
+ ):
34
+ if sum([implementation_type is not None, factory is not None, instance is not None]) != 1:
35
+ raise ValueError("Exactly one of implementation_type, factory, or instance must be provided.")
36
+
37
+ self.service_type = service_type
38
+ self.lifetime = lifetime
39
+ self.implementation_type = implementation_type
40
+ self.factory = factory
41
+ self.instance = instance
42
+
43
+ def __repr__(self) -> str:
44
+ return (
45
+ f"ServiceDescriptor(type={self.service_type.__name__}, "
46
+ f"lifetime={self.lifetime.value})"
47
+ )
48
+
49
+
50
+ class ServiceScope:
51
+ """
52
+ A resolution scope. Scoped services are created once per scope.
53
+ Use as a context manager for per-request scopes.
54
+ """
55
+
56
+ def __init__(self, container: "DIContainer"):
57
+ self._container = container
58
+ self._scoped_instances: Dict[Type, Any] = {}
59
+ self._async_generators: List[Any] = []
60
+ self._sync_generators: List[Any] = []
61
+
62
+ async def resolve(self, service_type: Type[T]) -> T:
63
+ return await self._container._resolve(service_type, scope=self)
64
+
65
+ async def __aenter__(self) -> "ServiceScope":
66
+ return self
67
+
68
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
69
+ await self._cleanup()
70
+
71
+ async def _cleanup(self):
72
+ for gen in reversed(self._async_generators):
73
+ try:
74
+ await gen.aclose()
75
+ except Exception:
76
+ pass
77
+ for gen in reversed(self._sync_generators):
78
+ try:
79
+ gen.close()
80
+ except Exception:
81
+ pass
82
+ self._scoped_instances.clear()
83
+ self._async_generators.clear()
84
+ self._sync_generators.clear()
85
+
86
+
87
+ class DIContainer:
88
+ """
89
+ Inversion of Control container for VayuAPI.
90
+
91
+ Supports three service lifetimes:
92
+ - SINGLETON: created once, shared across all requests
93
+ - TRANSIENT: new instance for every resolution
94
+ - SCOPED: one instance per scope (request)
95
+
96
+ Example::
97
+
98
+ container = DIContainer()
99
+
100
+ # Register by type
101
+ container.add_singleton(Database)
102
+ container.add_transient(IEmailService, SmtpEmailService)
103
+ container.add_scoped(UnitOfWork)
104
+
105
+ # Register with a factory
106
+ container.add_singleton_factory(Config, lambda c: Config.from_env())
107
+
108
+ # Register an existing instance
109
+ container.add_instance(Config, Config.from_env())
110
+
111
+ # Resolve
112
+ async with container.create_scope() as scope:
113
+ db = await scope.resolve(Database)
114
+ """
115
+
116
+ def __init__(self):
117
+ self._descriptors: Dict[Type, ServiceDescriptor] = {}
118
+ self._singletons: Dict[Type, Any] = {}
119
+ # Tracks types currently being created to detect circular dependencies.
120
+ self._creating: set = set()
121
+
122
+ # ------------------------------------------------------------------
123
+ # Registration helpers
124
+ # ------------------------------------------------------------------
125
+
126
+ def register(self, descriptor: ServiceDescriptor) -> "DIContainer":
127
+ self._descriptors[descriptor.service_type] = descriptor
128
+ return self
129
+
130
+ def add_singleton(
131
+ self,
132
+ service_type: Type[T],
133
+ implementation_type: Optional[Type[T]] = None,
134
+ ) -> "DIContainer":
135
+ impl = implementation_type or service_type
136
+ return self.register(ServiceDescriptor(service_type, ServiceLifetime.SINGLETON, implementation_type=impl))
137
+
138
+ def add_singleton_factory(
139
+ self,
140
+ service_type: Type[T],
141
+ factory: Callable[["DIContainer"], T],
142
+ ) -> "DIContainer":
143
+ return self.register(ServiceDescriptor(service_type, ServiceLifetime.SINGLETON, factory=factory))
144
+
145
+ def add_instance(self, service_type: Type[T], instance: T) -> "DIContainer":
146
+ return self.register(ServiceDescriptor(service_type, ServiceLifetime.SINGLETON, instance=instance))
147
+
148
+ def add_transient(
149
+ self,
150
+ service_type: Type[T],
151
+ implementation_type: Optional[Type[T]] = None,
152
+ ) -> "DIContainer":
153
+ impl = implementation_type or service_type
154
+ return self.register(ServiceDescriptor(service_type, ServiceLifetime.TRANSIENT, implementation_type=impl))
155
+
156
+ def add_transient_factory(
157
+ self,
158
+ service_type: Type[T],
159
+ factory: Callable[["DIContainer"], T],
160
+ ) -> "DIContainer":
161
+ return self.register(ServiceDescriptor(service_type, ServiceLifetime.TRANSIENT, factory=factory))
162
+
163
+ def add_scoped(
164
+ self,
165
+ service_type: Type[T],
166
+ implementation_type: Optional[Type[T]] = None,
167
+ ) -> "DIContainer":
168
+ impl = implementation_type or service_type
169
+ return self.register(ServiceDescriptor(service_type, ServiceLifetime.SCOPED, implementation_type=impl))
170
+
171
+ def add_scoped_factory(
172
+ self,
173
+ service_type: Type[T],
174
+ factory: Callable[["DIContainer"], T],
175
+ ) -> "DIContainer":
176
+ return self.register(ServiceDescriptor(service_type, ServiceLifetime.SCOPED, factory=factory))
177
+
178
+ # ------------------------------------------------------------------
179
+ # Scope factory
180
+ # ------------------------------------------------------------------
181
+
182
+ def create_scope(self) -> ServiceScope:
183
+ return ServiceScope(self)
184
+
185
+ # ------------------------------------------------------------------
186
+ # Internal resolution
187
+ # ------------------------------------------------------------------
188
+
189
+ async def _resolve(self, service_type: Type[T], scope: Optional[ServiceScope] = None) -> T:
190
+ descriptor = self._descriptors.get(service_type)
191
+ if descriptor is None:
192
+ raise LookupError(
193
+ f"Service '{service_type.__name__}' is not registered in the container."
194
+ )
195
+
196
+ if descriptor.lifetime == ServiceLifetime.SINGLETON:
197
+ return await self._resolve_singleton(descriptor, scope)
198
+ elif descriptor.lifetime == ServiceLifetime.TRANSIENT:
199
+ return await self._create_instance(descriptor, scope)
200
+ elif descriptor.lifetime == ServiceLifetime.SCOPED:
201
+ if scope is None:
202
+ raise RuntimeError(
203
+ f"Cannot resolve scoped service '{service_type.__name__}' outside of a scope."
204
+ )
205
+ if service_type not in scope._scoped_instances:
206
+ instance = await self._create_instance(descriptor, scope)
207
+ scope._scoped_instances[service_type] = instance
208
+ return scope._scoped_instances[service_type]
209
+
210
+ async def _resolve_singleton(self, descriptor: ServiceDescriptor, scope: Optional[ServiceScope]) -> Any:
211
+ if descriptor.service_type in self._singletons:
212
+ return self._singletons[descriptor.service_type]
213
+
214
+ if descriptor.service_type in self._creating:
215
+ raise RuntimeError(
216
+ f"Circular dependency detected while creating '{descriptor.service_type.__name__}'."
217
+ )
218
+
219
+ self._creating.add(descriptor.service_type)
220
+ try:
221
+ instance = await self._create_instance(descriptor, scope)
222
+ finally:
223
+ self._creating.discard(descriptor.service_type)
224
+
225
+ # Store only if not already stored (concurrent tasks are not possible in
226
+ # single-threaded asyncio, but guard for completeness).
227
+ if descriptor.service_type not in self._singletons:
228
+ self._singletons[descriptor.service_type] = instance
229
+ return self._singletons[descriptor.service_type]
230
+
231
+ async def _create_instance(self, descriptor: ServiceDescriptor, scope: Optional[ServiceScope]) -> Any:
232
+ if descriptor.instance is not None:
233
+ return descriptor.instance
234
+
235
+ if descriptor.factory is not None:
236
+ result = descriptor.factory(self)
237
+ if asyncio.iscoroutine(result):
238
+ result = await result
239
+ elif inspect.isasyncgen(result):
240
+ instance = await result.__anext__()
241
+ if scope is not None:
242
+ scope._async_generators.append(result)
243
+ return instance
244
+ elif inspect.isgenerator(result):
245
+ instance = next(result)
246
+ if scope is not None:
247
+ scope._sync_generators.append(result)
248
+ return instance
249
+ return result
250
+
251
+ impl_type = descriptor.implementation_type
252
+ # Auto-wire constructor dependencies
253
+ kwargs = await self._autowire(impl_type, scope)
254
+ return impl_type(**kwargs)
255
+
256
+ async def _autowire(self, impl_type: Type, scope: Optional[ServiceScope]) -> Dict[str, Any]:
257
+ # Use get_type_hints to properly resolve string annotations
258
+ # (PEP 563 / from __future__ import annotations in caller modules).
259
+ try:
260
+ hints = typing.get_type_hints(impl_type.__init__)
261
+ except Exception:
262
+ hints = {}
263
+
264
+ sig = inspect.signature(impl_type.__init__)
265
+ kwargs: Dict[str, Any] = {}
266
+
267
+ for name, param in sig.parameters.items():
268
+ if name == "self":
269
+ continue
270
+ # Prefer resolved hint; fall back to raw annotation on the Parameter.
271
+ annotation = hints.get(name, param.annotation)
272
+ if annotation is inspect.Parameter.empty:
273
+ if param.default is not inspect.Parameter.empty:
274
+ continue # has default, skip
275
+ raise TypeError(
276
+ f"Cannot auto-wire parameter '{name}' in '{impl_type.__name__}': "
277
+ f"no type annotation and no default value."
278
+ )
279
+ if annotation in self._descriptors:
280
+ kwargs[name] = await self._resolve(annotation, scope)
281
+ elif param.default is not inspect.Parameter.empty:
282
+ continue # use default, skip
283
+ # else: unknown / primitive type — skip silently, constructor
284
+ # will raise if it was required (gives a clear Python TypeError).
285
+
286
+ return kwargs
287
+
288
+ # ------------------------------------------------------------------
289
+ # Direct resolve (no scope — for singletons / root resolution)
290
+ # ------------------------------------------------------------------
291
+
292
+ async def resolve(self, service_type: Type[T]) -> T:
293
+ return await self._resolve(service_type, scope=None)
294
+
295
+ def is_registered(self, service_type: Type) -> bool:
296
+ return service_type in self._descriptors
297
+
298
+ def __contains__(self, service_type: Type) -> bool:
299
+ return self.is_registered(service_type)
300
+
301
+ def __repr__(self) -> str:
302
+ names = [d.service_type.__name__ for d in self._descriptors.values()]
303
+ return f"DIContainer(services=[{', '.join(names)}])"
304
+
305
+
306
+ __all__ = [
307
+ "DIContainer",
308
+ "ServiceLifetime",
309
+ "ServiceDescriptor",
310
+ "ServiceScope",
311
+ ]
@@ -0,0 +1,136 @@
1
+ """
2
+ @injectable and @inject decorators for VayuAPI DI.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import functools
8
+ import inspect
9
+ from typing import Any, Callable, Optional, Type, TypeVar
10
+
11
+ T = TypeVar("T")
12
+
13
+ # Registry of injectable classes (used for auto-discovery)
14
+ _INJECTABLE_REGISTRY: dict[Type, dict] = {}
15
+
16
+
17
+ def injectable(
18
+ lifetime: str = "transient",
19
+ *,
20
+ alias: Optional[Type] = None,
21
+ ):
22
+ """
23
+ Mark a class as injectable so it can be auto-registered in a DIContainer.
24
+
25
+ Args:
26
+ lifetime: 'singleton', 'transient', or 'scoped' (default 'transient')
27
+ alias: Optional interface/base-class to register the service under.
28
+
29
+ Example::
30
+
31
+ @injectable(lifetime="singleton")
32
+ class Database:
33
+ def __init__(self, config: Config):
34
+ self.url = config.db_url
35
+
36
+ @injectable(lifetime="scoped", alias=IUserRepository)
37
+ class UserRepository(IUserRepository):
38
+ def __init__(self, db: Database):
39
+ self.db = db
40
+ """
41
+ from vayuapi.di.container import ServiceLifetime
42
+
43
+ lifetime_map = {
44
+ "singleton": ServiceLifetime.SINGLETON,
45
+ "transient": ServiceLifetime.TRANSIENT,
46
+ "scoped": ServiceLifetime.SCOPED,
47
+ }
48
+
49
+ def decorator(cls: Type[T]) -> Type[T]:
50
+ sl = lifetime_map.get(lifetime.lower())
51
+ if sl is None:
52
+ raise ValueError(
53
+ f"Invalid lifetime '{lifetime}'. Use 'singleton', 'transient', or 'scoped'."
54
+ )
55
+ _INJECTABLE_REGISTRY[cls] = {
56
+ "lifetime": sl,
57
+ "alias": alias,
58
+ }
59
+ cls.__injectable_lifetime__ = sl
60
+ cls.__injectable_alias__ = alias
61
+ return cls
62
+
63
+ return decorator
64
+
65
+
66
+ def inject(container_attr: str = "container"):
67
+ """
68
+ Decorator that auto-resolves constructor parameters from a DIContainer
69
+ attached to ``self.<container_attr>``.
70
+
71
+ Mainly useful for class-based views / handlers that hold a reference
72
+ to the application's container.
73
+
74
+ Example::
75
+
76
+ class UserService:
77
+ container: DIContainer
78
+
79
+ @inject()
80
+ async def __init__(self, repo: UserRepository):
81
+ self.repo = repo
82
+ """
83
+ def decorator(func: Callable) -> Callable:
84
+ @functools.wraps(func)
85
+ async def wrapper(self, *args, **kwargs):
86
+ container = getattr(self, container_attr, None)
87
+ if container is None:
88
+ return await func(self, *args, **kwargs)
89
+
90
+ sig = inspect.signature(func)
91
+ resolved: dict[str, Any] = {}
92
+
93
+ for name, param in sig.parameters.items():
94
+ if name in ("self", *kwargs):
95
+ continue
96
+ annotation = param.annotation
97
+ if annotation is inspect.Parameter.empty:
98
+ continue
99
+ if container.is_registered(annotation):
100
+ resolved[name] = await container.resolve(annotation)
101
+
102
+ return await func(self, *args, **{**resolved, **kwargs})
103
+
104
+ return wrapper
105
+
106
+ return decorator
107
+
108
+
109
+ def register_injectables(container) -> None:
110
+ """
111
+ Register all classes decorated with @injectable into the given container.
112
+
113
+ Call this once during application startup after all modules are imported.
114
+
115
+ Example::
116
+
117
+ from vayuapi.di.injectable import register_injectables
118
+ register_injectables(app.container)
119
+ """
120
+ from vayuapi.di.container import ServiceDescriptor
121
+
122
+ for cls, meta in _INJECTABLE_REGISTRY.items():
123
+ service_type = meta["alias"] if meta["alias"] is not None else cls
124
+ descriptor = ServiceDescriptor(
125
+ service_type=service_type,
126
+ lifetime=meta["lifetime"],
127
+ implementation_type=cls,
128
+ )
129
+ container.register(descriptor)
130
+
131
+
132
+ __all__ = [
133
+ "injectable",
134
+ "inject",
135
+ "register_injectables",
136
+ ]
@@ -0,0 +1,56 @@
1
+ """
2
+ ServiceProvider — thin wrapper that exposes resolve() for use inside
3
+ request handlers and middleware via the VayuAPI `Depends` mechanism.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Optional, Type, TypeVar
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ class ServiceProvider:
14
+ """
15
+ High-level API for resolving services from a DIContainer within a
16
+ request scope.
17
+
18
+ VayuAPI creates one ServiceProvider per request and injects it when
19
+ a handler declares ``provider: ServiceProvider`` as a parameter or
20
+ uses ``Depends(get_service_provider)``.
21
+
22
+ Example::
23
+
24
+ from vayuapi.di import ServiceProvider
25
+
26
+ @app.get("/users")
27
+ async def list_users(provider: ServiceProvider = Depends(get_provider)):
28
+ repo = await provider.get(UserRepository)
29
+ return await repo.all()
30
+ """
31
+
32
+ def __init__(self, container, scope=None):
33
+ from vayuapi.di.container import DIContainer, ServiceScope
34
+ self._container: DIContainer = container
35
+ self._scope: Optional[ServiceScope] = scope
36
+
37
+ async def get(self, service_type: Type[T]) -> T:
38
+ if self._scope is not None:
39
+ return await self._scope.resolve(service_type)
40
+ return await self._container.resolve(service_type)
41
+
42
+ async def __aenter__(self) -> "ServiceProvider":
43
+ if self._scope is None:
44
+ self._scope = self._container.create_scope()
45
+ await self._scope.__aenter__()
46
+ return self
47
+
48
+ async def __aexit__(self, *args) -> None:
49
+ if self._scope is not None:
50
+ await self._scope.__aexit__(*args)
51
+
52
+ def __repr__(self) -> str:
53
+ return f"ServiceProvider(container={self._container!r})"
54
+
55
+
56
+ __all__ = ["ServiceProvider"]