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 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()