mdb-engine 0.1.7__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,97 +1,60 @@
1
1
  """
2
- Embedding Service Dependency Injection for FastAPI
2
+ Embedding Service Utilities
3
3
 
4
- This module provides FastAPI dependency functions to inject embedding services
5
- into route handlers. The embedding service is automatically initialized from
6
- the app's manifest.json configuration.
7
- """
4
+ This module provides utility functions for creating embedding services.
5
+ For FastAPI dependency injection, use the request-scoped dependencies
6
+ from `mdb_engine.dependencies` instead.
8
7
 
9
- from typing import Any, Optional
8
+ Usage:
9
+ # For FastAPI routes (RECOMMENDED):
10
+ from mdb_engine.dependencies import get_embedding_service
10
11
 
11
- # Optional FastAPI import (only needed if FastAPI is available)
12
- try:
13
- from fastapi import Depends, HTTPException
12
+ @app.post("/embed")
13
+ async def embed(embedding_service=Depends(get_embedding_service)):
14
+ ...
14
15
 
15
- FASTAPI_AVAILABLE = True
16
- except ImportError:
17
- FASTAPI_AVAILABLE = False
16
+ # For standalone/utility usage:
17
+ from mdb_engine.embeddings.dependencies import get_embedding_service_for_app
18
18
 
19
- # Stub for when FastAPI is not available
20
- def Depends(*args, **kwargs):
21
- return None
19
+ service = get_embedding_service_for_app("my_app", engine)
20
+ """
22
21
 
23
- class HTTPException(Exception):
24
- pass
22
+ from typing import TYPE_CHECKING, Optional
25
23
 
24
+ if TYPE_CHECKING:
25
+ from ..core.engine import MongoDBEngine
26
26
 
27
27
  from .service import EmbeddingService, get_embedding_service
28
28
 
29
- # Global engine registry (for apps that don't pass engine explicitly)
30
- _global_engine: Optional[Any] = None
31
- _global_app_slug: Optional[str] = None
32
-
33
-
34
- def set_global_engine(engine: Any, app_slug: Optional[str] = None) -> None:
35
- """
36
- Set global MongoDBEngine instance for embedding dependency injection.
37
-
38
- This is useful when you have a single engine instance that you want
39
- to use across all apps. Call this during application startup.
40
-
41
- Args:
42
- engine: MongoDBEngine instance
43
- app_slug: Optional app slug
44
- """
45
- global _global_engine, _global_app_slug
46
- _global_engine = engine
47
- _global_app_slug = app_slug
48
-
49
-
50
- def get_global_engine() -> Optional[Any]:
51
- """
52
- Get global MongoDBEngine instance.
53
-
54
- Returns:
55
- MongoDBEngine instance if set, None otherwise
56
- """
57
- return _global_engine
58
-
59
29
 
60
30
  def get_embedding_service_for_app(
61
- app_slug: str, engine: Optional[Any] = None
31
+ app_slug: str, engine: "MongoDBEngine"
62
32
  ) -> Optional[EmbeddingService]:
63
33
  """
64
- Get embedding service for a specific app.
34
+ Get embedding service for a specific app using the engine instance.
35
+
36
+ This is a utility function for cases where you need to create an
37
+ embedding service outside of a FastAPI request context (e.g., in
38
+ background tasks, CLI tools, or tests).
65
39
 
66
- This is a helper function that can be used with FastAPI's Depends()
67
- to inject the embedding service into route handlers.
40
+ For FastAPI routes, use `mdb_engine.dependencies.get_embedding_service` instead.
68
41
 
69
42
  Args:
70
- app_slug: App slug (typically extracted from route context)
71
- engine: MongoDBEngine instance (optional, will try to get from context)
43
+ app_slug: App slug to get embedding config from
44
+ engine: MongoDBEngine instance
72
45
 
73
46
  Returns:
74
- EmbeddingService instance if embedding is enabled for this app, None otherwise
47
+ EmbeddingService instance if embedding is enabled, None otherwise
75
48
 
76
49
  Example:
77
- ```python
78
- from fastapi import Depends
79
- from mdb_engine.embeddings.dependencies import get_embedding_service_for_app
80
-
81
- @app.post("/embed")
82
- async def embed_endpoint(
83
- embedding_service = Depends(lambda: get_embedding_service_for_app("my_app"))
84
- ):
85
- if not embedding_service:
86
- raise HTTPException(503, "Embedding service not available")
87
- embeddings = await embedding_service.embed_chunks(["Hello world"])
88
- return {"embeddings": embeddings}
89
- ```
90
- """
91
- # Try to get engine from context if not provided
92
- if engine is None:
93
- engine = _global_engine
50
+ # In a background task or CLI
51
+ engine = MongoDBEngine(...)
52
+ await engine.initialize()
94
53
 
54
+ service = get_embedding_service_for_app("my_app", engine)
55
+ if service:
56
+ embeddings = await service.embed_chunks(["Hello world"])
57
+ """
95
58
  if engine is None:
