mdb-engine 0.1.6__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.
Files changed (87) hide show
  1. mdb_engine/__init__.py +104 -11
  2. mdb_engine/auth/ARCHITECTURE.md +112 -0
  3. mdb_engine/auth/README.md +648 -11
  4. mdb_engine/auth/__init__.py +136 -29
  5. mdb_engine/auth/audit.py +592 -0
  6. mdb_engine/auth/base.py +252 -0
  7. mdb_engine/auth/casbin_factory.py +264 -69
  8. mdb_engine/auth/config_helpers.py +7 -6
  9. mdb_engine/auth/cookie_utils.py +3 -7
  10. mdb_engine/auth/csrf.py +373 -0
  11. mdb_engine/auth/decorators.py +3 -10
  12. mdb_engine/auth/dependencies.py +47 -50
  13. mdb_engine/auth/helpers.py +3 -3
  14. mdb_engine/auth/integration.py +53 -80
  15. mdb_engine/auth/jwt.py +2 -6
  16. mdb_engine/auth/middleware.py +77 -34
  17. mdb_engine/auth/oso_factory.py +18 -38
  18. mdb_engine/auth/provider.py +270 -171
  19. mdb_engine/auth/rate_limiter.py +504 -0
  20. mdb_engine/auth/restrictions.py +8 -24
  21. mdb_engine/auth/session_manager.py +14 -29
  22. mdb_engine/auth/shared_middleware.py +600 -0
  23. mdb_engine/auth/shared_users.py +759 -0
  24. mdb_engine/auth/token_store.py +14 -28
  25. mdb_engine/auth/users.py +54 -113
  26. mdb_engine/auth/utils.py +213 -15
  27. mdb_engine/cli/commands/generate.py +545 -9
  28. mdb_engine/cli/commands/validate.py +3 -7
  29. mdb_engine/cli/utils.py +3 -3
  30. mdb_engine/config.py +7 -21
  31. mdb_engine/constants.py +65 -0
  32. mdb_engine/core/README.md +117 -6
  33. mdb_engine/core/__init__.py +39 -7
  34. mdb_engine/core/app_registration.py +22 -41
  35. mdb_engine/core/app_secrets.py +290 -0
  36. mdb_engine/core/connection.py +18 -9
  37. mdb_engine/core/encryption.py +223 -0
  38. mdb_engine/core/engine.py +1057 -93
  39. mdb_engine/core/index_management.py +12 -16
  40. mdb_engine/core/manifest.py +459 -150
  41. mdb_engine/core/ray_integration.py +435 -0
  42. mdb_engine/core/seeding.py +10 -18
  43. mdb_engine/core/service_initialization.py +12 -23
  44. mdb_engine/core/types.py +2 -5
  45. mdb_engine/database/README.md +140 -17
  46. mdb_engine/database/__init__.py +17 -6
  47. mdb_engine/database/abstraction.py +25 -37
  48. mdb_engine/database/connection.py +11 -18
  49. mdb_engine/database/query_validator.py +367 -0
  50. mdb_engine/database/resource_limiter.py +204 -0
  51. mdb_engine/database/scoped_wrapper.py +713 -196
  52. mdb_engine/dependencies.py +426 -0
  53. mdb_engine/di/__init__.py +34 -0
  54. mdb_engine/di/container.py +248 -0
  55. mdb_engine/di/providers.py +205 -0
  56. mdb_engine/di/scopes.py +139 -0
  57. mdb_engine/embeddings/README.md +54 -24
  58. mdb_engine/embeddings/__init__.py +31 -24
  59. mdb_engine/embeddings/dependencies.py +37 -154
  60. mdb_engine/embeddings/service.py +11 -25
  61. mdb_engine/exceptions.py +92 -0
  62. mdb_engine/indexes/README.md +30 -13
  63. mdb_engine/indexes/__init__.py +1 -0
  64. mdb_engine/indexes/helpers.py +1 -1
  65. mdb_engine/indexes/manager.py +50 -114
  66. mdb_engine/memory/README.md +2 -2
  67. mdb_engine/memory/__init__.py +1 -2
  68. mdb_engine/memory/service.py +30 -87
  69. mdb_engine/observability/README.md +4 -2
  70. mdb_engine/observability/__init__.py +26 -9
  71. mdb_engine/observability/health.py +8 -9
  72. mdb_engine/observability/metrics.py +32 -12
  73. mdb_engine/repositories/__init__.py +34 -0
  74. mdb_engine/repositories/base.py +325 -0
  75. mdb_engine/repositories/mongo.py +233 -0
  76. mdb_engine/repositories/unit_of_work.py +166 -0
  77. mdb_engine/routing/README.md +1 -1
  78. mdb_engine/routing/__init__.py +1 -3
  79. mdb_engine/routing/websockets.py +25 -60
  80. mdb_engine-0.2.0.dist-info/METADATA +313 -0
  81. mdb_engine-0.2.0.dist-info/RECORD +96 -0
  82. mdb_engine-0.1.6.dist-info/METADATA +0 -213
  83. mdb_engine-0.1.6.dist-info/RECORD +0 -75
  84. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/WHEEL +0 -0
  85. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/entry_points.txt +0 -0
  86. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/licenses/LICENSE +0 -0
  87. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.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, 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
@@ -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, Dict, Generic, List, Optional, Type, 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: Optional[Dict[str, Any]]) -> Optional[T]:
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) -> Optional[T]:
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: Optional[Dict[str, Any]] = None,
82
+ skip: int = 0,
83
+ limit: int = 100,
84
+ sort: Optional[List[tuple]] = 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]) -> Optional[T]:
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):
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: Optional[Dict[str, Any]] = 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