lightodm 0.1.0__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.
lightodm/__init__.py CHANGED
@@ -23,7 +23,7 @@ Example:
23
23
  await user.asave()
24
24
  """
25
25
 
26
- __version__ = "0.1.0"
26
+ __version__ = "0.2.0"
27
27
 
28
28
  from lightodm.connection import (
29
29
  MongoConnection,
@@ -35,12 +35,13 @@ from lightodm.connection import (
35
35
  get_database,
36
36
  get_mongo_connection,
37
37
  )
38
- from lightodm.model import MongoBaseModel, generate_id
38
+ from lightodm.model import MongoBaseModel, generate_composite_id, generate_id
39
39
 
40
40
  __all__ = [
41
41
  # Model
42
42
  "MongoBaseModel",
43
43
  "generate_id",
44
+ "generate_composite_id",
44
45
  # Connection
45
46
  "MongoConnection",
46
47
  "connect",
lightodm/connection.py CHANGED
@@ -298,6 +298,7 @@ def connect(
298
298
  Initialize MongoDB connection with optional explicit parameters.
299
299
 
300
300
  If parameters are not provided, they will be read from environment variables:
301
+
301
302
  - MONGO_URL
302
303
  - MONGO_USER
303
304
  - MONGO_PASSWORD
@@ -312,7 +313,8 @@ def connect(
312
313
  Returns:
313
314
  PyMongo Database instance
314
315
 
315
- Example:
316
+ Example::
317
+
316
318
  # Connect with explicit parameters
317
319
  db = connect(
318
320
  url="mongodb://localhost:27017",
lightodm/model.py CHANGED
@@ -4,11 +4,12 @@ MongoDB Base Model for Pydantic
4
4
  Provides ODM functionality for MongoDB with both sync and async support.
5
5
  """
6
6
 
7
+ import hashlib
7
8
  from typing import AsyncIterator, Iterator, List, Optional, Type, TypeVar
8
9
 
9
10
  from bson import ObjectId
10
11
  from motor.motor_asyncio import AsyncIOMotorCollection
11
- from pydantic import BaseModel, ConfigDict, Field
12
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
12
13
  from pymongo.collection import Collection as PyMongoCollection
13
14
 
14
15
  from lightodm.connection import get_async_database, get_collection
@@ -27,6 +28,20 @@ def generate_id() -> str:
27
28
  return str(ObjectId())
28
29
 
29
30
 
31
+ def generate_composite_id(values: list) -> str:
32
+ """
33
+ Generate MD5 hash from list of values for composite key.
34
+
35
+ Args:
36
+ values: List of values to concatenate and hash
37
+
38
+ Returns:
39
+ 32-character hexadecimal MD5 hash string
40
+ """
41
+ concatenated = "".join(str(v) for v in values)
42
+ return hashlib.md5(concatenated.encode("utf-8")).hexdigest()
43
+
44
+
30
45
  class MongoBaseModel(BaseModel):
31
46
  """
32
47
  Base class for MongoDB document models with ODM functionality.
@@ -66,6 +81,7 @@ class MongoBaseModel(BaseModel):
66
81
  # Settings inner class - must be overridden in subclasses
67
82
  class Settings:
68
83
  name: Optional[str] = None # MongoDB collection name
84
+ composite_key: Optional[List[str]] = None # Fields for composite key ID
69
85
 
70
86
  @classmethod
71
87
  def _uses_mongo_id_alias(cls) -> bool:
@@ -77,6 +93,34 @@ class MongoBaseModel(BaseModel):
77
93
  alias = getattr(field, "validation_alias", None)
78
94
  return alias == "_id"
79
95
 
96
+ @model_validator(mode="after")
97
+ def _compute_composite_key(self):
98
+ """Compute composite key ID if defined in Settings."""
99
+ composite_key = getattr(self.Settings, "composite_key", None)
100
+
101
+ if composite_key is None:
102
+ return self
103
+
104
+ if not isinstance(composite_key, (list, tuple)) or len(composite_key) == 0:
105
+ raise ValueError("composite_key must be a non-empty list")
106
+
107
+ values = []
108
+ for field_name in composite_key:
109
+ # Access model_fields from the class, not the instance (Pydantic v2.11+)
110
+ if field_name not in self.__class__.model_fields and not hasattr(self, field_name):
111
+ raise ValueError(f"Composite key field '{field_name}' not found in model")
112
+
113
+ value = getattr(self, field_name, None)
114
+ if value is None:
115
+ raise ValueError(
116
+ f"Composite key field '{field_name}' is None. All fields must have values."
117
+ )
118
+
119
+ values.append(value)
120
+
121
+ self.id = generate_composite_id(values)
122
+ return self
123
+
80
124
  def __init_subclass__(cls, **kwargs):
81
125
  """
