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 CHANGED
@@ -1,3 +1,4 @@
1
1
  from ppa.mongo.interface import IRepository
2
2
  from ppa.mongo.interface import query
3
- from ppa.mongo.interface import document
3
+ from ppa.mongo.interface import document
4
+ from ppa.mongo.interface import DocumentModel
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=BaseModel)
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 exists, relying on user implementation")
90
- return cls
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] # Inject whatever object type the user passed
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
- def sanitize_output(doc: Any) -> Any:
257
- if doc and "_id" in doc:
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
- return [entity_cls.model_validate(sanitize_output(doc)) for doc in cursor]
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(sanitize_output(doc)) if doc else None
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
- data = pyd_model.model_dump(exclude_none=True)
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) -> list[T]:
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
- # 2. Local serialization helper to clean BSON primary keys
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.find(query_dict)
328
+ data = self.collection.find_one(query_dict)
316
329
  if not data:
317
330
  raise ValueError("No data found")
318
- return dict(data)
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-ppa
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: JPA-like ORM for Python
5
5
  Author: Pratyush Kumar
6
6
  License-Expression: MIT
@@ -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,,