database-wrapper 0.1.33__py3-none-any.whl → 0.1.37__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.
@@ -1,13 +1,11 @@
1
- import logging
1
+ from typing import AsyncGenerator, Any, overload
2
2
 
3
- from typing import AsyncGenerator, cast, Any, overload
4
-
5
- from .db_backend import DatabaseBackend
3
+ from .common import OrderByItem, DataModelType
6
4
  from .db_data_model import DBDataModel
7
- from .db_wrapper_interface import DBWrapperInterface, OrderByItem, NoParam, T
5
+ from .db_wrapper_mixin import DBWrapperMixin
8
6
 
9
7
 
10
- class DBWrapperAsync(DBWrapperInterface):
8
+ class DBWrapperAsync(DBWrapperMixin):
11
9
  """
12
10
  Async Database wrapper class.
13
11
 
@@ -15,66 +13,10 @@ class DBWrapperAsync(DBWrapperInterface):
15
13
  It means you will need to call close method manually from async context.
16
14
  """
17
15
 
18
- ###########################
19
- ### Instance properties ###
20
- ###########################
21
-
22
- # Db backend
23
- db: Any
24
- """Database backend object"""
25
-
26
- dbConn: Any
27
- """
28
- Database connection object.
29
-
30
- Its not always set. Currently is used as a placeholder for async connections.
31
- For sync connections db - DatabaseBackend.connection is used.
32
- """
33
-
34
- # logger
35
- logger: Any
36
- """Logger object"""
37
-
38
16
  #######################
39
17
  ### Class lifecycle ###
40
18
  #######################
41
19
 
42
- # Meta methods
43
- def __init__(
44
- self,
45
- db: DatabaseBackend,
46
- dbConn: Any = None,
47
- logger: logging.Logger | None = None,
48
- ):
49
- """
50
- Initializes a new instance of the DBWrapper class.
51
-
52
- Args:
53
- db (DatabaseBackend): The DatabaseBackend object.
54
- logger (logging.Logger, optional): The logger object. Defaults to None.
55
- """
56
- self.db = db
57
- self.dbConn = dbConn
58
-
59
- if logger is None:
60
- loggerName = f"{__name__}.{self.__class__.__name__}"
61
- self.logger = logging.getLogger(loggerName)
62
- else:
63
- self.logger = logger
64
-
65
- def __del__(self):
66
- """
67
- Deallocates the instance of the DBWrapper class.
68
- """
69
- self.logger.debug("Dealloc")
70
-
71
- # Force remove instances so that there are no circular references
72
- if hasattr(self, "db") and self.db:
73
- del self.db
74
-
75
- if hasattr(self, "dbConn") and self.dbConn:
76
- del self.dbConn
77
-
78
20
  async def close(self) -> None:
79
21
  """
80
22
  Async method for closing async resources.
@@ -85,22 +27,6 @@ class DBWrapperAsync(DBWrapperInterface):
85
27
  ### Helper methods ###
86
28
  ######################
87
29
 
88
- def makeIdentifier(self, schema: str | None, name: str) -> Any:
89
- """
90
- Creates a SQL identifier object from the given name.
91
-
92
- Args:
93
- schema (str | None): The schema to create the identifier from.
94
- name (str): The name to create the identifier from.
95
-
96
- Returns:
97
- str: The created SQL identifier object.
98
- """
99
- if schema:
100
- return f"{schema}.{name}"
101
-
102
- return name
103
-
104
30
  @overload
105
31
  async def createCursor(self) -> Any: ...
106
32
 
@@ -115,93 +41,32 @@ class DBWrapperAsync(DBWrapperInterface):
115
41
  emptyDataClass (T | None, optional): The data model to use for the cursor. Defaults to None.
116
42
 
117
43
  Returns:
118
- AsyncCursor[DictRow] | AsyncCursor[T]: The created cursor object.
44
+ The created cursor object.
119
45
  """
120
46
  assert self.db is not None, "Database connection is not set"
121
47
  return self.db.cursor
122
48
 
