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.
- mdb_engine/__init__.py +71 -10
- mdb_engine/auth/ARCHITECTURE.md +112 -0
- mdb_engine/auth/README.md +125 -11
- mdb_engine/auth/__init__.py +7 -1
- mdb_engine/auth/base.py +252 -0
- mdb_engine/auth/casbin_factory.py +258 -59
- mdb_engine/auth/dependencies.py +10 -5
- mdb_engine/auth/integration.py +23 -7
- mdb_engine/auth/oso_factory.py +2 -2
- mdb_engine/auth/provider.py +263 -143
- mdb_engine/core/engine.py +307 -6
- mdb_engine/core/manifest.py +35 -15
- mdb_engine/database/README.md +28 -1
- mdb_engine/dependencies.py +426 -0
- mdb_engine/di/__init__.py +34 -0
- mdb_engine/di/container.py +248 -0
- mdb_engine/di/providers.py +205 -0
- mdb_engine/di/scopes.py +139 -0
- mdb_engine/embeddings/README.md +54 -24
- mdb_engine/embeddings/__init__.py +22 -23
- mdb_engine/embeddings/dependencies.py +37 -152
- 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-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/METADATA +42 -14
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/RECORD +31 -20
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/WHEEL +0 -0
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/entry_points.txt +0 -0
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {mdb_engine-0.1.7.dist-info → mdb_engine-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,97 +1,60 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Embedding Service
|
|
2
|
+
Embedding Service Utilities
|
|
3
3
|
|
|
4
|
-
This module provides
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
8
|
+
Usage:
|
|
9
|
+
# For FastAPI routes (RECOMMENDED):
|
|
10
|
+
from mdb_engine.dependencies import get_embedding_service
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
@app.post("/embed")
|
|
13
|
+
async def embed(embedding_service=Depends(get_embedding_service)):
|
|
14
|
+
...
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
FASTAPI_AVAILABLE = False
|
|
16
|
+
# For standalone/utility usage:
|
|
17
|
+
from mdb_engine.embeddings.dependencies import get_embedding_service_for_app
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return None
|
|
19
|
+
service = get_embedding_service_for_app("my_app", engine)
|
|
20
|
+
"""
|
|
22
21
|
|
|
23
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
71
|
-
engine: MongoDBEngine instance
|
|
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
|
|
47
|
+
EmbeddingService instance if embedding is enabled, None otherwise
|
|
75
48
|
|
|
76
49
|
Example:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
112
|
-
""
|
|
113
|
-
|
|
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
|