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.
- {vayuapi-0.1.2 → vayuapi-0.1.4}/PKG-INFO +16 -6
- {vayuapi-0.1.2 → vayuapi-0.1.4}/pyproject.toml +23 -4
- {vayuapi-0.1.2 → vayuapi-0.1.4}/setup.py +2 -2
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/__init__.py +23 -2
- vayuapi-0.1.4/vayuapi/di/__init__.py +23 -0
- vayuapi-0.1.4/vayuapi/di/container.py +311 -0
- vayuapi-0.1.4/vayuapi/di/injectable.py +136 -0
- vayuapi-0.1.4/vayuapi/di/provider.py +56 -0
- vayuapi-0.1.4/vayuapi/grpc/__init__.py +16 -0
- vayuapi-0.1.4/vayuapi/grpc/client.py +129 -0
- vayuapi-0.1.4/vayuapi/grpc/decorators.py +161 -0
- vayuapi-0.1.4/vayuapi/grpc/server.py +244 -0
- vayuapi-0.1.4/vayuapi/tasks/__init__.py +18 -0
- vayuapi-0.1.4/vayuapi/tasks/arq_app.py +352 -0
- vayuapi-0.1.4/vayuapi/tasks/celery_app.py +239 -0
- vayuapi-0.1.4/vayuapi/tasks/decorators.py +184 -0
- vayuapi-0.1.4/vayuapi/tasks/worker.py +103 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi.egg-info/SOURCES.txt +13 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/LICENSE +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/MANIFEST.in +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/README.md +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/requirements.txt +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/setup.cfg +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/admin/__init__.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/admin/enhanced_panel.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/admin/panel.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/ai/__init__.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/ai/langchain.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/ai/pydantic_ai.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/__init__.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/application.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/concurrency.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/decorators.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/dependencies.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/docs.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/exceptions.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/middleware.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/params.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/responses.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/routing.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/staticfiles.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/templating.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/uploads.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/core/websocket.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/middleware/__init__.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/orm/__init__.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/orm/async_orm.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/orm/database.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/orm/django_orm.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/scheduler/__init__.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/scheduler/tasks.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/security/__init__.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/security/auth.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/security/encryption.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/security/jwt.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/status.py +0 -0
- {vayuapi-0.1.2 → vayuapi-0.1.4}/vayuapi/utils/__init__.py +0 -0
- {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.
|
|
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 <
|
|
8
|
-
Maintainer-email: VayuAPI Team <
|
|
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
|
-
|
|
92
|
-
Requires-Dist:
|
|
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.
|
|
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 = "
|
|
20
|
+
{name = "VayuAPI Team", email = "codeswithalok@gmail.com"}
|
|
21
21
|
]
|
|
22
22
|
maintainers = [
|
|
23
|
-
{name = "VayuAPI Team", email = "
|
|
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.
|
|
10
|
+
version="0.1.4",
|
|
11
11
|
author="VayuAPI Team",
|
|
12
|
-
author_email="
|
|
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
|
-
#
|
|
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"]
|