123
- def logQuery(self, cursor: Any, query: Any, params: tuple[Any, ...]) -> None:
124
- """
125
- Logs the given query and parameters.
126
-
127
- Args:
128
- cursor (Any): The database cursor.
129
- query (Any): The query to log.
130
- params (tuple[Any, ...]): The parameters to log.
131
- """
132
- self.logger.debug(f"Query: {query} with params: {params}")
133
-
134
- def turnDataIntoModel(
135
- self,
136
- emptyDataClass: T,
137
- dbData: dict[str, Any],
138
- ) -> T:
139
- """
140
- Turns the given data into a data model.
141
- By default we are pretty sure that there is no factory in the cursor,
142
- So we need to create a new instance of the data model and fill it with data
143
-
144
- Args:
145
- emptyDataClass (T): The data model to use.
146
- dbData (dict[str, Any]): The data to turn into a model.
147
-
148
- Returns:
149
- T: The data model filled with data.
150
- """
151
-
152
- result = emptyDataClass.__class__()
153
- result.fillDataFromDict(dbData)
154
- result.raw_data = dbData
155
- return result
156
-
157
49
  #####################
158
50
  ### Query methods ###
159
51
  #####################
160
52
 
161
- def filterQuery(self, schemaName: str | None, tableName: str) -> Any:
162
- """
163
- Creates a SQL query to filter data from the given table.
164
-
165
- Args:
166
- schemaName (str | None): The name of the schema to filter data from.
167
- tableName (str): The name of the table to filter data from.
168
-
169
- Returns:
170
- Any: The created SQL query object.
171
- """
172
- fullTableName = self.makeIdentifier(schemaName, tableName)
173
- return f"SELECT * FROM {fullTableName}"
174
-
175
- def limitQuery(self, offset: int = 0, limit: int = 100) -> Any:
176
- """
177
- Creates a SQL query to limit the number of results returned.
178
-
179
- Args:
180
- offset (int, optional): The number of results to skip. Defaults to 0.
181
- limit (int, optional): The maximum number of results to return. Defaults to 100.
182
-
183
- Returns:
184
- Any: The created SQL query object.
185
- """
186
- return f"LIMIT {limit} OFFSET {offset}"
187
-
188
53
  # Action methods
189
54
  async def getOne(
190
55
  self,
191
- emptyDataClass: T,
56
+ emptyDataClass: DataModelType,
192
57
  customQuery: Any = None,
193
- ) -> T | None:
58
+ ) -> DataModelType | None:
194
59
  """
195
- Retrieves a single record from the database.
60
+ Retrieves a single record from the database by class defined id.
196
61
 
197
62
  Args:
198
- emptyDataClass (T): The data model to use for the query.
63
+ emptyDataClass (DataModelType): The data model to use for the query.
199
64
  customQuery (Any, optional): The custom query to use for the query. Defaults to None.
200
65
 
201
66
  Returns:
202
- T | None: The result of the query.
67
+ DataModelType | None: The result of the query.
203
68
  """
204
- # Query
69
+ # Query and filter
205
70
  _query = (
206
71
  customQuery
207
72
  or emptyDataClass.queryBase()
@@ -214,18 +79,21 @@ class DBWrapperAsync(DBWrapperInterface):
214
79
  if not idValue:
215
80
  raise ValueError("Id value is not set")
216
81
 
82
+ _filter = f"WHERE {self.makeIdentifier(emptyDataClass.tableAlias, idKey)} = %s"
83
+ _params = (idValue,)
84
+
217
85
  # Create a SQL object for the query and format it
218
- querySql = f"{_query} WHERE {self.makeIdentifier(emptyDataClass.tableAlias, idKey)} = %s"
86
+ querySql = self._formatFilterQuery(_query, _filter, None, None)
219
87
 
220
88
  # Create a new cursor
221
89
  newCursor = await self.createCursor(emptyDataClass)
222
90
 
223
91
  # Log
224
- self.logQuery(newCursor, querySql, (idValue,))
92
+ self.logQuery(newCursor, querySql, _params)
225
93
 
226
94
  # Load data
227
95
  try:
228
- await newCursor.execute(querySql, (idValue,))
96
+ await newCursor.execute(querySql, _params)
229
97
 
230
98
  # Fetch one row
231
99
  row = await newCursor.fetchone()
@@ -240,42 +108,44 @@ class DBWrapperAsync(DBWrapperInterface):
240
108
 
241
109
  async def getByKey(
242
110
  self,
243
- emptyDataClass: T,
111
+ emptyDataClass: DataModelType,
244
112
  idKey: str,
245
113
  idValue: Any,
246
114
  customQuery: Any = None,
247
- ) -> T | None:
115
+ ) -> DataModelType | None:
248
116
  """
249
117
  Retrieves a single record from the database using the given key.
250
118
 
251
119
  Args:
252
- emptyDataClass (T): The data model to use for the query.
120
+ emptyDataClass (DataModelType): The data model to use for the query.
253
121
  idKey (str): The name of the key to use for the query.
254
122
  idValue (Any): The value of the key to use for the query.
255
123
  customQuery (Any, optional): The custom query to use for the query. Defaults to None.
256
124
 
257
125
  Returns:
258
- T | None: The result of the query.
126
+ DataModelType | None: The result of the query.
259
127
  """