82
126
  Validate Settings class is properly defined in subclass.
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lightodm
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Lightweight MongoDB ODM - Simple async/sync Object-Document Mapper for MongoDB
5
5
  Project-URL: Homepage, https://github.com/Aprova-GmbH/lightodm
6
6
  Project-URL: Repository, https://github.com/Aprova-GmbH/lightodm
7
- Project-URL: Documentation, https://github.com/Aprova-GmbH/lightodm#readme
7
+ Project-URL: Documentation, https://lightodm.readthedocs.io
8
8
  Project-URL: Issues, https://github.com/Aprova-GmbH/lightodm/issues
9
+ Project-URL: Changelog, https://github.com/Aprova-GmbH/lightodm/blob/main/CHANGELOG.md
9
10
  Author-email: Andrey Vykhodtsev <vya@aprova.ch>
10
11
  License: Apache-2.0
11
12
  License-File: LICENSE
@@ -179,6 +180,37 @@ product = Product(name="Widget", sku="WDG-001")
179
180
  print(product.id) # Generated ObjectId string
180
181
  ```
181
182
 
183
+ ### Composite Keys
184
+
185
+ For multi-tenant applications or when you need deterministic IDs based on multiple fields, use composite keys:
186
+
187
+ ```python
188
+ from lightodm import MongoBaseModel
189
+
190
+ class TenantUser(MongoBaseModel):
191
+ class Settings:
192
+ name = "tenant_users"
193
+ composite_key = ["tenant_id", "user_id"] # Order matters
194
+
195
+ tenant_id: str
196
+ user_id: str
197
+ data: str
198
+
199
+ # ID is computed as MD5 hash of concatenated field values
200
+ user = TenantUser(tenant_id="tenant1", user_id="user1", data="test")
201
+ print(user.id) # e.g., "a1b2c3..." - always the same for same tenant_id + user_id
202
+
203
+ # Same values always produce the same ID (idempotent)
204
+ user2 = TenantUser(tenant_id="tenant1", user_id="user1", data="different")
205
+ assert user.id == user2.id # True - composite key only uses specified fields
206
+ ```
207
+
208
+ Composite key behavior:
209
+ - Field order in `composite_key` list determines concatenation order
210
+ - All composite key fields must have non-None values
211
+ - Composite key takes precedence over explicitly provided IDs
212
+ - Produces a 32-character hexadecimal MD5 hash
213
+
182
214
  ### Custom Connection
183
215
 
184
216
  Override `get_collection()` or `get_async_collection()` for custom connection logic:
@@ -0,0 +1,8 @@
1
+ lightodm/__init__.py,sha256=Md1CP92RVTVxtMpT37gMp8UfgO7b7V5q6ogiJUSnbVI,1134
2
+ lightodm/connection.py,sha256=r-MJUXDDyybRnsUTQJpi5HwA2NmHdhW7nk9KZ_oGI2Q,9891
3
+ lightodm/model.py,sha256=uOviHHudJ4cnw0ULLmfkpEb7nC9NAPSKeDAw3XFiHkE,20678
4
+ lightodm/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ lightodm-0.2.0.dist-info/METADATA,sha256=w7Vzmf9e3CGbeziIvycHIA2_16AECk7MFGbyFTBSClI,11053
6
+ lightodm-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
7
+ lightodm-0.2.0.dist-info/licenses/LICENSE,sha256=fL7HOAbNeitdXBSp3bN-4VK6kH26E8ScX3vv5qSsAQI,11345
8
+ lightodm-0.2.0.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- lightodm/__init__.py,sha256=HWLNWbrsx_hzn2Vx8qi3gaQWLK6mRn0GaQ_SLc5KdtI,1082
2
- lightodm/connection.py,sha256=6iNaSVO9BVA6Y8XoOoULBw7i__h4L3WBjnJKGLjpR9s,9888
3
- lightodm/model.py,sha256=po98-T2OQQTIwVxVi7PsG4j8FOGzqwzMisKLDW1OmJA,19103
4
- lightodm/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- lightodm-0.1.0.dist-info/METADATA,sha256=4snZ6jkissXHIFJElPJxG_TCIN9yF7RvDoRE-Wx8BNc,9921
6
- lightodm-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
7
- lightodm-0.1.0.dist-info/licenses/LICENSE,sha256=fL7HOAbNeitdXBSp3bN-4VK6kH26E8ScX3vv5qSsAQI,11345
8
- lightodm-0.1.0.dist-info/RECORD,,