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