260
- # Query
128
+ # Query and filter
261
129
  _query = (
262
130
  customQuery
263
131
  or emptyDataClass.queryBase()
264
132
  or self.filterQuery(emptyDataClass.schemaName, emptyDataClass.tableName)
265
133
  )
134
+ _filter = f"WHERE {self.makeIdentifier(emptyDataClass.tableAlias, idKey)} = %s"
135
+ _params = (idValue,)
266
136
 
267
137
  # Create a SQL object for the query and format it
268
- querySql = f"{_query} WHERE {self.makeIdentifier(emptyDataClass.tableAlias, idKey)} = %s"
138
+ querySql = self._formatFilterQuery(_query, _filter, None, None)
269
139
 
270
140
  # Create a new cursor
271
141
  newCursor = await self.createCursor(emptyDataClass)
272
142
 
273
143
  # Log
274
- self.logQuery(newCursor, querySql, (idValue,))
144
+ self.logQuery(newCursor, querySql, _params)
275
145
 
276
146
  # Load data
277
147
  try:
278
- await newCursor.execute(querySql, (idValue,))
148
+ await newCursor.execute(querySql, _params)
279
149
 
280
150
  # Fetch one row
281
151
  row = await newCursor.fetchone()
@@ -291,19 +161,19 @@ class DBWrapperAsync(DBWrapperInterface):
291
161
 
292
162
  async def getAll(
293
163
  self,
294
- emptyDataClass: T,
164
+ emptyDataClass: DataModelType,
295
165
  idKey: str | None = None,
296
166
  idValue: Any | None = None,
297
167
  orderBy: OrderByItem | None = None,
298
168
  offset: int = 0,
299
169
  limit: int = 100,
300
170
  customQuery: Any = None,
301
- ) -> AsyncGenerator[T, None]:
171
+ ) -> AsyncGenerator[DataModelType, None]:
302
172
  """
303
173
  Retrieves all records from the database.
304
174
 
305
175
  Args:
306
- emptyDataClass (T): The data model to use for the query.
176
+ emptyDataClass (DataModelType): The data model to use for the query.
307
177
  idKey (str | None, optional): The name of the key to use for filtering. Defaults to None.
308
178
  idValue (Any | None, optional): The value of the key to use for filtering. Defaults to None.
309
179
  orderBy (OrderByItem | None, optional): The order by item to use for sorting. Defaults to None.
@@ -312,36 +182,28 @@ class DBWrapperAsync(DBWrapperInterface):
312
182
  customQuery (Any, optional): The custom query to use for the query. Defaults to None.
313
183
 
314
184
  Returns:
315
- AsyncGenerator[T, None]: The result of the query.
185
+ AsyncGenerator[DataModelType, None]: The result of the query.
316
186
  """
317
- # Query
187
+ # Query and filter
318
188
  _query = (
319
189
  customQuery
320
190
  or emptyDataClass.queryBase()
321
191
  or self.filterQuery(emptyDataClass.schemaName, emptyDataClass.tableName)
322
192
  )
323
193
  _params: tuple[Any, ...] = ()
324
-
325
- # Filter
194
+ _filter = None
326
195
  if idKey and idValue:
327
- _query = f"{_query} WHERE {self.makeIdentifier(emptyDataClass.tableAlias, idKey)} = %s"
196
+ _filter = (
197
+ f"WHERE {self.makeIdentifier(emptyDataClass.tableAlias, idKey)} = %s"
198
+ )
328
199
  _params = (idValue,)
