mdb-engine 0.1.6__py3-none-any.whl → 0.4.12__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.
- mdb_engine/__init__.py +116 -11
- mdb_engine/auth/ARCHITECTURE.md +112 -0
- mdb_engine/auth/README.md +654 -11
- mdb_engine/auth/__init__.py +136 -29
- mdb_engine/auth/audit.py +592 -0
- mdb_engine/auth/base.py +252 -0
- mdb_engine/auth/casbin_factory.py +265 -70
- mdb_engine/auth/config_defaults.py +5 -5
- mdb_engine/auth/config_helpers.py +19 -18
- mdb_engine/auth/cookie_utils.py +12 -16
- mdb_engine/auth/csrf.py +483 -0
- mdb_engine/auth/decorators.py +10 -16
- mdb_engine/auth/dependencies.py +69 -71
- mdb_engine/auth/helpers.py +3 -3
- mdb_engine/auth/integration.py +61 -88
- mdb_engine/auth/jwt.py +11 -15
- mdb_engine/auth/middleware.py +79 -35
- mdb_engine/auth/oso_factory.py +21 -41
- mdb_engine/auth/provider.py +270 -171
- mdb_engine/auth/rate_limiter.py +505 -0
- mdb_engine/auth/restrictions.py +21 -36
- mdb_engine/auth/session_manager.py +24 -41
- mdb_engine/auth/shared_middleware.py +977 -0
- mdb_engine/auth/shared_users.py +775 -0
- mdb_engine/auth/token_lifecycle.py +10 -12
- mdb_engine/auth/token_store.py +17 -32
- mdb_engine/auth/users.py +99 -159
- mdb_engine/auth/utils.py +236 -42
- mdb_engine/cli/commands/generate.py +546 -10
- mdb_engine/cli/commands/validate.py +3 -7
- mdb_engine/cli/utils.py +7 -7
- mdb_engine/config.py +13 -28
- mdb_engine/constants.py +65 -0
- mdb_engine/core/README.md +117 -6
- mdb_engine/core/__init__.py +39 -7
- mdb_engine/core/app_registration.py +31 -50
- mdb_engine/core/app_secrets.py +289 -0
- mdb_engine/core/connection.py +20 -12
- mdb_engine/core/encryption.py +222 -0
- mdb_engine/core/engine.py +2862 -115
- mdb_engine/core/index_management.py +12 -16
- mdb_engine/core/manifest.py +628 -204
- mdb_engine/core/ray_integration.py +436 -0
- mdb_engine/core/seeding.py +13 -21
- mdb_engine/core/service_initialization.py +20 -30
- mdb_engine/core/types.py +40 -43
- mdb_engine/database/README.md +140 -17
- mdb_engine/database/__init__.py +17 -6
- mdb_engine/database/abstraction.py +37 -50
- mdb_engine/database/connection.py +51 -30
- mdb_engine/database/query_validator.py +367 -0
- mdb_engine/database/resource_limiter.py +204 -0
- mdb_engine/database/scoped_wrapper.py +747 -237
- mdb_engine/dependencies.py +427 -0
- mdb_engine/di/__init__.py +34 -0
- mdb_engine/di/container.py +247 -0
- mdb_engine/di/providers.py +206 -0
- mdb_engine/di/scopes.py +139 -0
- mdb_engine/embeddings/README.md +54 -24
- mdb_engine/embeddings/__init__.py +31 -24
- mdb_engine/embeddings/dependencies.py +38 -155
- mdb_engine/embeddings/service.py +78 -75
- mdb_engine/exceptions.py +104 -12
- mdb_engine/indexes/README.md +30 -13
- mdb_engine/indexes/__init__.py +1 -0
- mdb_engine/indexes/helpers.py +11 -11
- mdb_engine/indexes/manager.py +59 -123
- mdb_engine/memory/README.md +95 -4
- mdb_engine/memory/__init__.py +1 -2
- mdb_engine/memory/service.py +363 -1168
- mdb_engine/observability/README.md +4 -2
- mdb_engine/observability/__init__.py +26 -9
- mdb_engine/observability/health.py +17 -17
- mdb_engine/observability/logging.py +10 -10
- mdb_engine/observability/metrics.py +40 -19
- mdb_engine/repositories/__init__.py +34 -0
- mdb_engine/repositories/base.py +325 -0
- mdb_engine/repositories/mongo.py +233 -0
- mdb_engine/repositories/unit_of_work.py +166 -0
- mdb_engine/routing/README.md +1 -1
- mdb_engine/routing/__init__.py +1 -3
- mdb_engine/routing/websockets.py +41 -75
- mdb_engine/utils/__init__.py +3 -1
- mdb_engine/utils/mongo.py +117 -0
- mdb_engine-0.4.12.dist-info/METADATA +492 -0
- mdb_engine-0.4.12.dist-info/RECORD +97 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/WHEEL +1 -1
- mdb_engine-0.1.6.dist-info/METADATA +0 -213
- mdb_engine-0.1.6.dist-info/RECORD +0 -75
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/top_level.txt +0 -0
|
@@ -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, Generic, 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: str | None = None
|
|
32
|
+
created_at: datetime | None = field(default=None)
|
|
33
|
+
updated_at: datetime | None = 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) -> T | None:
|
|
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: dict[str, Any] | None = None,
|
|
107
|
+
skip: int = 0,
|
|
108
|
+
limit: int = 100,
|
|
109
|
+
sort: list[tuple] | None = 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
|
+
) -> T | None:
|
|
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: dict[str, Any] | None = 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) -> T | None:
|
|
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: dict[str, Any] | None = None,
|
|
257
|
+
skip: int = 0,
|
|
258
|
+
limit: int = 100,
|
|
259
|
+
sort: list[tuple] | None = 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]) -> T | None:
|
|
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: dict[str, Any] | None = 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
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MongoDB Repository Implementation
|
|
3
|
+
|
|
4
|
+
Implements the Repository interface using MongoDB through ScopedCollectionWrapper.
|
|
5
|
+
This provides automatic app scoping and security features.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any, Generic, TypeVar
|
|
11
|
+
|
|
12
|
+
from bson import ObjectId
|
|
13
|
+
|
|
14
|
+
from .base import Entity, Repository
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T", bound=Entity)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MongoRepository(Repository[T], Generic[T]):
|
|
22
|
+
"""
|
|
23
|
+
MongoDB implementation of the Repository interface.
|
|
24
|
+
|
|
25
|
+
Uses ScopedCollectionWrapper for automatic app_id scoping
|
|
26
|
+
and security features.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
# Direct usage
|
|
30
|
+
users_collection = db.users # ScopedCollectionWrapper
|
|
31
|
+
user_repo = MongoRepository(users_collection, User)
|
|
32
|
+
|
|
33
|
+
# Find users
|
|
34
|
+
users = await user_repo.find({"role": "admin"})
|
|
35
|
+
|
|
36
|
+
# Add new user
|
|
37
|
+
user = User(email="john@example.com", name="John")
|
|
38
|
+
user_id = await user_repo.add(user)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
collection: Any, # ScopedCollectionWrapper - avoid import cycle
|
|
44
|
+
entity_class: type[T],
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
Initialize the MongoDB repository.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
collection: ScopedCollectionWrapper for the collection
|
|
51
|
+
entity_class: Entity subclass for this repository
|
|
52
|
+
"""
|
|
53
|
+
self._collection = collection
|
|
54
|
+
self._entity_class = entity_class
|
|
55
|
+
|
|
56
|
+
def _to_entity(self, doc: dict[str, Any] | None) -> T | None:
|
|
57
|
+
"""Convert a MongoDB document to an entity."""
|
|
58
|
+
if doc is None:
|
|
59
|
+
return None
|
|
60
|
+
return self._entity_class.from_dict(doc)
|
|
61
|
+
|
|
62
|
+
def _to_document(self, entity: T, include_id: bool = False) -> dict[str, Any]:
|
|
63
|
+
"""Convert an entity to a MongoDB document."""
|
|
64
|
+
doc = entity.to_dict()
|
|
65
|
+
if not include_id and "_id" in doc:
|
|
66
|
+
del doc["_id"]
|
|
67
|
+
return doc
|
|
68
|
+
|
|
69
|
+
async def get(self, id: str) -> T | None:
|
|
70
|
+
"""Get entity by ID."""
|
|
71
|
+
try:
|
|
72
|
+
object_id = ObjectId(id) if ObjectId.is_valid(id) else id
|
|
73
|
+
except (TypeError, ValueError):
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
doc = await self._collection.find_one({"_id": object_id})
|
|
77
|
+
return self._to_entity(doc)
|
|
78
|
+
|
|
79
|
+
async def find(
|
|
80
|
+
self,
|
|
81
|
+
filter: dict[str, Any] | None = None,
|
|
82
|
+
skip: int = 0,
|
|
83
|
+
limit: int = 100,
|
|
84
|
+
sort: list[tuple] | None = None,
|
|
85
|
+
) -> list[T]:
|
|
86
|
+
"""Find entities matching a filter."""
|
|
87
|
+
cursor = self._collection.find(filter or {})
|
|
88
|
+
|
|
89
|
+
if skip > 0:
|
|
90
|
+
cursor = cursor.skip(skip)
|
|
91
|
+
if limit > 0:
|
|
92
|
+
cursor = cursor.limit(limit)
|
|
93
|
+
if sort:
|
|
94
|
+
cursor = cursor.sort(sort)
|
|
95
|
+
|
|
96
|
+
docs = await cursor.to_list(length=limit)
|
|
97
|
+
return [self._to_entity(doc) for doc in docs]
|
|
98
|
+
|
|
99
|
+
async def find_one(self, filter: dict[str, Any]) -> T | None:
|
|
100
|
+
"""Find a single entity matching a filter."""
|
|
101
|
+
doc = await self._collection.find_one(filter)
|
|
102
|
+
return self._to_entity(doc)
|
|
103
|
+
|
|
104
|
+
async def add(self, entity: T) -> str:
|
|
105
|
+
"""Add a new entity and return its ID."""
|
|
106
|
+
entity.created_at = datetime.utcnow()
|
|
107
|
+
doc = self._to_document(entity)
|
|
108
|
+
|
|
109
|
+
result = await self._collection.insert_one(doc)
|
|
110
|
+
entity.id = str(result.inserted_id)
|
|
111
|
+
|
|
112
|
+
logger.debug(f"Added {self._entity_class.__name__} with id={entity.id}")
|
|
113
|
+
return entity.id
|
|
114
|
+
|
|
115
|
+
async def add_many(self, entities: list[T]) -> list[str]:
|
|
116
|
+
"""Add multiple entities and return their IDs."""
|
|
117
|
+
now = datetime.utcnow()
|
|
118
|
+
docs = []
|
|
119
|
+
|
|
120
|
+
for entity in entities:
|
|
121
|
+
entity.created_at = now
|
|
122
|
+
docs.append(self._to_document(entity))
|
|
123
|
+
|
|
124
|
+
result = await self._collection.insert_many(docs)
|
|
125
|
+
ids = [str(id) for id in result.inserted_ids]
|
|
126
|
+
|
|
127
|
+
for entity, id in zip(entities, ids, strict=False):
|
|
128
|
+
entity.id = id
|
|
129
|
+
|
|
130
|
+
logger.debug(f"Added {len(ids)} {self._entity_class.__name__} entities")
|
|
131
|
+
return ids
|
|
132
|
+
|
|
133
|
+
async def update(self, id: str, entity: T) -> bool:
|
|
134
|
+
"""Update an entity by ID."""
|
|
135
|
+
try:
|
|
136
|
+
object_id = ObjectId(id) if ObjectId.is_valid(id) else id
|
|
137
|
+
except (TypeError, ValueError):
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
entity.updated_at = datetime.utcnow()
|
|
141
|
+
doc = self._to_document(entity)
|
|
142
|
+
|
|
143
|
+
result = await self._collection.update_one({"_id": object_id}, {"$set": doc})
|
|
144
|
+
|
|
145
|
+
return result.modified_count > 0
|
|
146
|
+
|
|
147
|
+
async def update_fields(self, id: str, fields: dict[str, Any]) -> bool:
|
|
148
|
+
"""Update specific fields of an entity."""
|
|
149
|
+
try:
|
|
150
|
+
object_id = ObjectId(id) if ObjectId.is_valid(id) else id
|
|
151
|
+
except (TypeError, ValueError):
|
|
152
|
+
return False
|
|
153
|
+
|
|
154
|
+
fields["updated_at"] = datetime.utcnow()
|
|
155
|
+
|
|
156
|
+
result = await self._collection.update_one({"_id": object_id}, {"$set": fields})
|
|
157
|
+
|
|
158
|
+
return result.modified_count > 0
|
|
159
|
+
|
|
160
|
+
async def delete(self, id: str) -> bool:
|
|
161
|
+
"""Delete an entity by ID."""
|
|
162
|
+
try:
|
|
163
|
+
object_id = ObjectId(id) if ObjectId.is_valid(id) else id
|
|
164
|
+
except (TypeError, ValueError):
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
result = await self._collection.delete_one({"_id": object_id})
|
|
168
|
+
return result.deleted_count > 0
|
|
169
|
+
|
|
170
|
+
async def count(self, filter: dict[str, Any] | None = None) -> int:
|
|
171
|
+
"""Count entities matching a filter."""
|
|
172
|
+
return await self._collection.count_documents(filter or {})
|
|
173
|
+
|
|
174
|
+
async def exists(self, id: str) -> bool:
|
|
175
|
+
"""Check if an entity exists."""
|
|
176
|
+
try:
|
|
177
|
+
object_id = ObjectId(id) if ObjectId.is_valid(id) else id
|
|
178
|
+
except (TypeError, ValueError):
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
doc = await self._collection.find_one({"_id": object_id}, projection={"_id": 1})
|
|
182
|
+
return doc is not None
|
|
183
|
+
|
|
184
|
+
# Additional MongoDB-specific methods
|
|
185
|
+
|
|
186
|
+
async def aggregate(self, pipeline: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
187
|
+
"""
|
|
188
|
+
Run an aggregation pipeline.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
pipeline: MongoDB aggregation pipeline
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
List of result documents
|
|
195
|
+
"""
|
|
196
|
+
cursor = self._collection.aggregate(pipeline)
|
|
197
|
+
return await cursor.to_list(length=None)
|
|
198
|
+
|
|
199
|
+
async def update_many(
|
|
200
|
+
self,
|
|
201
|
+
filter: dict[str, Any],
|
|
202
|
+
update: dict[str, Any],
|
|
203
|
+
) -> int:
|
|
204
|
+
"""
|
|
205
|
+
Update multiple documents matching a filter.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
filter: MongoDB filter
|
|
209
|
+
update: Update operations
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Number of modified documents
|
|
213
|
+
"""
|
|
214
|
+
if "$set" not in update:
|
|
215
|
+
update = {"$set": update}
|
|
216
|
+
|
|
217
|
+
update["$set"]["updated_at"] = datetime.utcnow()
|
|
218
|
+
|
|
219
|
+
result = await self._collection.update_many(filter, update)
|
|
220
|
+
return result.modified_count
|
|
221
|
+
|
|
222
|
+
async def delete_many(self, filter: dict[str, Any]) -> int:
|
|
223
|
+
"""
|
|
224
|
+
Delete multiple documents matching a filter.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
filter: MongoDB filter
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Number of deleted documents
|
|
231
|
+
"""
|
|
232
|
+
result = await self._collection.delete_many(filter)
|
|
233
|
+
return result.deleted_count
|