maleo-database 0.0.4__py3-none-any.whl → 0.0.6__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.
@@ -0,0 +1,427 @@
1
+ import traceback
2
+ from abc import ABC, abstractmethod
3
+ from datetime import datetime, timezone
4
+ from elasticsearch import AsyncElasticsearch, Elasticsearch
5
+ from motor.motor_asyncio import AsyncIOMotorClient
6
+ from pymongo import MongoClient
7
+ from redis.asyncio import Redis as AsyncRedis
8
+ from redis import Redis as SyncRedis
9
+ from sqlalchemy import MetaData, text
10
+ from typing import Generic, Optional, TypeVar
11
+ from uuid import uuid4
12
+ from maleo.dtos.authentication import GenericAuthentication
13
+ from maleo.dtos.contexts.operation import generate_operation_context
14
+ from maleo.dtos.contexts.request import RequestContext
15
+ from maleo.dtos.contexts.service import ServiceContext
16
+ from maleo.enums.operation import (
17
+ OperationType,
18
+ SystemOperationType,
19
+ Origin,
20
+ Layer,
21
+ Target,
22
+ )
23
+ from maleo.exceptions import MaleoException, InternalServerError
24
+ from maleo.logging.enums import Level
25
+ from maleo.logging.logger import Database
26
+ from maleo.mixins.timestamp import OperationTimestamp
27
+ from maleo.schemas.operation.system import (
28
+ SystemOperationAction,
29
+ SuccessfulSystemOperation,
30
+ )
31
+ from maleo.schemas.response import NoDataResponse
32
+ from maleo.types.base.uuid import OptionalUUID
33
+ from ..config import (
34
+ MySQLDatabaseConfig,
35
+ PostgreSQLDatabaseConfig,
36
+ SQLiteDatabaseConfig,
37
+ SQLServerDatabaseConfig,
38
+ SQLConfigT,
39
+ ElasticsearchDatabaseConfig,
40
+ MongoDBDatabaseConfig,
41
+ RedisDatabaseConfig,
42
+ NoSQLConfigT,
43
+ ConfigT,
44
+ )
45
+ from ..enums import Connection
46
+ from .client import (
47
+ AsyncClientT,
48
+ SyncClientT,
49
+ ClientManager,
50
+ )
51
+ from .engine import EngineManager
52
+ from .session import SessionManager
53
+
54
+
55
+ class DatabaseManager(ABC, Generic[ConfigT]):
56
+ def __init__(
57
+ self,
58
+ config: ConfigT,
59
+ logger: Database,
60
+ service_context: Optional[ServiceContext] = None,
61
+ ) -> None:
62
+ super().__init__()
63
+ self._config = config
64
+ self._logger = logger
65
+ self._service_context = (
66
+ service_context
67
+ if service_context is not None
68
+ else ServiceContext.from_env()
69
+ )
70
+ self._operation_context = generate_operation_context(
71
+ origin=Origin.SERVICE,
72
+ layer=Layer.UTILITY,
73
+ target=Target.DATABASE,
74
+ )
75
+
76
+ @abstractmethod
77
+ async def async_check_connection(
78
+ self,
79
+ operation_id: OptionalUUID = None,
80
+ request_context: Optional[RequestContext] = None,
81
+ authentication: Optional[GenericAuthentication] = None,
82
+ ) -> bool:
83
+ pass
84
+
85
+ @abstractmethod
86
+ def sync_check_connection(
87
+ self,
88
+ operation_id: OptionalUUID = None,
89
+ request_context: Optional[RequestContext] = None,
90
+ authentication: Optional[GenericAuthentication] = None,
91
+ ) -> bool:
92
+ pass
93
+
94
+ @abstractmethod
95
+ async def dispose(self):
96
+ pass
97
+
98
+
99
+ class SQLDatabaseManager(DatabaseManager[SQLConfigT], Generic[SQLConfigT]):
100
+ def __init__(
101
+ self,
102
+ config: SQLConfigT,
103
+ metadata: MetaData,
104
+ logger: Database,
105
+ service_context: Optional[ServiceContext] = None,
106
+ ) -> None:
107
+ super().__init__(config, logger, service_context)
108
+ self._metadata = metadata
109
+ self._operation_context.target.details = self._config.model_dump()
110
+ self._engine_manager = EngineManager[SQLConfigT](config)
111
+ self._session_manager = SessionManager(
112
+ config=config,
113
+ engines=self._engine_manager.get_all(),
114
+ logger=self._logger,
115
+ service_context=self._service_context,
116
+ )
117
+ self._metadata.create_all(bind=self._engine_manager.get(Connection.SYNC))
118
+
119
+ @property
120
+ def engine(self) -> EngineManager[SQLConfigT]:
121
+ return self._engine_manager
122
+
123
+ @property
124
+ def session(self) -> SessionManager:
125
+ return self._session_manager
126
+
127
+ async def async_check_connnection(
128
+ self,
129
+ operation_id: OptionalUUID = None,
130
+ request_context: Optional[RequestContext] = None,
131
+ authentication: Optional[GenericAuthentication] = None,
132
+ ) -> bool:
133
+ """Check database connectivity by executing a simple query."""
134
+ operation_id = operation_id if operation_id is not None else uuid4()
135
+ operation_action = SystemOperationAction(
136
+ type=SystemOperationType.DATABASE_CONNECTION, details=None
137
+ )
138
+ executed_at = datetime.now(tz=timezone.utc)
139
+ try:
140
+ async with self._session_manager.get(
141
+ Connection.ASYNC,
142
+ operation_id=operation_id,
143
+ request_context=request_context,
144
+ authentication=authentication,
145
+ ) as session:
146
+ await session.execute(text("SELECT 1"))
147
+ completed_at = datetime.now(tz=timezone.utc)
148
+ operation_timestamp = OperationTimestamp(
149
+ executed_at=executed_at,
150
+ completed_at=completed_at,
151
+ duration=(completed_at - executed_at).total_seconds(),
152
+ )
153
+ SuccessfulSystemOperation[
154
+ Optional[GenericAuthentication], NoDataResponse[None]
155
+ ](
156
+ service_context=self._service_context,
157
+ id=operation_id,
158
+ context=self._operation_context,
159
+ timestamp=operation_timestamp,
160
+ summary="Database connectivity check successful",
161
+ request_context=request_context,
162
+ authentication=authentication,
163
+ action=operation_action,
164
+ response=NoDataResponse[None](metadata=None, other=None),
165
+ ).log(
166
+ self._logger, Level.INFO
167
+ )
168
+ return True
169
+ except MaleoException:
170
+ return False
171
+ except Exception as e:
172
+ completed_at = datetime.now(tz=timezone.utc)
173
+ operation_timestamp = OperationTimestamp(
174
+ executed_at=executed_at,
175
+ completed_at=completed_at,
176
+ duration=(completed_at - executed_at).total_seconds(),
177
+ )
178
+ error = InternalServerError[Optional[GenericAuthentication]](
179
+ OperationType.SYSTEM,
180
+ service_context=self._service_context,
181
+ operation_id=operation_id,
182
+ operation_context=self._operation_context,
183
+ operation_timestamp=operation_timestamp,
184
+ operation_summary="Unexpected error occured checking database connection",
185
+ request_context=request_context,
186
+ authentication=authentication,
187
+ operation_action=operation_action,
188
+ details={
189
+ "exc_type": type(e).__name__,
190
+ "exc_data": {
191
+ "message": str(e),
192
+ "args": e.args,
193
+ },
194
+ },
195
+ )
196
+ operation = error.generate_operation(OperationType.SYSTEM)
197
+ operation.log(self._logger, level=Level.ERROR)
198
+ return False
199
+
200
+ def sync_check_connection(
201
+ self,
202
+ operation_id: OptionalUUID = None,
203
+ request_context: Optional[RequestContext] = None,
204
+ authentication: Optional[GenericAuthentication] = None,
205
+ ) -> bool:
206
+ """Check database connectivity by executing a simple query."""
207
+ operation_id = operation_id if operation_id is not None else uuid4()
208
+ operation_action = SystemOperationAction(
209
+ type=SystemOperationType.DATABASE_CONNECTION, details=None
210
+ )
211
+ executed_at = datetime.now(tz=timezone.utc)
212
+ try:
213
+ with self._session_manager.get(
214
+ Connection.SYNC,
215
+ operation_id=operation_id,
216
+ request_context=request_context,
217
+ authentication=authentication,
218
+ ) as session:
219
+ session.execute(text("SELECT 1"))
220
+ completed_at = datetime.now(tz=timezone.utc)
221
+ operation_timestamp = OperationTimestamp(
222
+ executed_at=executed_at,
223
+ completed_at=completed_at,
224
+ duration=(completed_at - executed_at).total_seconds(),
225
+ )
226
+ SuccessfulSystemOperation[
227
+ Optional[GenericAuthentication], NoDataResponse[None]
228
+ ](
229
+ service_context=self._service_context,
230
+ id=operation_id,
231
+ context=self._operation_context,
232
+ timestamp=operation_timestamp,
233
+ summary="Database connectivity check successful",
234
+ request_context=request_context,
235
+ authentication=authentication,
236
+ action=operation_action,
237
+ response=NoDataResponse[None](metadata=None, other=None),
238
+ ).log(
239
+ self._logger, Level.INFO
240
+ )
241
+ return True
242
+ except MaleoException:
243
+ return False
244
+ except Exception as e:
245
+ completed_at = datetime.now(tz=timezone.utc)
246
+ operation_timestamp = OperationTimestamp(
247
+ executed_at=executed_at,
248
+ completed_at=completed_at,
249
+ duration=(completed_at - executed_at).total_seconds(),
250
+ )
251
+ error = InternalServerError[Optional[GenericAuthentication]](
252
+ OperationType.SYSTEM,
253
+ service_context=self._service_context,
254
+ operation_id=operation_id,
255
+ operation_context=self._operation_context,
256
+ operation_timestamp=operation_timestamp,
257
+ operation_summary="Unexpected error occured checking database connection",
258
+ request_context=request_context,
259
+ authentication=authentication,
260
+ operation_action=operation_action,
261
+ details={
262
+ "exc_type": type(e).__name__,
263
+ "exc_data": {
264
+ "message": str(e),
265
+ "args": e.args,
266
+ },
267
+ },
268
+ )
269
+ operation = error.generate_operation(OperationType.SYSTEM)
270
+ operation.log(self._logger, level=Level.ERROR)
271
+ return False
272
+
273
+ async def dispose(self):
274
+ self._session_manager.dispose()
275
+ await self._engine_manager.dispose()
276
+
277
+
278
+ class MySQLDatabaseManager(SQLDatabaseManager[MySQLDatabaseConfig]):
279
+ pass
280
+
281
+
282
+ class PostgreSQLDatabaseManager(SQLDatabaseManager[PostgreSQLDatabaseConfig]):
283
+ pass
284
+
285
+
286
+ class SQLiteDatabaseManager(SQLDatabaseManager[SQLiteDatabaseConfig]):
287
+ pass
288
+
289
+
290
+ class SQLServerDatabaseManager(SQLDatabaseManager[SQLServerDatabaseConfig]):
291
+ pass
292
+
293
+
294
+ SQLDatabaseManagerT = TypeVar(
295
+ "SQLDatabaseManagerT",
296
+ MySQLDatabaseManager,
297
+ PostgreSQLDatabaseManager,
298
+ SQLiteDatabaseManager,
299
+ SQLServerDatabaseManager,
300
+ )
301
+
302
+
303
+ class NoSQLDatabaseManager(
304
+ DatabaseManager[NoSQLConfigT],
305
+ Generic[
306
+ NoSQLConfigT,
307
+ AsyncClientT,
308
+ SyncClientT,
309
+ ],
310
+ ):
311
+ def __init__(
312
+ self,
313
+ config: NoSQLConfigT,
314
+ metadata: MetaData,
315
+ logger: Database,
316
+ service_context: Optional[ServiceContext] = None,
317
+ ) -> None:
318
+ super().__init__(config, logger, service_context)
319
+ self._metadata = metadata
320
+ self._operation_context.target.details = self._config.model_dump()
321
+ self._client_manager = ClientManager[
322
+ NoSQLConfigT,
323
+ AsyncClientT,
324
+ SyncClientT,
325
+ ](config)
326
+
327
+ @property
328
+ def client(self) -> ClientManager[
329
+ NoSQLConfigT,
330
+ AsyncClientT,
331
+ SyncClientT,
332
+ ]:
333
+ return self._client_manager
334
+
335
+ async def async_check_connnection(
336
+ self,
337
+ operation_id: OptionalUUID = None,
338
+ request_context: Optional[RequestContext] = None,
339
+ authentication: Optional[GenericAuthentication] = None,
340
+ ) -> bool:
341
+ """Check client connectivity by executing a simple query."""
342
+ client = self._client_manager.get(Connection.ASYNC)
343
+ try:
344
+ if isinstance(client, AsyncElasticsearch):
345
+ return await client.ping()
346
+ elif isinstance(client, AsyncIOMotorClient):
347
+ db = client.get_database(str(self._config.connection.database))
348
+ await db.command("ping")
349
+ return True
350
+ elif isinstance(client, AsyncRedis):
351
+ await client.ping()
352
+ return True
353
+ else:
354
+ raise TypeError(f"Invalid client type: '{type(client)}'")
355
+ except Exception:
356
+ print("Unable to check client connection:\n", traceback.format_exc())
357
+ return False
358
+
359
+ def sync_check_connection(
360
+ self,
361
+ operation_id: OptionalUUID = None,
362
+ request_context: Optional[RequestContext] = None,
363
+ authentication: Optional[GenericAuthentication] = None,
364
+ ) -> bool:
365
+ """Check client connectivity by executing a simple query."""
366
+ client = self._client_manager.get(Connection.SYNC)
367
+ try:
368
+ if isinstance(client, Elasticsearch):
369
+ return client.ping()
370
+ elif isinstance(client, MongoClient):
371
+ db = client.get_database(str(self._config.connection.database))
372
+ db.command("ping")
373
+ return True
374
+ elif isinstance(client, SyncRedis):
375
+ client.ping()
376
+ return True
377
+ else:
378
+ raise TypeError(f"Invalid client type: '{type(client)}'")
379
+ except Exception:
380
+ print("Unable to check client connection:\n", traceback.format_exc())
381
+ return False
382
+
383
+ async def dispose(self):
384
+ await self._client_manager.dispose()
385
+
386
+
387
+ class ElasticsearchDatabaseManager(
388
+ NoSQLDatabaseManager[ElasticsearchDatabaseConfig, AsyncElasticsearch, Elasticsearch]
389
+ ):
390
+ pass
391
+
392
+
393
+ class MongoDBDatabaseManager(
394
+ NoSQLDatabaseManager[MongoDBDatabaseConfig, AsyncIOMotorClient, MongoClient]
395
+ ):
396
+ pass
397
+
398
+
399
+ class RedisDatabaseManager(
400
+ NoSQLDatabaseManager[RedisDatabaseConfig, AsyncRedis, SyncRedis]
401
+ ):
402
+ pass
403
+
404
+
405
+ NoSQLDatabaseManagerT = TypeVar(
406
+ "NoSQLDatabaseManagerT",
407
+ ElasticsearchDatabaseManager,
408
+ MongoDBDatabaseManager,
409
+ RedisDatabaseManager,
410
+ )
411
+
412
+
413
+ GenericDatabaseManagerT = TypeVar(
414
+ "GenericDatabaseManagerT", SQLDatabaseManager, NoSQLDatabaseManager
415
+ )
416
+
417
+
418
+ SpecificDatabaseManagerT = TypeVar(
419
+ "SpecificDatabaseManagerT",
420
+ MySQLDatabaseManager,
421
+ PostgreSQLDatabaseManager,
422
+ SQLiteDatabaseManager,
423
+ SQLServerDatabaseManager,
424
+ ElasticsearchDatabaseManager,
425
+ MongoDBDatabaseManager,
426
+ RedisDatabaseManager,
427
+ )
@@ -0,0 +1,82 @@
1
+ from elasticsearch import AsyncElasticsearch, Elasticsearch
2
+ from motor.motor_asyncio import AsyncIOMotorClient
3
+ from pymongo import MongoClient
4
+ from redis.asyncio import Redis as AsyncRedis
5
+ from redis import Redis as SyncRedis
6
+ from typing import Generic, Literal, Tuple, TypeVar, Union, overload
7
+ from ..config import (
8
+ ElasticsearchDatabaseConfig,
9
+ MongoDBDatabaseConfig,
10
+ RedisDatabaseConfig,
11
+ NoSQLConfigT,
12
+ )
13
+ from ..enums import Connection
14
+
15
+
16
+ AsyncClientT = TypeVar(
17
+ "AsyncClientT", AsyncElasticsearch, AsyncIOMotorClient, AsyncRedis
18
+ )
19
+
20
+
21
+ SyncClientT = TypeVar("SyncClientT", Elasticsearch, MongoClient, SyncRedis)
22
+
23
+
24
+ class ClientManager(
25
+ Generic[
26
+ NoSQLConfigT,
27
+ AsyncClientT,
28
+ SyncClientT,
29
+ ]
30
+ ):
31
+ def __init__(self, config: NoSQLConfigT) -> None:
32
+ super().__init__()
33
+ self._config = config
34
+
35
+ self._async_client = self._config.create_client(Connection.ASYNC)
36
+ self._sync_client = self._config.create_client(Connection.SYNC)
37
+
38
+ @overload
39
+ def get(self, connection: Literal[Connection.ASYNC]) -> AsyncClientT: ...
40
+ @overload
41
+ def get(self, connection: Literal[Connection.SYNC]) -> SyncClientT: ...
42
+ def get(
43
+ self, connection: Connection = Connection.ASYNC
44
+ ) -> Union[AsyncClientT, SyncClientT]:
45
+ if connection is Connection.ASYNC:
46
+ return self._async_client
47
+ elif connection is Connection.SYNC:
48
+ return self._sync_client
49
+
50
+ def get_all(self) -> Tuple[AsyncClientT, SyncClientT]:
51
+ return (self._async_client, self._sync_client)
52
+
53
+ async def dispose(self):
54
+ if isinstance(self._async_client, AsyncIOMotorClient):
55
+ self._async_client.close()
56
+ else:
57
+ await self._async_client.close()
58
+ self._sync_client.close()
59
+
60
+
61
+ class ElasticsearchClientManager(
62
+ ClientManager[ElasticsearchDatabaseConfig, AsyncElasticsearch, Elasticsearch]
63
+ ):
64
+ pass
65
+
66
+
67
+ class MongoDBClientManager(
68
+ ClientManager[MongoDBDatabaseConfig, AsyncIOMotorClient, MongoClient]
69
+ ):
70
+ pass
71
+
72
+
73
+ class RedisClientManager(ClientManager[RedisDatabaseConfig, AsyncRedis, SyncRedis]):
74
+ pass
75
+
76
+
77
+ ClientManagerT = TypeVar(
78
+ "ClientManagerT",
79
+ ElasticsearchClientManager,
80
+ MongoDBClientManager,
81
+ RedisClientManager,
82
+ )
@@ -0,0 +1,64 @@
1
+ from sqlalchemy.engine import Engine
2
+ from sqlalchemy.ext.asyncio import AsyncEngine
3
+ from typing import Generic, Literal, Tuple, TypeVar, Union, overload
4
+ from ..config import (
5
+ MySQLDatabaseConfig,
6
+ PostgreSQLDatabaseConfig,
7
+ SQLiteDatabaseConfig,
8
+ SQLServerDatabaseConfig,
9
+ SQLConfigT,
10
+ )
11
+ from ..enums import Connection
12
+
13
+
14
+ class EngineManager(Generic[SQLConfigT]):
15
+ def __init__(self, config: SQLConfigT) -> None:
16
+ super().__init__()
17
+ self._config = config
18
+
19
+ self._async_engine: AsyncEngine = self._config.create_engine(Connection.ASYNC)
20
+ self._sync_engine: Engine = self._config.create_engine(Connection.SYNC)
21
+
22
+ @overload
23
+ def get(self, connection: Literal[Connection.ASYNC]) -> AsyncEngine: ...
24
+ @overload
25
+ def get(self, connection: Literal[Connection.SYNC]) -> Engine: ...
26
+ def get(
27
+ self, connection: Connection = Connection.ASYNC
28
+ ) -> Union[AsyncEngine, Engine]:
29
+ if connection is Connection.ASYNC:
30
+ return self._async_engine
31
+ elif connection is Connection.SYNC:
32
+ return self._sync_engine
33
+
34
+ def get_all(self) -> Tuple[AsyncEngine, Engine]:
35
+ return (self._async_engine, self._sync_engine)
36
+
37
+ async def dispose(self):
38
+ await self._async_engine.dispose()
39
+ self._sync_engine.dispose()
40
+
41
+
42
+ class MySQLEngineManager(EngineManager[MySQLDatabaseConfig]):
43
+ pass
44
+
45
+
46
+ class PostgreSQLEngineManager(EngineManager[PostgreSQLDatabaseConfig]):
47
+ pass
48
+
49
+
50
+ class SQLiteEngineManager(EngineManager[SQLiteDatabaseConfig]):
51
+ pass
52
+
53
+
54
+ class SQLServerEngineManager(EngineManager[SQLServerDatabaseConfig]):
55
+ pass
56
+
57
+
58
+ EngineManagerT = TypeVar(
59
+ "EngineManagerT",
60
+ MySQLEngineManager,
61
+ PostgreSQLEngineManager,
62
+ SQLiteEngineManager,
63
+ SQLServerEngineManager,
64
+ )