329
200
 
330
- # Limits
331
- _order = ""
332
- _limit = ""
333
-
334
- if orderBy:
335
- orderList = [
336
- f"{item[0]} {item[1] if len(item) > 1 and item[1] != None else 'ASC'}"
337
- for item in orderBy
338
- ]
339
- _order = "ORDER BY %s" % ", ".join(orderList)
340
- if offset or limit:
341
- _limit = f"{self.limitQuery(offset, limit)}"
201
+ # Order and limit
202
+ _order = self.orderQuery(orderBy)
203
+ _limit = self.limitQuery(offset, limit)
342
204
 
343
205
  # Create a SQL object for the query and format it
344
- querySql = f"{_query} {_order} {_limit}"
206
+ querySql = self._formatFilterQuery(_query, _filter, _order, _limit)
345
207
 
346
208
  # Create a new cursor
347
209
  newCursor = await self.createCursor(emptyDataClass)
@@ -359,114 +221,36 @@ class DBWrapperAsync(DBWrapperInterface):
359
221
  row = await newCursor.fetchone()
360
222
  if row is None:
361
223
  break
224
+
362
225
  yield self.turnDataIntoModel(emptyDataClass, row)
363
226
 
364
227
  finally:
365
228
  # Ensure the cursor is closed after the generator is exhausted or an error occurs
366
229
  await newCursor.close()
367
230
 
368
- def formatFilter(self, key: str, filter: Any) -> tuple[Any, ...]:
369
- if type(filter) is dict:
370
- if "$contains" in filter:
371
- return (
372
- f"{key} LIKE %s",
373
- f"%{filter['$contains']}%",
374
- )
375
- elif "$starts_with" in filter:
376
- return (f"{key} LIKE %s", f"{filter['$starts_with']}%")
377
- elif "$ends_with" in filter:
378
- return (f"{key} LIKE %s", f"%{filter['$ends_with']}")
379
- elif "$min" in filter and "$max" not in filter:
380
- return (f"{key} >= %s", filter["$min"]) # type: ignore
381
- elif "$max" in filter and "$min" not in filter:
382
- return (f"{key} <= %s", filter["$max"]) # type: ignore
383
- elif "$min" in filter and "$max" in filter:
384
- return (f"{key} BETWEEN %s AND %s", filter["$min"], filter["$max"]) # type: ignore
385
- elif "$in" in filter:
386
- inFilter1: list[Any] = cast(list[Any], filter["$in"])
387
- return (f"{key} IN (%s)" % ",".join(["%s"] * len(inFilter1)),) + tuple(
388
- inFilter1
389
- )
390
- elif "$not_in" in filter:
391
- inFilter2: list[Any] = cast(list[Any], filter["$in"])
392
- return (
393
- f"{key} NOT IN (%s)" % ",".join(["%s"] * len(inFilter2)),
394
- ) + tuple(inFilter2)
395
- elif "$not" in filter:
396
- return (f"{key} != %s", filter["$not"]) # type: ignore
397
-
398
- elif "$gt" in filter:
399
- return (f"{key} > %s", filter["$gt"]) # type: ignore
400
- elif "$gte" in filter:
401
- return (f"{key} >= %s", filter["$gte"]) # type: ignore
402
- elif "$lt" in filter:
403
- return (f"{key} < %s", filter["$lt"]) # type: ignore
404
- elif "$lte" in filter:
405
- return (f"{key} <= %s", filter["$lte"]) # type: ignore
406
- elif "$is_null" in filter:
407
- return (f"{key} IS NULL",) # type: ignore
408
- elif "$is_not_null" in filter:
409
- return (f"{key} IS NOT NULL",) # type: ignore
410
-
411
- raise NotImplementedError("Filter type not supported")
412
- elif type(filter) is str or type(filter) is int or type(filter) is float:
413
- return (f"{key} = %s", filter)
414
- elif type(filter) is bool:
415
- return (
416
- f"{key} = TRUE" if filter else f"{key} = FALSE",
417
- NoParam,
418
- )
419
- else:
420
- raise NotImplementedError(
421
- f"Filter type not supported: {key} = {type(filter)}"
422
- )
423
-
424
- def createFilter(
425
- self, filter: dict[str, Any] | None
426
- ) -> tuple[str, tuple[Any, ...]]:
427
- if filter is None or len(filter) == 0:
428
- return ("", tuple())
429
-
430
- raw = [self.formatFilter(key, filter[key]) for key in filter]
431
- _query = " AND ".join([tup[0] for tup in raw])
432
- _query = f"WHERE {_query}"
433
- _params = tuple([val for tup in raw for val in tup[1:] if val is not NoParam])
434
-
435
- return (_query, _params)
436
-
437
231
  async def getFiltered(
438
232
  self,
439
- emptyDataClass: T,
233
+ emptyDataClass: DataModelType,
440
234
  filter: dict[str, Any],
441
235
  orderBy: OrderByItem | None = None,
442
236
  offset: int = 0,
443
237
  limit: int = 100,
444
238
  customQuery: Any = None,
445
- ) -> AsyncGenerator[T, None]:
446
- # Filter
239
+ ) -> AsyncGenerator[DataModelType, None]:
240
+ # Query and filter
447
241
  _query = (
448
242
  customQuery
449
243
  or emptyDataClass.queryBase()
450
244
  or self.filterQuery(emptyDataClass.schemaName, emptyDataClass.tableName)
451
245
  )
