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 +3 -2
- lightodm/connection.py +3 -1
- lightodm/model.py +45 -1
- {lightodm-0.1.0.dist-info → lightodm-0.2.0.dist-info}/METADATA +34 -2
- lightodm-0.2.0.dist-info/RECORD +8 -0
- lightodm-0.1.0.dist-info/RECORD +0 -8
- {lightodm-0.1.0.dist-info → lightodm-0.2.0.dist-info}/WHEEL +0 -0
- {lightodm-0.1.0.dist-info → lightodm-0.2.0.dist-info}/licenses/LICENSE +0 -0
lightodm/__init__.py
CHANGED
|
@@ -23,7 +23,7 @@ Example:
|
|
|
23
23
|
await user.asave()
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
-
__version__ = "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.
|
|
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://
|
|
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,,
|
lightodm-0.1.0.dist-info/RECORD
DELETED
|
@@ -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,,
|
|
File without changes
|
|
File without changes
|