96
59
  return None
97
60
 
@@ -108,84 +71,6 @@ def get_embedding_service_for_app(
108
71
  return get_embedding_service(config=embedding_config)
109
72
 
110
73
 
111
- def create_embedding_dependency(app_slug: str, engine: Optional[Any] = None):
112
- """
113
- Create a FastAPI dependency function for embedding service.
114
-
115
- This creates a dependency function that can be used with Depends()
116
- to inject the embedding service into route handlers.
117
-
118
- Args:
119
- app_slug: App slug
120
- engine: MongoDBEngine instance (optional)
121
-
122
- Returns:
123
- Dependency function that returns EmbeddingService or raises HTTPException
124
-
125
- Example:
126
- ```python
127
- from fastapi import Depends
128
- from mdb_engine.embeddings.dependencies import create_embedding_dependency
129
-
130
- embedding_dep = create_embedding_dependency("my_app", engine)
131
-
132
- @app.post("/embed")
133
- async def embed_endpoint(embedding_service = Depends(embedding_dep)):
134
- embeddings = await embedding_service.embed_chunks(["Hello world"])
135
- return {"embeddings": embeddings}
136
- ```
137
- """
138
-
139
- def _get_embedding_service() -> EmbeddingService:
140
- embedding_service = get_embedding_service_for_app(app_slug, engine)
141
- if embedding_service is None:
142
- if FASTAPI_AVAILABLE:
143
- raise HTTPException(
144
- status_code=503,
145
- detail=f"Embedding service not available for app '{app_slug}'. "
146
- "Ensure 'embedding_config.enabled' is true in manifest.json and "
147
- "embedding dependencies are installed.",
148
- )
149
- else:
150
- raise RuntimeError(f"Embedding service not available for app '{app_slug}'")
151
- return embedding_service
152
-
153
- return _get_embedding_service
154
-
155
-
156
- def get_embedding_service_dependency(app_slug: str):
157
- """
158
- Get embedding service dependency using global engine.
159
-
160
- This is a convenience function that uses the global engine registry.
161
- Set the engine with set_global_engine() during app startup.
162
-
163
- Args:
164
- app_slug: App slug
165
-
166
- Returns:
167
- Dependency function for FastAPI Depends()
168
-
169
- Example:
170
- ```python
171
- from fastapi import FastAPI, Depends
172
- from mdb_engine.embeddings.dependencies import (
173
- set_global_engine, get_embedding_service_dependency
174
- )
175
-
176
- app = FastAPI()
177
-
178
- # During startup
179
- set_global_engine(engine, app_slug="my_app")
180
-
181
- # In routes
182
- @app.post("/embed")
183
- async def embed(embedding_service = Depends(get_embedding_service_dependency("my_app"))):
184
- return await embedding_service.embed_chunks(["Hello world"])
185
- ```
186
- """
187
- return create_embedding_dependency(app_slug, _global_engine)
188
-
189
-
190
- # Alias for backward compatibility
191
- get_embedding_service_dep = get_embedding_service_dependency
74
+ __all__ = [
75
+ "get_embedding_service_for_app",
76
+ ]
@@ -0,0 +1,34 @@
1
+ """
2
+ MDB Engine Repository Pattern
3
+
4
+ Provides abstract repository interfaces and MongoDB implementations
5
+ for clean data access patterns.
6
+
7
+ Usage:
8
+ from mdb_engine.repositories import Repository, MongoRepository
9
+
10
+ # In domain services
11
+ class UserService:
12
+ def __init__(self, users: Repository[User]):
13
+ self._users = users
14
+
15
+ async def get_user(self, id: str) -> User:
16
+ return await self._users.get(id)
17
+
18
+ # In FastAPI routes using UnitOfWork
19
+ @app.get("/users/{user_id}")
20
+ async def get_user(user_id: str, ctx: RequestContext = Depends()):
21
+ user = await ctx.uow.users.get(user_id)
22
+ return user
23
+ """
24
+
25
+ from .base import Entity, Repository
26
+ from .mongo import MongoRepository
27
+ from .unit_of_work import UnitOfWork
28
+
29
+ __all__ = [
30
+ "Repository",
31
+ "Entity",
32
+ "MongoRepository",
33
+ "UnitOfWork",
34
+ ]
@@ -0,0 +1,325 @@
1
+ """
2
+ Abstract Repository Pattern
3
+
4
+ Defines the repository interface that abstracts data access operations.
5
+ This allows domain services to work with any data store implementation.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime
11
+ from typing import Any, Dict, Generic, List, Optional, TypeVar
12
+
13
+ from bson import ObjectId
14
+
15
+
16
+ @dataclass
17
+ class Entity:
18
+ """
19
+ Base class for domain entities.
20
+
21
+ All entities have an ID and timestamps. Subclass this for your domain models.
22
+
23
+ Example:
24
+ @dataclass
25
+ class User(Entity):
26
+ email: str
27
+ name: str
28
+ role: str = "user"
29
+ """
30
+
31
+ id: Optional[str] = None
32
+ created_at: Optional[datetime] = field(default=None)
33
+ updated_at: Optional[datetime] = field(default=None)
34
+
35
+ def to_dict(self) -> Dict[str, Any]:
36
+ """Convert entity to dictionary for storage."""
37
+ data = {}
38
+ for key, value in self.__dict__.items():
39
+ if value is not None:
40
+ if key == "id":
41
+ # Convert string ID to ObjectId for MongoDB
42
+ if value and ObjectId.is_valid(value):
43
+ data["_id"] = ObjectId(value)
44
+ else:
45
+ data["_id"] = value
46
+ else:
47
+ data[key] = value
48
+ return data
49
+
50
+ @classmethod
51
+ def from_dict(cls, data: Dict[str, Any]) -> "Entity":
52
+ """Create entity from dictionary (e.g., from database)."""
53
+ if data is None:
54
+ return None
55
+
56
+ # Convert _id to id
57
+ if "_id" in data:
58
+ data["id"] = str(data.pop("_id"))
59
+
60
+ # Get field names from dataclass
61
+ import dataclasses
62
+
63
+ field_names = {f.name for f in dataclasses.fields(cls)}
64
+
65
+ # Filter to only known fields
66
+ filtered_data = {k: v for k, v in data.items() if k in field_names}
67
+
68
+ return cls(**filtered_data)
69
+
70
+
71
+ T = TypeVar("T", bound=Entity)
72
+
73
+
74
+ class Repository(ABC, Generic[T]):
75
+ """
76
+ Abstract repository interface for data access.
77
+
78
+ This interface defines standard CRUD operations that can be
79
+ implemented for any data store (MongoDB, PostgreSQL, in-memory, etc.)
80
+
81
+ Type parameter T should be an Entity subclass.
82
+
83
+ Example:
84
+ class UserRepository(Repository[User]):
85
+ async def find_by_email(self, email: str) -> Optional[User]:
86
+ users = await self.find({"email": email}, limit=1)
87
+ return users[0] if users else None
88
+ """
89
+
90
+ @abstractmethod
91
+ async def get(self, id: str) -> Optional[T]:
92
+ """
93
+ Get a single entity by ID.
94
+
95
+ Args:
96
+ id: Entity ID
97
+
98
+ Returns:
99
+ Entity if found, None otherwise
100
+ """
101
+ pass
102
+
103
+ @abstractmethod
104
+ async def find(
105
+ self,
106
+ filter: Optional[Dict[str, Any]] = None,
107
+ skip: int = 0,
108
+ limit: int = 100,
109
+ sort: Optional[List[tuple]] = None,
110
+ ) -> List[T]:
111
+ """
112
+ Find entities matching a filter.
113
+
114
+ Args:
115
+ filter: MongoDB-style filter dictionary
116
+ skip: Number of documents to skip
117
+ limit: Maximum documents to return
118
+ sort: List of (field, direction) tuples
119
+
120
+ Returns:
121
+ List of matching entities
122
+ """
123
+ pass
124
+
125
+ @abstractmethod
126
+ async def find_one(
127
+ self,
128
+ filter: Dict[str, Any],
129
+ ) -> Optional[T]:
130
+ """
131
+ Find a single entity matching a filter.
132
+
133
+ Args:
134
+ filter: MongoDB-style filter dictionary
135
+
136
+ Returns:
137
+ First matching entity or None
138
+ """
139
+ pass
140
+
141
+ @abstractmethod
142
+ async def add(self, entity: T) -> str:
143
+ """
144
+ Add a new entity.
145
+
146
+ Args:
147
+ entity: Entity to add
148
+
149
+ Returns:
150
+ ID of the created entity
151
+ """
152
+ pass
153
+
154
+ @abstractmethod
155
+ async def add_many(self, entities: List[T]) -> List[str]:
156
+ """
157
+ Add multiple entities.
158
+
159
+ Args:
160
+ entities: List of entities to add
161
+
162
+ Returns:
163
+ List of created entity IDs
164
+ """
165
+ pass
166
+
167
+ @abstractmethod
168
+ async def update(self, id: str, entity: T) -> bool:
169
+ """
170
+ Update an existing entity.
171
+
172
+ Args:
173
+ id: Entity ID
174
+ entity: Updated entity data
175
+
176
+ Returns:
177
+ True if entity was updated, False if not found
178
+ """
179
+ pass
180
+
181
+ @abstractmethod
182
+ async def update_fields(self, id: str, fields: Dict[str, Any]) -> bool:
183
+ """
184
+ Update specific fields of an entity.
185
+
186
+ Args:
187
+ id: Entity ID
188
+ fields: Dictionary of fields to update
189
+
190
+ Returns:
191
+ True if entity was updated, False if not found
192
+ """
193
+ pass
194
+
195
+ @abstractmethod
196
+ async def delete(self, id: str) -> bool:
197
+ """
198
+ Delete an entity by ID.
199
+
200
+ Args:
201
+ id: Entity ID
202
+
203
+ Returns:
204
+ True if entity was deleted, False if not found
205
+ """
206
+ pass
207
+
208
+ @abstractmethod
209
+ async def count(self, filter: Optional[Dict[str, Any]] = None) -> int:
210
+ """
211
+ Count entities matching a filter.
212
+
213
+ Args:
214
+ filter: MongoDB-style filter dictionary
215
+
216
+ Returns:
217
+ Count of matching entities
218
+ """
219
+ pass
220
+
221
+ @abstractmethod
222
+ async def exists(self, id: str) -> bool:
223
+ """
224
+ Check if an entity exists.
225
+
226
+ Args:
227
+ id: Entity ID
228
+
229
+ Returns:
230
+ True if entity exists
231
+ """
232
+ pass
233
+
234
+
235
+ class InMemoryRepository(Repository[T]):
236
+ """
237
+ In-memory repository implementation for testing.
238
+
239
+ Stores entities in a dictionary, useful for unit tests
240
+ without database dependencies.
241
+ """
242
+
243
+ def __init__(self, entity_class: type):
244
+ self._entity_class = entity_class
245
+ self._storage: Dict[str, Dict[str, Any]] = {}
246
+ self._counter = 0
247
+
248
+ async def get(self, id: str) -> Optional[T]:
249
+ data = self._storage.get(id)
250
+ if data is None:
251
+ return None
252
+ return self._entity_class.from_dict(data)
253
+
254
+ async def find(
255
+ self,
256
+ filter: Optional[Dict[str, Any]] = None,
257
+ skip: int = 0,
258
+ limit: int = 100,
259
+ sort: Optional[List[tuple]] = None,
260
+ ) -> List[T]:
261
+ results = []
262
+ for data in self._storage.values():
263
+ if filter is None or self._matches_filter(data, filter):
264
+ results.append(self._entity_class.from_dict(data))
265
+
266
+ # Apply skip and limit
267
+ return results[skip : skip + limit]
268
+
269
+ async def find_one(self, filter: Dict[str, Any]) -> Optional[T]:
270
+ results = await self.find(filter, limit=1)
271
+ return results[0] if results else None
272
+
273
+ async def add(self, entity: T) -> str:
274
+ self._counter += 1
275
+ id = str(self._counter)
276
+ entity.id = id
277
+ entity.created_at = datetime.utcnow()
278
+ self._storage[id] = entity.to_dict()
279
+ return id
280
+
281
+ async def add_many(self, entities: List[T]) -> List[str]:
282
+ return [await self.add(e) for e in entities]
283
+
284
+ async def update(self, id: str, entity: T) -> bool:
285
+ if id not in self._storage:
286
+ return False
287
+ entity.id = id
288
+ entity.updated_at = datetime.utcnow()
289
+ self._storage[id] = entity.to_dict()
290
+ return True
291
+
292
+ async def update_fields(self, id: str, fields: Dict[str, Any]) -> bool:
293
+ if id not in self._storage:
294
+ return False
295
+ self._storage[id].update(fields)
296
+ self._storage[id]["updated_at"] = datetime.utcnow()
297
+ return True
298
+
299
+ async def delete(self, id: str) -> bool:
300
+ if id not in self._storage:
301
+ return False
302
+ del self._storage[id]
303
+ return True
304
+
305
+ async def count(self, filter: Optional[Dict[str, Any]] = None) -> int:
306
+ if filter is None:
307
+ return len(self._storage)
308
+ return len(await self.find(filter, limit=999999))
309
+
310
+ async def exists(self, id: str) -> bool:
311
+ return id in self._storage
312
+
313
+ def _matches_filter(self, data: Dict[str, Any], filter: Dict[str, Any]) -> bool:
314
+ """Simple filter matching for testing."""
315
+ for key, value in filter.items():
316
+ if key not in data:
317
+ return False
318
+ if data[key] != value:
319
+ return False
320
+ return True
321
+
322
+ def clear(self) -> None:
323
+ """Clear all entities (useful for test setup)."""
324
+ self._storage.clear()
325
+ self._counter = 0