mdb-engine 0.1.7__py3-none-any.whl → 0.2.1__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.
@@ -0,0 +1,205 @@
1
+ """
2
+ Service Providers for Dependency Injection
3
+
4
+ Providers are responsible for creating and managing service instances
5
+ according to their configured scope.
6
+ """
7
+
8
+ import inspect
9
+ import logging
10
+ from abc import ABC, abstractmethod
11
+ from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, Optional, Type, TypeVar
12
+
13
+ from .scopes import Scope, ScopeManager
14
+
15
+ if TYPE_CHECKING:
16
+ from .container import Container
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ T = TypeVar("T")
21
+
22
+
23
+ class Provider(ABC, Generic[T]):
24
+ """
25
+ Abstract base class for service providers.
26
+
27
+ Providers know how to create instances of a service and manage
28
+ their lifecycle according to the configured scope.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ service_type: Type[T],
34
+ scope: Scope,
35
+ factory: Optional[Callable[..., T]] = None,
36
+ ):
37
+ self.service_type = service_type
38
+ self.scope = scope
39
+ self._factory = factory or service_type
40
+
41
+ @abstractmethod
42
+ def get(self, container: "Container") -> T:
43
+ """
44
+ Get or create a service instance.
45
+
46
+ Args:
47
+ container: The DI container for resolving dependencies
48
+
49
+ Returns:
50
+ Service instance
51
+ """
52
+ pass
53
+
54
+ def _create_instance(self, container: "Container") -> T:
55
+ """
56
+ Create a new instance, injecting dependencies.
57
+
58
+ Inspects the factory/constructor signature and resolves
59
+ any type-hinted parameters from the container.
60
+ """
61
+ # Get constructor signature
62
+ sig = inspect.signature(self._factory)
63
+ kwargs: Dict[str, Any] = {}
64
+
65
+ for param_name, param in sig.parameters.items():
66
+ if param_name == "self":
67
+ continue
68
+
69
+ # Check if parameter has a type annotation
70
+ if param.annotation != inspect.Parameter.empty:
71
+ param_type = param.annotation
72
+
73
+ # Skip primitive types and Optional markers
74
+ if param_type in (str, int, float, bool, type(None)):
75
+ continue
76
+
77
+ # Handle Optional[X] - extract X
78
+ origin = getattr(param_type, "__origin__", None)
79
+ if origin is type(None):
80
+ continue
81
+
82
+ # Try to resolve from container
83
+ try:
84
+ kwargs[param_name] = container.resolve(param_type)
85
+ except KeyError:
86
+ # If not registered and has default, skip
87
+ if param.default != inspect.Parameter.empty:
88
+ continue
89
+ # If not registered and no default, re-raise
90
+ raise
91
+
92
+ return self._factory(**kwargs)
93
+
94
+
95
+ class SingletonProvider(Provider[T]):
96
+ """
97
+ Provider that creates a single instance shared across the application.
98
+
99
+ The instance is created lazily on first request and cached forever.
100
+ """
101
+
102
+ def __init__(
103
+ self,
104
+ service_type: Type[T],
105
+ factory: Optional[Callable[..., T]] = None,
106
+ ):
107
+ super().__init__(service_type, Scope.SINGLETON, factory)
108
+ self._instance: Optional[T] = None
109
+
110
+ def get(self, container: "Container") -> T:
111
+ if self._instance is None:
112
+ self._instance = self._create_instance(container)
113
+ logger.debug(f"Created singleton: {self.service_type.__name__}")
114
+ return self._instance
115
+
116
+ def reset(self) -> None:
117
+ """Reset the singleton (useful for testing)."""
118
+ self._instance = None
119
+
120
+
121
+ class RequestProvider(Provider[T]):
122
+ """
123
+ Provider that creates one instance per request.
124
+
125
+ Uses ScopeManager to cache instances within the request scope.
126
+ """
127
+
128
+ def __init__(
129
+ self,
130
+ service_type: Type[T],
131
+ factory: Optional[Callable[..., T]] = None,
132
+ ):
133
+ super().__init__(service_type, Scope.REQUEST, factory)
134
+
135
+ def get(self, container: "Container") -> T:
136
+ return ScopeManager.get_or_create(
137
+ self.service_type, lambda: self._create_instance(container)
138
+ )
139
+
140
+
141
+ class TransientProvider(Provider[T]):
142
+ """
143
+ Provider that creates a new instance every time.
144
+ """
145
+
146
+ def __init__(
147
+ self,
148
+ service_type: Type[T],
149
+ factory: Optional[Callable[..., T]] = None,
150
+ ):
151
+ super().__init__(service_type, Scope.TRANSIENT, factory)
152
+
153
+ def get(self, container: "Container") -> T:
154
+ instance = self._create_instance(container)
155
+ logger.debug(f"Created transient: {self.service_type.__name__}")
156
+ return instance
157
+
158
+
159
+ class FactoryProvider(Provider[T]):
160
+ """
161
+ Provider that uses a custom factory function.
162
+
163
+ The factory is called with the container as the first argument,
164
+ allowing manual dependency resolution.
165
+
166
+ Usage:
167
+ def create_user_service(container: Container) -> UserService:
168
+ db = container.resolve(Database)
169
+ return UserService(db, custom_config=...)
170
+
171
+ container.register_factory(UserService, create_user_service, Scope.REQUEST)
172
+ """
173
+
174
+ def __init__(
175
+ self,
176
+ service_type: Type[T],
177
+ factory: Callable[["Container"], T],
178
+ scope: Scope,
179
+ ):
180
+ super().__init__(service_type, scope, None)
181
+ self._custom_factory = factory
182
+ self._singleton_instance: Optional[T] = None
183
+
184
+ def get(self, container: "Container") -> T:
185
+ if self.scope == Scope.SINGLETON:
186
+ if self._singleton_instance is None:
187
+ self._singleton_instance = self._custom_factory(container)
188
+ return self._singleton_instance
189
+
190
+ elif self.scope == Scope.REQUEST:
191
+ return ScopeManager.get_or_create(
192
+ self.service_type, lambda: self._custom_factory(container)
193
+ )
194
+
195
+ else: # TRANSIENT
196
+ return self._custom_factory(container)
197
+
198
+
199
+ __all__ = [
200
+ "Provider",
201
+ "SingletonProvider",
202
+ "RequestProvider",
203
+ "TransientProvider",
204
+ "FactoryProvider",
205
+ ]
@@ -0,0 +1,139 @@
1
+ """
2
+ Service Scopes for Dependency Injection
3
+
4
+ Defines service lifetime scopes following enterprise patterns:
5
+ - SINGLETON: Created once, shared across all requests
6
+ - REQUEST: Created once per HTTP request, disposed after
7
+ - TRANSIENT: Created fresh on every injection
8
+ """
9
+
10
+ import logging
11
+ from contextvars import ContextVar
12
+ from enum import Enum
13
+ from typing import Any, Dict, Optional
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Context variable for request-scoped instances
18
+ _request_scope: ContextVar[Optional[Dict[type, Any]]] = ContextVar("request_scope", default=None)
19
+
20
+
21
+ class Scope(Enum):
22
+ """
23
+ Service lifetime scopes.
24
+
25
+ SINGLETON: One instance for the entire application lifetime.
26
+ Use for: Database connections, configuration, caches.
27
+
28
+ REQUEST: One instance per HTTP request. Automatically disposed
29
+ when the request ends.
30
+ Use for: Unit of Work, request context, user session.
31
+
32
+ TRANSIENT: New instance created every time it's requested.
33
+ Use for: Stateless services, utilities.
34
+ """
35
+
36
+ SINGLETON = "singleton"
37
+ REQUEST = "request"
38
+ TRANSIENT = "transient"
39
+
40
+
41
+ class ScopeManager:
42
+ """
43
+ Manages request-scoped instance lifecycles.
44
+
45
+ Usage with FastAPI middleware:
46
+ @app.middleware("http")
47
+ async def scope_middleware(request: Request, call_next):
48
+ async with ScopeManager.request_scope():
49
+ response = await call_next(request)
50
+ return response
51
+ """
52
+
53
+ @classmethod
54
+ def begin_request(cls) -> Dict[type, Any]:
55
+ """
56
+ Begin a new request scope.
57
+
58
+ Returns the scope dictionary for manual management if needed.
59
+ """
60
+ scope_dict: Dict[type, Any] = {}
61
+ _request_scope.set(scope_dict)
62
+ logger.debug("Request scope started")
63
+ return scope_dict
64
+
65
+ @classmethod
66
+ def end_request(cls) -> None:
67
+ """
68
+ End the current request scope and cleanup instances.
69
+
70
+ Calls dispose() on any instances that have it.
71
+ """
72
+ scope_dict = _request_scope.get()
73
+ if scope_dict:
74
+ for instance in scope_dict.values():
75
+ if hasattr(instance, "dispose"):
76
+ try:
77
+ instance.dispose()
78
+ except (AttributeError, RuntimeError, TypeError) as e:
79
+ logger.warning(f"Error disposing {type(instance).__name__}: {e}")
80
+ scope_dict.clear()
81
+ _request_scope.set(None)
82
+ logger.debug("Request scope ended")
83
+
84
+ @classmethod
85
+ def get_request_scope(cls) -> Optional[Dict[type, Any]]:
86
+ """Get the current request scope dictionary."""
87
+ return _request_scope.get()
88
+
89
+ @classmethod
90
+ def get_or_create(cls, key: type, factory: callable) -> Any:
91
+ """
92
+ Get an existing instance from request scope or create one.
93
+
94
+ Args:
95
+ key: The type to use as cache key
96
+ factory: Callable to create a new instance if not cached
97
+
98
+ Returns:
99
+ The cached or newly created instance
100
+
101
+ Raises:
102
+ RuntimeError: If called outside a request scope
103
+ """
104
+ scope_dict = _request_scope.get()
105
+ if scope_dict is None:
106
+ raise RuntimeError(
107
+ "No active request scope. Ensure ScopeManager.begin_request() "
108
+ "was called (usually via middleware)."
109
+ )
110
+
111
+ if key not in scope_dict:
112
+ scope_dict[key] = factory()
113
+ logger.debug(f"Created request-scoped instance: {key.__name__}")
114
+
115
+ return scope_dict[key]
116
+
117
+ @classmethod
118
+ async def request_scope(cls):
119
+ """
120
+ Async context manager for request scope.
121
+
122
+ Usage:
123
+ async with ScopeManager.request_scope():
124
+ # Request-scoped services available here
125
+ pass
126
+ """
127
+ return _RequestScopeContext()
128
+
129
+
130
+ class _RequestScopeContext:
131
+ """Async context manager for request scope."""
132
+
133
+ async def __aenter__(self):
134
+ ScopeManager.begin_request()
135
+ return self
136
+
137
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
138
+ ScopeManager.end_request()
139
+ return False
@@ -9,6 +9,7 @@ Semantic text splitting and embedding generation for MDB_ENGINE applications.
9
9
  - **Token-Aware**: Never exceeds model token limits
10
10
  - **Batch Processing**: Efficient batch embedding generation
11
11
  - **MongoDB Integration**: Built-in support for storing embeddings with metadata
12
+ - **Request-Scoped Dependencies**: Clean FastAPI integration via `mdb_engine.dependencies`
12
13
 
13
14
  ## Installation
14
15
 
@@ -38,7 +39,44 @@ Enable embedding service in your `manifest.json`:
38
39
 
39
40
  ## Usage
40
41
 
41
- ### 1. Basic Usage (Auto-Detection)
42
+ ### 1. FastAPI Routes (Recommended)
43
+
44
+ Use request-scoped dependencies from `mdb_engine.dependencies`:
45
+
46
+ ```python
47
+ from fastapi import Depends
48
+ from mdb_engine import MongoDBEngine
49
+ from mdb_engine.dependencies import get_embedding_service, get_scoped_db
50
+
51
+ engine = MongoDBEngine(mongo_uri=..., db_name=...)
52
+ app = engine.create_app(slug="my_app", manifest=Path("manifest.json"))
53
+
54
+ @app.post("/embed")
55
+ async def embed_endpoint(
56
+ db=Depends(get_scoped_db),
57
+ embedding_service=Depends(get_embedding_service),
58
+ ):
59
+ # Services are automatically bound to the current app
60
+ result = await embedding_service.process_and_store(
61
+ text_content="Hello world",
62
+ source_id="doc_1",
63
+ collection=db.knowledge_base,
64
+ )
65
+ return {"chunks_created": result["chunks_created"]}
66
+
67
+ @app.post("/search")
68
+ async def search(
69
+ query: str,
70
+ db=Depends(get_scoped_db),
71
+ embedding_service=Depends(get_embedding_service),
72
+ ):
73
+ # Generate query embedding
74
+ vectors = await embedding_service.embed_chunks([query])
75
+ # Use vectors for vector search...
76
+ return {"query_vector_dims": len(vectors[0])}
77
+ ```
78
+
79
+ ### 2. Basic Usage (Standalone)
42
80
 
43
81
  ```python
44
82
  from mdb_engine.embeddings import EmbeddingService
@@ -56,7 +94,7 @@ chunks = await embedding_service.chunk_text(
56
94
  vectors = await embedding_service.embed_chunks(chunks, model="text-embedding-3-small")
57
95
  ```
58
96
 
59
- ### 2. Process and Store in MongoDB
97
+ ### 3. Process and Store in MongoDB
60
98
 
61
99
  ```python
62
100
  from mdb_engine.embeddings import EmbeddingService
@@ -75,36 +113,28 @@ result = await embedding_service.process_and_store(
75
113
  print(f"Created {result['chunks_created']} chunks")
76
114
  ```
77
115
 
78
- ### 3. Explicit Provider
116
+ ### 4. Utility Function (Background Tasks/CLI)
117
+
118
+ For use outside of FastAPI request handlers:
79
119
 
80
120
  ```python
81
- from mdb_engine.embeddings import EmbeddingService, OpenAIEmbeddingProvider, EmbeddingProvider
121
+ from mdb_engine.embeddings.dependencies import get_embedding_service_for_app
82
122
 
83
- # Use OpenAI explicitly
84
- openai_provider = OpenAIEmbeddingProvider(default_model="text-embedding-3-small")
85
- provider = EmbeddingProvider(embedding_provider=openai_provider)
86
- embedding_service = EmbeddingService(embedding_provider=provider)
123
+ # In a background task or CLI tool
124
+ service = get_embedding_service_for_app("my_app", engine)
125
+ if service:
126
+ embeddings = await service.embed_chunks(["Hello world"])
87
127
  ```
88
128
 
89
- ### 4. In FastAPI Routes
129
+ ### 5. Explicit Provider
90
130
 
91
131
  ```python
92
- from fastapi import FastAPI, Depends
93
- from mdb_engine.embeddings.dependencies import get_embedding_service_dependency
94
- from mdb_engine.embeddings import EmbeddingService
95
-
96
- app = FastAPI()
97
-
98
- # Set global engine during startup
99
- from mdb_engine.embeddings.dependencies import set_global_engine
100
- set_global_engine(engine, app_slug="my_app")
132
+ from mdb_engine.embeddings import EmbeddingService, OpenAIEmbeddingProvider, EmbeddingProvider
101
133
 
102
- @app.post("/embed")
103
- async def embed_endpoint(
104
- embedding_service: EmbeddingService = Depends(get_embedding_service_dependency("my_app"))
105
- ):
106
- embeddings = await embedding_service.embed_chunks(["Hello world"])
107
- return {"embeddings": embeddings}
134
+ # Use OpenAI explicitly
135
+ openai_provider = OpenAIEmbeddingProvider(default_model="text-embedding-3-small")
136
+ provider = EmbeddingProvider(embedding_provider=openai_provider)
137
+ embedding_service = EmbeddingService(embedding_provider=provider)
108
138
  ```
109
139
 
110
140
  ## Environment Variables
@@ -7,6 +7,22 @@ Examples should implement their own LLM clients directly using the OpenAI SDK.
7
7
  For memory functionality, use mdb_engine.memory.Mem0MemoryService which
8
8
  handles embeddings and LLM via environment variables (.env).
9
9
 
10
+ FastAPI Dependency Injection:
11
+ # RECOMMENDED: Use request-scoped dependencies
12
+ from mdb_engine.dependencies import get_embedding_service
13
+
14
+ @app.post("/embed")
15
+ async def embed_text(embedding_service=Depends(get_embedding_service)):
16
+ embeddings = await embedding_service.embed_chunks(["Hello world"])
17
+ return {"embeddings": embeddings}
18
+
19
+ Standalone Usage:
20
+ from mdb_engine.embeddings import EmbeddingService, get_embedding_service
21
+
22
+ # Auto-detects OpenAI or Azure from environment variables
23
+ service = get_embedding_service(config={"default_embedding_model": "text-embedding-3-small"})
24
+ embeddings = await service.embed_chunks(["Hello world"])
25
+
10
26
  Example LLM implementation:
11
27
  from openai import AzureOpenAI
12
28
  from dotenv import load_dotenv
@@ -24,25 +40,9 @@ Example LLM implementation:
24
40
  model=os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME"),
25
41
  messages=[...]
26
42
  )
27
-
28
- Example EmbeddingService usage:
29
- from mdb_engine.embeddings import EmbeddingService, get_embedding_service
30
-
31
- # In FastAPI route
32
- @app.post("/embed")
33
- async def embed_text(embedding_service: EmbeddingService = Depends(get_embedding_service)):
34
- embeddings = await embedding_service.embed_chunks(["Hello world"])
35
- return {"embeddings": embeddings}
36
43
  """
