lightodm 0.1.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 +53 -0
- lightodm/connection.py +338 -0
- lightodm/model.py +613 -0
- lightodm/py.typed +0 -0
- lightodm-0.1.0.dist-info/METADATA +346 -0
- lightodm-0.1.0.dist-info/RECORD +8 -0
- lightodm-0.1.0.dist-info/WHEEL +4 -0
- lightodm-0.1.0.dist-info/licenses/LICENSE +201 -0
lightodm/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LightODM - Lightweight MongoDB ODM
|
|
3
|
+
|
|
4
|
+
A simple, lightweight Object-Document Mapper (ODM) for MongoDB with full async/sync support.
|
|
5
|
+
Alternative to Beanie with zero dependencies beyond Pydantic, PyMongo, and Motor.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
from lightodm import MongoBaseModel
|
|
9
|
+
|
|
10
|
+
class User(MongoBaseModel):
|
|
11
|
+
class Settings:
|
|
12
|
+
name = "users"
|
|
13
|
+
|
|
14
|
+
name: str
|
|
15
|
+
email: str
|
|
16
|
+
age: int = None
|
|
17
|
+
|
|
18
|
+
# Sync usage
|
|
19
|
+
user = User(name="John", email="john@example.com")
|
|
20
|
+
user.save()
|
|
21
|
+
|
|
22
|
+
# Async usage
|
|
23
|
+
await user.asave()
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
|
|
28
|
+
from lightodm.connection import (
|
|
29
|
+
MongoConnection,
|
|
30
|
+
connect,
|
|
31
|
+
get_async_client,
|
|
32
|
+
get_async_database,
|
|
33
|
+
get_client,
|
|
34
|
+
get_collection,
|
|
35
|
+
get_database,
|
|
36
|
+
get_mongo_connection,
|
|
37
|
+
)
|
|
38
|
+
from lightodm.model import MongoBaseModel, generate_id
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
# Model
|
|
42
|
+
"MongoBaseModel",
|
|
43
|
+
"generate_id",
|
|
44
|
+
# Connection
|
|
45
|
+
"MongoConnection",
|
|
46
|
+
"connect",
|
|
47
|
+
"get_mongo_connection",
|
|
48
|
+
"get_collection",
|
|
49
|
+
"get_async_database",
|
|
50
|
+
"get_database",
|
|
51
|
+
"get_client",
|
|
52
|
+
"get_async_client",
|
|
53
|
+
]
|
lightodm/connection.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MongoDB Connection Manager
|
|
3
|
+
|
|
4
|
+
Thread-safe singleton connection manager for MongoDB supporting both sync (pymongo)
|
|
5
|
+
and async (motor) clients with automatic cleanup.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import atexit
|
|
9
|
+
import os
|
|
10
|
+
import threading
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
|
|
14
|
+
from pymongo import MongoClient
|
|
15
|
+
from pymongo.collection import Collection
|
|
16
|
+
from pymongo.database import Database
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MongoConnection:
|
|
20
|
+
"""
|
|
21
|
+
Singleton MongoDB connection manager supporting both sync (pymongo)
|
|
22
|
+
and async (motor) clients with thread-safety.
|
|
23
|
+
|
|
24
|
+
The connection is configured via environment variables:
|
|
25
|
+
- MONGO_URL: MongoDB connection URL
|
|
26
|
+
- MONGO_USER: MongoDB username
|
|
27
|
+
- MONGO_PASSWORD: MongoDB password
|
|
28
|
+
- MONGO_DB_NAME: Database name
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
# Sync usage
|
|
32
|
+
conn = MongoConnection()
|
|
33
|
+
db = conn.database
|
|
34
|
+
collection = conn.get_collection("users")
|
|
35
|
+
|
|
36
|
+
# Async usage
|
|
37
|
+
conn = MongoConnection()
|
|
38
|
+
client = await conn.get_async_client()
|
|
39
|
+
db = await conn.get_async_database()
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
_instance: Optional["MongoConnection"] = None
|
|
43
|
+
_lock = threading.Lock()
|
|
44
|
+
|
|
45
|
+
# Sync client
|
|
46
|
+
_client: Optional[MongoClient] = None
|
|
47
|
+
_db: Optional[Database] = None
|
|
48
|
+
|
|
49
|
+
# Async client (motor)
|
|
50
|
+
_async_client: Optional[AsyncIOMotorClient] = None
|
|
51
|
+
|
|
52
|
+
def __new__(cls):
|
|
53
|
+
if cls._instance is None:
|
|
54
|
+
with cls._lock:
|
|
55
|
+
if cls._instance is None:
|
|
56
|
+
cls._instance = super().__new__(cls)
|
|
57
|
+
return cls._instance
|
|
58
|
+
|
|
59
|
+
def __init__(self):
|
|
60
|
+
# Initialize sync client eagerly (to keep existing behavior)
|
|
61
|
+
if self._client is None:
|
|
62
|
+
self._initialize_connection()
|
|
63
|
+
|
|
64
|
+
def _initialize_connection(self):
|
|
65
|
+
"""Initialize synchronous MongoDB connection"""
|
|
66
|
+
mongo_url = os.environ.get("MONGO_URL")
|
|
67
|
+
mongo_user = os.environ.get("MONGO_USER")
|
|
68
|
+
mongo_password = os.environ.get("MONGO_PASSWORD")
|
|
69
|
+
mongo_db_name = os.environ.get("MONGO_DB_NAME")
|
|
70
|
+
|
|
71
|
+
if not all([mongo_url, mongo_user, mongo_password]):
|
|
72
|
+
raise ValueError(
|
|
73
|
+
"MongoDB connection parameters are not set. "
|
|
74
|
+
"Please set MONGO_URL, MONGO_USER, and MONGO_PASSWORD environment variables."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
self._client = MongoClient(
|
|
79
|
+
mongo_url,
|
|
80
|
+
username=mongo_user,
|
|
81
|
+
password=mongo_password,
|
|
82
|
+
maxPoolSize=50,
|
|
83
|
+
minPoolSize=5,
|
|
84
|
+
maxIdleTimeMS=30000,
|
|
85
|
+
serverSelectionTimeoutMS=5000,
|
|
86
|
+
socketTimeoutMS=20000,
|
|
87
|
+
connectTimeoutMS=20000,
|
|
88
|
+
heartbeatFrequencyMS=10000,
|
|
89
|
+
retryWrites=True,
|
|
90
|
+
retryReads=True,
|
|
91
|
+
maxConnecting=2,
|
|
92
|
+
waitQueueTimeoutMS=10000,
|
|
93
|
+
)
|
|
94
|
+
self._db = (
|
|
95
|
+
self._client[mongo_db_name]
|
|
96
|
+
if mongo_db_name
|
|
97
|
+
else self._client.get_default_database()
|
|
98
|
+
)
|
|
99
|
+
# Test the connection
|
|
100
|
+
self._client.admin.command("ping")
|
|
101
|
+
atexit.register(self.close_connection)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
raise ConnectionError(f"Failed to initialize MongoDB (sync) connection: {e}") from e
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def client(self) -> MongoClient:
|
|
107
|
+
"""Get the synchronous MongoDB client"""
|
|
108
|
+
if self._client is None:
|
|
109
|
+
self._initialize_connection()
|
|
110
|
+
return self._client
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def database(self) -> Database:
|
|
114
|
+
"""Get the synchronous MongoDB database"""
|
|
115
|
+
if self._db is None:
|
|
116
|
+
self._initialize_connection()
|
|
117
|
+
return self._db
|
|
118
|
+
|
|
119
|
+
def get_collection(self, collection_name: str) -> Collection:
|
|
120
|
+
"""
|
|
121
|
+
Get a synchronous collection.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
collection_name: Name of the collection
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
PyMongo Collection instance
|
|
128
|
+
"""
|
|
129
|
+
if self._db is None:
|
|
130
|
+
self._initialize_connection()
|
|
131
|
+
return self._db[collection_name]
|
|
132
|
+
|
|
133
|
+
async def get_async_client(self) -> AsyncIOMotorClient:
|
|
134
|
+
"""
|
|
135
|
+
Get or create the AsyncIOMotorClient and verify connectivity asynchronously.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Motor AsyncIOMotorClient instance
|
|
139
|
+
"""
|
|
140
|
+
if self._async_client is None:
|
|
141
|
+
mongo_url = os.environ.get("MONGO_URL")
|
|
142
|
+
mongo_user = os.environ.get("MONGO_USER")
|
|
143
|
+
mongo_password = os.environ.get("MONGO_PASSWORD")
|
|
144
|
+
|
|
145
|
+
if not all([mongo_url, mongo_user, mongo_password]):
|
|
146
|
+
raise ValueError(
|
|
147
|
+
"MongoDB connection parameters are not set. "
|
|
148
|
+
"Please set MONGO_URL, MONGO_USER, and MONGO_PASSWORD environment variables."
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
# Create motor client lazily
|
|
153
|
+
self._async_client = AsyncIOMotorClient(
|
|
154
|
+
mongo_url,
|
|
155
|
+
username=mongo_user,
|
|
156
|
+
password=mongo_password,
|
|
157
|
+
)
|
|
158
|
+
# Perform an async ping to ensure connectivity
|
|
159
|
+
await self._async_client.admin.command("ping")
|
|
160
|
+
except Exception as e:
|
|
161
|
+
# Ensure no half-initialized client remains
|
|
162
|
+
if self._async_client is not None:
|
|
163
|
+
self._async_client.close()
|
|
164
|
+
self._async_client = None
|
|
165
|
+
raise ConnectionError(
|
|
166
|
+
f"Failed to initialize MongoDB (async) connection: {e}"
|
|
167
|
+
) from e
|
|
168
|
+
|
|
169
|
+
return self._async_client
|
|
170
|
+
|
|
171
|
+
async def get_async_database(self, db_name: Optional[str] = None) -> AsyncIOMotorDatabase:
|
|
172
|
+
"""
|
|
173
|
+
Get the asynchronous MongoDB database.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
db_name: Optional database name override
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Motor AsyncIOMotorDatabase instance
|
|
180
|
+
"""
|
|
181
|
+
client = await self.get_async_client()
|
|
182
|
+
if db_name:
|
|
183
|
+
return client[db_name]
|
|
184
|
+
|
|
185
|
+
mongo_db_name = os.environ.get("MONGO_DB_NAME")
|
|
186
|
+
if not mongo_db_name:
|
|
187
|
+
return client.get_default_database()
|
|
188
|
+
return client[mongo_db_name]
|
|
189
|
+
|
|
190
|
+
def close_connection(self):
|
|
191
|
+
"""Close both sync and async clients if present."""
|
|
192
|
+
# Close sync client
|
|
193
|
+
if self._client:
|
|
194
|
+
try:
|
|
195
|
+
self._client.close()
|
|
196
|
+
except Exception:
|
|
197
|
+
pass
|
|
198
|
+
finally:
|
|
199
|
+
self._client = None
|
|
200
|
+
self._db = None
|
|
201
|
+
|
|
202
|
+
# Close async client
|
|
203
|
+
if self._async_client:
|
|
204
|
+
try:
|
|
205
|
+
# Motor's close is synchronous method
|
|
206
|
+
self._async_client.close()
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
finally:
|
|
210
|
+
self._async_client = None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# Global connection instance
|
|
214
|
+
_mongo_conn: Optional[MongoConnection] = None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def get_mongo_connection() -> MongoConnection:
|
|
218
|
+
"""
|
|
219
|
+
Get the singleton MongoConnection instance.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
MongoConnection singleton
|
|
223
|
+
"""
|
|
224
|
+
global _mongo_conn
|
|
225
|
+
if _mongo_conn is None:
|
|
226
|
+
_mongo_conn = MongoConnection()
|
|
227
|
+
return _mongo_conn
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def get_collection(collection_name: str) -> Collection:
|
|
231
|
+
"""
|
|
232
|
+
Get a MongoDB collection by name using the singleton connection (sync).
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
collection_name: Name of the collection
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
PyMongo Collection instance
|
|
239
|
+
"""
|
|
240
|
+
conn = get_mongo_connection()
|
|
241
|
+
return conn.get_collection(collection_name)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
async def get_async_database(db_name: Optional[str] = None) -> AsyncIOMotorDatabase:
|
|
245
|
+
"""
|
|
246
|
+
Get asynchronous MongoDB database using the singleton connection.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
db_name: Optional database name override
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Motor AsyncIOMotorDatabase instance
|
|
253
|
+
"""
|
|
254
|
+
conn = get_mongo_connection()
|
|
255
|
+
return await conn.get_async_database(db_name)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def get_database() -> Database:
|
|
259
|
+
"""
|
|
260
|
+
Get synchronous MongoDB database using the singleton connection.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
PyMongo Database instance
|
|
264
|
+
"""
|
|
265
|
+
conn = get_mongo_connection()
|
|
266
|
+
return conn.database
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def get_client() -> MongoClient:
|
|
270
|
+
"""
|
|
271
|
+
Get synchronous MongoDB client using the singleton connection.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
PyMongo MongoClient instance
|
|
275
|
+
"""
|
|
276
|
+
conn = get_mongo_connection()
|
|
277
|
+
return conn.client
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
async def get_async_client() -> AsyncIOMotorClient:
|
|
281
|
+
"""
|
|
282
|
+
Get asynchronous MongoDB client using the singleton connection.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Motor AsyncIOMotorClient instance
|
|
286
|
+
"""
|
|
287
|
+
conn = get_mongo_connection()
|
|
288
|
+
return await conn.get_async_client()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def connect(
|
|
292
|
+
url: Optional[str] = None,
|
|
293
|
+
username: Optional[str] = None,
|
|
294
|
+
password: Optional[str] = None,
|
|
295
|
+
db_name: Optional[str] = None,
|
|
296
|
+
) -> Database:
|
|
297
|
+
"""
|
|
298
|
+
Initialize MongoDB connection with optional explicit parameters.
|
|
299
|
+
|
|
300
|
+
If parameters are not provided, they will be read from environment variables:
|
|
301
|
+
- MONGO_URL
|
|
302
|
+
- MONGO_USER
|
|
303
|
+
- MONGO_PASSWORD
|
|
304
|
+
- MONGO_DB_NAME
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
url: MongoDB connection URL (optional)
|
|
308
|
+
username: MongoDB username (optional)
|
|
309
|
+
password: MongoDB password (optional)
|
|
310
|
+
db_name: Database name (optional)
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
PyMongo Database instance
|
|
314
|
+
|
|
315
|
+
Example:
|
|
316
|
+
# Connect with explicit parameters
|
|
317
|
+
db = connect(
|
|
318
|
+
url="mongodb://localhost:27017",
|
|
319
|
+
username="myuser",
|
|
320
|
+
password="mypass",
|
|
321
|
+
db_name="mydb"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Or use environment variables
|
|
325
|
+
db = connect()
|
|
326
|
+
"""
|
|
327
|
+
# Set environment variables if provided
|
|
328
|
+
if url:
|
|
329
|
+
os.environ["MONGO_URL"] = url
|
|
330
|
+
if username:
|
|
331
|
+
os.environ["MONGO_USER"] = username
|
|
332
|
+
if password:
|
|
333
|
+
os.environ["MONGO_PASSWORD"] = password
|
|
334
|
+
if db_name:
|
|
335
|
+
os.environ["MONGO_DB_NAME"] = db_name
|
|
336
|
+
|
|
337
|
+
# Initialize connection and return database
|
|
338
|
+
return get_database()
|