452
246
  (_filter, _params) = self.createFilter(filter)
453
- _filter = _filter
454
247
 
455
- # Limits
456
- _order = ""
457
- _limit = ""
248
+ # Order and limit
249
+ _order = self.orderQuery(orderBy)
250
+ _limit = self.limitQuery(offset, limit)
458
251
 
459
- if orderBy:
460
- orderList = [
461
- f"{item[0]} {item[1] if len(item) > 1 and item[1] != None else 'ASC'}"
462
- for item in orderBy
463
- ]
464
- _order = "ORDER BY %s" % ", ".join(orderList)
465
- if offset or limit:
466
- _limit = f"{self.limitQuery(offset, limit)}"
467
-
468
- # Create a SQL object for the query and format it
469
- querySql = f"{_query} {_filter} {_order} {_limit}"
252
+ # Create SQL query
253
+ querySql = self._formatFilterQuery(_query, _filter, _order, _limit)
470
254
 
471
255
  # Create a new cursor
472
256
  newCursor = await self.createCursor(emptyDataClass)
@@ -484,6 +268,7 @@ class DBWrapperAsync(DBWrapperInterface):
484
268
  row = await newCursor.fetchone()
485
269
  if row is None:
486
270
  break
271
+
487
272
  yield self.turnDataIntoModel(emptyDataClass, row)
488
273
 
489
274
  finally:
@@ -511,20 +296,10 @@ class DBWrapperAsync(DBWrapperInterface):
511
296
  Returns:
512
297
  tuple[int, int]: The id of the record and the number of affected rows.
513
298
  """
514
- keys = storeData.keys()
515
299
  values = list(storeData.values())
516
-
517
300
  tableIdentifier = self.makeIdentifier(schemaName, tableName)
518
301
  returnKey = self.makeIdentifier(emptyDataClass.tableAlias, idKey)
519
-
520
- columns = ", ".join(keys)
521
- valuesPlaceholder = ", ".join(["%s"] * len(values))
522
- insertQuery = (
523
- f"INSERT INTO {tableIdentifier} "
524
- f"({columns}) "
525
- f"VALUES ({valuesPlaceholder}) "
526
- f"RETURNING {returnKey}"
527
- )
302
+ insertQuery = self._formatInsertQuery(tableIdentifier, storeData, returnKey)
528
303
 
529
304
  # Create a new cursor
530
305
  newCursor = await self.createCursor(emptyDataClass)
@@ -548,21 +323,21 @@ class DBWrapperAsync(DBWrapperInterface):
548
323
  await newCursor.close()
549
324
 
550
325
  @overload
551
- async def store(self, records: T) -> tuple[int, int]: # type: ignore
326
+ async def store(self, records: DataModelType) -> tuple[int, int]: # type: ignore
552
327
  ...
553
328
 
554
329
  @overload
555
- async def store(self, records: list[T]) -> list[tuple[int, int]]: ...
330
+ async def store(self, records: list[DataModelType]) -> list[tuple[int, int]]: ...
556
331
 
557
332
  async def store(
558
333
  self,
559
- records: T | list[T],
334
+ records: DataModelType | list[DataModelType],
560
335
  ) -> tuple[int, int] | list[tuple[int, int]]:
561
336
  """
