python-ppa 0.1.3__py3-none-any.whl → 0.1.5__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.
- ppa/mongo/__init__.py +2 -1
- ppa/mongo/interface.py +74 -61
- {python_ppa-0.1.3.dist-info → python_ppa-0.1.5.dist-info}/METADATA +1 -1
- python_ppa-0.1.5.dist-info/RECORD +9 -0
- python_ppa-0.1.3.dist-info/RECORD +0 -9
- {python_ppa-0.1.3.dist-info → python_ppa-0.1.5.dist-info}/WHEEL +0 -0
- {python_ppa-0.1.3.dist-info → python_ppa-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {python_ppa-0.1.3.dist-info → python_ppa-0.1.5.dist-info}/top_level.txt +0 -0
ppa/mongo/__init__.py
CHANGED
ppa/mongo/interface.py
CHANGED
|
@@ -1,27 +1,64 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import re
|
|
3
|
-
from typing import Any, Callable, ClassVar, Generic, TypeVar, cast, get_args, get_origin, get_type_hints, List
|
|
4
|
-
from unittest import result
|
|
3
|
+
from typing import Any, Callable, ClassVar, Generic, Optional, TypeVar, cast, get_args, get_origin, get_type_hints, List
|
|
5
4
|
|
|
6
|
-
from pydantic import BaseModel, ConfigDict
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
6
|
from bson import ObjectId, errors
|
|
8
7
|
|
|
9
8
|
from ppa.config import get_collection, get_framework_logger
|
|
10
9
|
|
|
11
|
-
T = TypeVar("T", bound=
|
|
10
|
+
T = TypeVar("T", bound='DocumentModel')
|
|
12
11
|
F = TypeVar("F", bound=Callable[..., Any])
|
|
13
12
|
framework_logger = get_framework_logger("repository")
|
|
14
13
|
|
|
15
|
-
DEFAULT_DOCUMENT_CONFIG = dict[str, Any] = {
|
|
16
|
-
"populate_by_name": True,
|
|
17
|
-
"arbitrary_types_allowed": True,
|
|
18
|
-
"json_encoders": {ObjectId: str}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
14
|
|
|
22
15
|
class DocumentModel(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
Base class for all MongoDB models.
|
|
18
|
+
Inherits default configurations and the standard MongoDB _id mapping.
|
|
19
|
+
"""
|
|
23
20
|
__collection_name__: ClassVar[str]
|
|
24
21
|
|
|
22
|
+
# Automatically inherited and merged by child models
|
|
23
|
+
model_config = ConfigDict(
|
|
24
|
+
populate_by_name=True,
|
|
25
|
+
arbitrary_types_allowed=True,
|
|
26
|
+
json_encoders={ObjectId: str}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Universal MongoDB _id mapping
|
|
30
|
+
id: Optional[ObjectId] = Field(default=None, alias="_id")
|
|
31
|
+
|
|
32
|
+
def model_dump(self, **kwargs: Any) -> dict[str, Any]:
|
|
33
|
+
"""
|
|
34
|
+
Overrides default Pydantic dump to ensure MongoDB compatibility.
|
|
35
|
+
Defaults to excluding None values and using aliases (id -> _id).
|
|
36
|
+
"""
|
|
37
|
+
kwargs.setdefault("exclude_none", True)
|
|
38
|
+
kwargs.setdefault("by_alias", True)
|
|
39
|
+
return super().model_dump(**kwargs)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Inherit from list[T] so we can use DocumentList[User], DocumentList[Booking], etc.
|
|
43
|
+
class DocumentList(list[T]):
|
|
44
|
+
"""
|
|
45
|
+
List of documents that can be used to perform bulk operations.
|
|
46
|
+
Inherits default configurations
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def model_dump(self, **kwargs: Any) -> list[dict[str, Any]]:
|
|
50
|
+
return [doc.model_dump(**kwargs) for doc in self]
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def model_validate(cls, obj: list[Any], entity_cls: type[T]) -> 'DocumentList[T]':
|
|
54
|
+
"""
|
|
55
|
+
Validates a list of raw dictionaries and returns a populated DocumentList.
|
|
56
|
+
We pass `entity_cls` because generic type T is erased at runtime.
|
|
57
|
+
"""
|
|
58
|
+
if not isinstance(obj, list) and not hasattr(obj, "__iter__"):
|
|
59
|
+
raise ValueError("Data provided to DocumentList.model_validate must be iterable.")
|
|
60
|
+
|
|
61
|
+
return cls([entity_cls.model_validate(item) for item in obj])
|
|
25
62
|
|
|
26
63
|
def query(definition: dict[str, Any]) -> Callable[[F], F]:
|
|
27
64
|
"""Annotation to specify a custom MongoDB query template."""
|
|
@@ -38,20 +75,6 @@ def document(name: str) -> Callable[[type[DocumentModel]], type[DocumentModel]]:
|
|
|
38
75
|
|
|
39
76
|
def decorator(cls):
|
|
40
77
|
cls.__collection_name__ = name
|
|
41
|
-
current_cfs = dict(getattr(cls, "model_config", {}) or {})
|
|
42
|
-
current_encoders = dict(current_cfs.get("json_encoders", {}) or {})
|
|
43
|
-
merged_encoders = {
|
|
44
|
-
**DEFAULT_DOCUMENT_CONFIG["json_encoders"],
|
|
45
|
-
**current_encoders
|
|
46
|
-
}
|
|
47
|
-
merged_cfs = {
|
|
48
|
-
**DEFAULT_DOCUMENT_CONFIG["model_config"],
|
|
49
|
-
**current_cfs,
|
|
50
|
-
"json_encoders": merged_encoders,
|
|
51
|
-
}
|
|
52
|
-
cls.model_config = ConfigDict(**merged_cfs)
|
|
53
|
-
if hasattr(cls, "model_rebuild"):
|
|
54
|
-
cls.model_rebuild(force=True)
|
|
55
78
|
return cls
|
|
56
79
|
|
|
57
80
|
return decorator
|
|
@@ -86,8 +109,8 @@ class RepositoryMeta(type):
|
|
|
86
109
|
is_convention = re.match(r"(find_by|find_all_by|exists_by|count_by)_(.*)", attr_name)
|
|
87
110
|
|
|
88
111
|
if not is_convention:
|
|
89
|
-
framework_logger.info("Convention does not
|
|
90
|
-
|
|
112
|
+
framework_logger.info("Convention does not exist, relying on user implementation")
|
|
113
|
+
continue
|
|
91
114
|
|
|
92
115
|
if query_template or is_convention is not None:
|
|
93
116
|
hints = get_type_hints(attr_value)
|
|
@@ -139,12 +162,11 @@ class RepositoryMeta(type):
|
|
|
139
162
|
if p_name in bound.arguments:
|
|
140
163
|
ordered_args.append(bound.arguments[p_name])
|
|
141
164
|
|
|
142
|
-
# Pure dictionary walk for placeholder substitution (no internal casting strings to dates)
|
|
143
165
|
def substitute_placeholders(node: Any) -> Any:
|
|
144
166
|
if isinstance(node, str) and node.startswith("?"):
|
|
145
167
|
try:
|
|
146
168
|
idx = int(node[1:])
|
|
147
|
-
return ordered_args[idx]
|
|
169
|
+
return ordered_args[idx]
|
|
148
170
|
except (ValueError, IndexError):
|
|
149
171
|
return node
|
|
150
172
|
elif isinstance(node, dict):
|
|
@@ -193,6 +215,13 @@ class RepositoryMeta(type):
|
|
|
193
215
|
bound.apply_defaults()
|
|
194
216
|
actual_value = bound.arguments.get(param_names[0]) if param_names else None
|
|
195
217
|
|
|
218
|
+
# Convert string IDs to ObjectId if querying against _id
|
|
219
|
+
if mongo_field == "_id" and isinstance(actual_value, str):
|
|
220
|
+
try:
|
|
221
|
+
actual_value = ObjectId(actual_value)
|
|
222
|
+
except errors.InvalidId:
|
|
223
|
+
pass
|
|
224
|
+
|
|
196
225
|
query_dict: dict[str, Any] = {mongo_field: actual_value}
|
|
197
226
|
|
|
198
227
|
framework_logger.debug(
|
|
@@ -235,7 +264,6 @@ def execute_dynamic_query(
|
|
|
235
264
|
return_type: Any,
|
|
236
265
|
entity_cls: type[DocumentModel],
|
|
237
266
|
) -> Any:
|
|
238
|
-
# Only cast fields checking against the database's internal _id field
|
|
239
267
|
processed_query = cast_primary_keys(query_dict)
|
|
240
268
|
|
|
241
269
|
framework_logger.debug(
|
|
@@ -253,18 +281,15 @@ def execute_dynamic_query(
|
|
|
253
281
|
if strategy == "exists_by" or origin_type is bool:
|
|
254
282
|
return collection.count_documents(processed_query, limit=1) > 0
|
|
255
283
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
doc = dict(doc)
|
|
259
|
-
doc["_id"] = str(doc["_id"])
|
|
260
|
-
return doc
|
|
261
|
-
|
|
262
|
-
if origin_type is list:
|
|
284
|
+
# UPDATE: Support both standard lists and our new DocumentList
|
|
285
|
+
if origin_type in (list, DocumentList):
|
|
263
286
|
cursor = collection.find(processed_query)
|
|
264
|
-
|
|
287
|
+
results = [entity_cls.model_validate(doc) for doc in cursor]
|
|
288
|
+
# Return the specific list type requested
|
|
289
|
+
return DocumentList(results) if origin_type is DocumentList else results
|
|
265
290
|
|
|
266
291
|
doc = collection.find_one(processed_query)
|
|
267
|
-
return entity_cls.model_validate(
|
|
292
|
+
return entity_cls.model_validate(doc) if doc else None
|
|
268
293
|
|
|
269
294
|
|
|
270
295
|
class IRepository(Generic[T], metaclass=RepositoryMeta):
|
|
@@ -284,43 +309,31 @@ class IRepository(Generic[T], metaclass=RepositoryMeta):
|
|
|
284
309
|
)
|
|
285
310
|
|
|
286
311
|
def save(self, pyd_model: T) -> str:
|
|
287
|
-
|
|
312
|
+
# Base model override handles exclude_none and by_alias automatically
|
|
313
|
+
data = pyd_model.model_dump()
|
|
288
314
|
inserted_data = self.collection.insert_one(data)
|
|
289
|
-
return inserted_data.inserted_id
|
|
315
|
+
return str(inserted_data.inserted_id)
|
|
290
316
|
|
|
291
317
|
def save_all(self, pyd_models: List[T]) -> List[str]:
|
|
292
|
-
data = [pyd_model for pyd_model in pyd_models]
|
|
318
|
+
data = [pyd_model.model_dump() for pyd_model in pyd_models]
|
|
293
319
|
inserted_data = self.collection.insert_many(data)
|
|
294
|
-
return inserted_data.inserted_ids
|
|
320
|
+
return [str(obj_id) for obj_id in inserted_data.inserted_ids]
|
|
295
321
|
|
|
296
|
-
def find_all(self) ->
|
|
297
|
-
"""
|
|
298
|
-
Fetches all documents matching a query and maps them safely to
|
|
299
|
-
the repository's bound Pydantic model.
|
|
300
|
-
"""
|
|
301
|
-
# 1. Fetch raw cursor streams from MongoDB
|
|
322
|
+
def find_all(self) -> DocumentList[T]:
|
|
302
323
|
cursor = self.collection.find()
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
# def sanitize(doc):
|
|
306
|
-
# if doc and "_id" in doc:
|
|
307
|
-
# doc = dict(doc)
|
|
308
|
-
# doc["_id"] = str(doc["_id"])
|
|
309
|
-
# return doc
|
|
310
|
-
|
|
311
|
-
# 3. Instantiate using self._entity_cls (which holds the real model at runtime)
|
|
312
|
-
return [self._entity_cls(**dict(data_dict)) for data_dict in cursor]
|
|
324
|
+
# UPDATE: Wrap the list comprehension in DocumentList
|
|
325
|
+
return DocumentList([self._entity_cls.model_validate(doc) for doc in cursor])
|
|
313
326
|
|
|
314
327
|
def find(self, query_dict: dict) -> T:
|
|
315
|
-
data = self.collection.
|
|
328
|
+
data = self.collection.find_one(query_dict)
|
|
316
329
|
if not data:
|
|
317
330
|
raise ValueError("No data found")
|
|
318
|
-
return
|
|
331
|
+
return self._entity_cls.model_validate(data)
|
|
319
332
|
|
|
320
333
|
def delete(self, resource_id: str) -> bool:
|
|
321
334
|
query_result = self.collection.delete_one({"_id": ObjectId(resource_id)})
|
|
322
335
|
return query_result.deleted_count > 0
|
|
323
336
|
|
|
324
|
-
def update(self, resource_id: str, data:dict):
|
|
337
|
+
def update(self, resource_id: str, data: dict) -> bool:
|
|
325
338
|
query_result = self.collection.update_one({"_id": ObjectId(resource_id)}, {"$set": data})
|
|
326
339
|
return query_result.modified_count > 0
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
ppa/__init__.py,sha256=hPJEX6p9ISqFn9oCk29_HIZYIFVUF_XLEk02dg5fl9c,44
|
|
2
|
+
ppa/config.py,sha256=BpnjdXIYjKe7P8HGKFwZkkTbHf94734dhuT1ki4x0UM,8430
|
|
3
|
+
ppa/mongo/__init__.py,sha256=tioeX330txLUWpEyAuj8rCO6C8DISjRkNFVDs8wGe54,168
|
|
4
|
+
ppa/mongo/interface.py,sha256=lKDtd1KOXIjl9gKPGP_WVoKsE54_esJadNqKIHTadcw,12684
|
|
5
|
+
python_ppa-0.1.5.dist-info/licenses/LICENSE,sha256=J2MOuVHjIUNmdrDLa3O35gQz6E_wAJh9Ke3dod_ZDWc,1071
|
|
6
|
+
python_ppa-0.1.5.dist-info/METADATA,sha256=SGw0-dKyDKSE5A2gFEPiJ6JMHDpJZA9Bpi78jtrBUSY,4903
|
|
7
|
+
python_ppa-0.1.5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
python_ppa-0.1.5.dist-info/top_level.txt,sha256=43G6at2pfqB1LsMChCc2_K8WwSSt-LoYBAYN6Sf-krk,4
|
|
9
|
+
python_ppa-0.1.5.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
ppa/__init__.py,sha256=hPJEX6p9ISqFn9oCk29_HIZYIFVUF_XLEk02dg5fl9c,44
|
|
2
|
-
ppa/config.py,sha256=BpnjdXIYjKe7P8HGKFwZkkTbHf94734dhuT1ki4x0UM,8430
|
|
3
|
-
ppa/mongo/__init__.py,sha256=eOEbRiNLeS8zU6vhyNTIZqOCS6Pg_W_aAfe1JFbq5qk,122
|
|
4
|
-
ppa/mongo/interface.py,sha256=pp8SE35l6sNxBhXjnfaR1jQf6t5ac7VFumSA3cTdW2U,11949
|
|
5
|
-
python_ppa-0.1.3.dist-info/licenses/LICENSE,sha256=J2MOuVHjIUNmdrDLa3O35gQz6E_wAJh9Ke3dod_ZDWc,1071
|
|
6
|
-
python_ppa-0.1.3.dist-info/METADATA,sha256=lLN8XxLOfCrpRjlrkcDgbHexnY1gL924XWVjtBjP2fU,4903
|
|
7
|
-
python_ppa-0.1.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
-
python_ppa-0.1.3.dist-info/top_level.txt,sha256=43G6at2pfqB1LsMChCc2_K8WwSSt-LoYBAYN6Sf-krk,4
|
|
9
|
-
python_ppa-0.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|