37
44
 
38
- from .dependencies import (
39
- create_embedding_dependency,
40
- get_embedding_service_dep,
41
- get_embedding_service_dependency,
42
- get_embedding_service_for_app,
43
- get_global_engine,
44
- set_global_engine,
45
- )
45
+ from .dependencies import get_embedding_service_for_app
46
46
  from .service import (
47
47
  AzureOpenAIEmbeddingProvider,
48
48
  BaseEmbeddingProvider,
@@ -54,17 +54,16 @@ from .service import (
54
54
  )
55
55
 
56
56
  __all__ = [
57
+ # Core service classes
57
58
  "EmbeddingService",
58
59
  "EmbeddingServiceError",
60
+ "EmbeddingProvider",
61
+ # Embedding providers
59
62
  "BaseEmbeddingProvider",
60
63
  "OpenAIEmbeddingProvider",
61
64
  "AzureOpenAIEmbeddingProvider",
62
- "EmbeddingProvider",
65
+ # Factory function
63
66
  "get_embedding_service",
67
+ # Utility for standalone usage
64
68
  "get_embedding_service_for_app",
65
- "create_embedding_dependency",
66
- "set_global_engine",
67
- "get_global_engine",
68
- "get_embedding_service_dependency",
69
- "get_embedding_service_dep",
70
69
  ]