562
337
  Stores a record or a list of records in the database.
563
338
 
564
339
  Args:
565
- records (T | list[T]): The record or records to store.
340
+ records (DataModelType | list[DataModelType]): The record or records to store.
566
341
 
567
342
  Returns:
568
343
  tuple[int, int] | list[tuple[int, int]]: The id of the record and
@@ -621,17 +396,12 @@ class DBWrapperAsync(DBWrapperInterface):
621
396
  int: The number of affected rows.
622
397
  """
623
398
  (idKey, idValue) = updateId
624
- keys = updateData.keys()
625
399
  values = list(updateData.values())
626
400
  values.append(idValue)
627
401
 
628
- set_clause = ", ".join(f"{key} = %s" for key in keys)
629
-
630
402
  tableIdentifier = self.makeIdentifier(schemaName, tableName)
631
403
  updateKey = self.makeIdentifier(emptyDataClass.tableAlias, idKey)
632
- updateQuery = (
633
- f"UPDATE {tableIdentifier} SET {set_clause} WHERE {updateKey} = %s"
634
- )
404
+ updateQuery = self._formatUpdateQuery(tableIdentifier, updateKey, updateData)
635
405
 
636
406
  # Create a new cursor
637
407
  newCursor = await self.createCursor(emptyDataClass)
@@ -651,18 +421,20 @@ class DBWrapperAsync(DBWrapperInterface):
651
421
  await newCursor.close()
652
422
 
653
423
  @overload
654
- async def update(self, records: T) -> int: # type: ignore
424
+ async def update(self, records: DataModelType) -> int: # type: ignore
655
425
  ...
656
426
 
657
427
  @overload
658
- async def update(self, records: list[T]) -> list[int]: ...
428
+ async def update(self, records: list[DataModelType]) -> list[int]: ...
659
429
 
660
- async def update(self, records: T | list[T]) -> int | list[int]:
430
+ async def update(
431
+ self, records: DataModelType | list[DataModelType]
432
+ ) -> int | list[int]:
661
433
  """
662
434
  Updates a record or a list of records in the database.
663
435
 
664
436
  Args:
665
- records (T | list[T]): The record or records to update.
437
+ records (DataModelType | list[DataModelType]): The record or records to update.
666
438
 
667
439
  Returns:
668
440
  int | list[int]: The number of affected rows for a single record or a list of
@@ -745,7 +517,7 @@ class DBWrapperAsync(DBWrapperInterface):
745
517
 
746
518
  tableIdentifier = self.makeIdentifier(schemaName, tableName)
747
519
  deleteKey = self.makeIdentifier(emptyDataClass.tableAlias, idKey)
748
- delete_query = f"DELETE FROM {tableIdentifier} WHERE {deleteKey} = %s"
520
+ delete_query = self._formatDeleteQuery(tableIdentifier, deleteKey)
749
521
 
750
522
  # Create a new cursor
751
523
  newCursor = await self.createCursor(emptyDataClass)
@@ -765,18 +537,20 @@ class DBWrapperAsync(DBWrapperInterface):
765
537
  await newCursor.close()
766
538
 
767
539
  @overload
768
- async def delete(self, records: T) -> int: # type: ignore
540
+ async def delete(self, records: DataModelType) -> int: # type: ignore
769
541
  ...
770
542
 
771
543
  @overload
772
- async def delete(self, records: list[T]) -> list[int]: ...
544
+ async def delete(self, records: list[DataModelType]) -> list[int]: ...
773
545
 
774
- async def delete(self, records: T | list[T]) -> int | list[int]:
546
+ async def delete(
547
+ self, records: DataModelType | list[DataModelType]
548
+ ) -> int | list[int]:
775
549
  """
776
550
  Deletes a record or a list of records from the database.
777
551
 
778
552
  Args:
779
- records (T | list[T]): The record or records to delete.
553
+ records (DataModelType | list[DataModelType]): The record or records to delete.
780
554
 
781
555
  Returns:
782
556
  int | list[int]: The number of affected rows for a single record or a list of