database-wrapper-pgsql 0.1.28__tar.gz → 0.1.33__tar.gz

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.
Files changed (14) hide show
  1. {database_wrapper_pgsql-0.1.28 → database_wrapper_pgsql-0.1.33}/PKG-INFO +7 -7
  2. {database_wrapper_pgsql-0.1.28 → database_wrapper_pgsql-0.1.33}/README.md +5 -5
  3. {database_wrapper_pgsql-0.1.28 → database_wrapper_pgsql-0.1.33}/database_wrapper_pgsql/__init__.py +8 -3
  4. {database_wrapper_pgsql-0.1.28 → database_wrapper_pgsql-0.1.33}/database_wrapper_pgsql/connector.py +2 -2
  5. database_wrapper_pgsql-0.1.33/database_wrapper_pgsql/db_wrapper_pgsql.py +498 -0
  6. database_wrapper_pgsql-0.1.28/database_wrapper_pgsql/db_wrapper_pgsql.py → database_wrapper_pgsql-0.1.33/database_wrapper_pgsql/db_wrapper_pgsql_async.py +80 -118
  7. {database_wrapper_pgsql-0.1.28 → database_wrapper_pgsql-0.1.33}/database_wrapper_pgsql.egg-info/PKG-INFO +7 -7
  8. {database_wrapper_pgsql-0.1.28 → database_wrapper_pgsql-0.1.33}/database_wrapper_pgsql.egg-info/SOURCES.txt +1 -0
  9. {database_wrapper_pgsql-0.1.28 → database_wrapper_pgsql-0.1.33}/database_wrapper_pgsql.egg-info/requires.txt +1 -1
  10. {database_wrapper_pgsql-0.1.28 → database_wrapper_pgsql-0.1.33}/pyproject.toml +2 -2
  11. {database_wrapper_pgsql-0.1.28 → database_wrapper_pgsql-0.1.33}/database_wrapper_pgsql/py.typed +0 -0
  12. {database_wrapper_pgsql-0.1.28 → database_wrapper_pgsql-0.1.33}/database_wrapper_pgsql.egg-info/dependency_links.txt +0 -0
  13. {database_wrapper_pgsql-0.1.28 → database_wrapper_pgsql-0.1.33}/database_wrapper_pgsql.egg-info/top_level.txt +0 -0
  14. {database_wrapper_pgsql-0.1.28 → database_wrapper_pgsql-0.1.33}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: database_wrapper_pgsql
3
- Version: 0.1.28
3
+ Version: 0.1.33
4
4
  Summary: database_wrapper for PostgreSQL database
5
5
  Author-email: Gints Murans <gm@gm.lv>
6
6
  License: GNU General Public License v3.0 (GPL-3.0)
@@ -32,7 +32,7 @@ Classifier: Topic :: Software Development
32
32
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
33
33
  Requires-Python: >=3.8
34
34
  Description-Content-Type: text/markdown
35
- Requires-Dist: database_wrapper==0.1.28
35
+ Requires-Dist: database_wrapper==0.1.33
36
36
  Requires-Dist: psycopg[binary]>=3.2.0
37
37
  Requires-Dist: psycopg[pool]>=3.2.0
38
38
 
@@ -40,7 +40,7 @@ Requires-Dist: psycopg[pool]>=3.2.0
40
40
 
41
41
  _Part of the `database_wrapper` package._
42
42
 
43
- This python package is a database wrapper for [PostgreSQL](https://www.postgresql.org/) (also called pgsql) databases.
43
+ This python package is a database wrapper for [PostgreSQL](https://www.postgresql.org/) (also called pgsql) database.
44
44
 
45
45
  ## Installation
46
46
 
@@ -51,9 +51,9 @@ pip install database_wrapper[pgsql]
51
51
  ## Usage
52
52
 
53
53
  ```python
54
- from database_wrapper_pgsql import AsyncPgSQLWithPooling, DBWrapperPgSQL
54
+ from database_wrapper_pgsql import PgSQLWithPoolingAsync, DBWrapperPgSQLAsync
55
55
 
56
- db = MySQL({
56
+ db = PgSQLWithPoolingAsync({
57
57
  "hostname": "localhost",
58
58
  "port": 3306,
59
59
  "username": "root",
@@ -61,7 +61,7 @@ db = MySQL({
61
61
  "database": "my_database"
62
62
  })
63
63
  db.open()
64
- dbWrapper = DBWrapperMySQL(db=db)
64
+ dbWrapper = DBWrapperPgSQLAsync(db=db)
65
65
 
66
66
  # Simple query
67
67
  aModel = MyModel()
@@ -78,7 +78,7 @@ else:
78
78
  # Raw query
79
79
  res = await dbWrapper.getAll(
80
80
  aModel,
81
- """
81
+ customQuery="""
82
82
  SELECT t1.*, t2.name AS other_name
83
83
  FROM my_table AS t1
84
84
  LEFT JOIN other_table AS t2 ON t1.other_id = t2.id
@@ -2,7 +2,7 @@
2
2
 
3
3
  _Part of the `database_wrapper` package._
4
4
 
5
- This python package is a database wrapper for [PostgreSQL](https://www.postgresql.org/) (also called pgsql) databases.
5
+ This python package is a database wrapper for [PostgreSQL](https://www.postgresql.org/) (also called pgsql) database.
6
6
 
7
7
  ## Installation
8
8
 
@@ -13,9 +13,9 @@ pip install database_wrapper[pgsql]
13
13
  ## Usage
14
14
 
15
15
  ```python
16
- from database_wrapper_pgsql import AsyncPgSQLWithPooling, DBWrapperPgSQL
16
+ from database_wrapper_pgsql import PgSQLWithPoolingAsync, DBWrapperPgSQLAsync
17
17
 
18
- db = MySQL({
18
+ db = PgSQLWithPoolingAsync({
19
19
  "hostname": "localhost",
20
20
  "port": 3306,
21
21
  "username": "root",
@@ -23,7 +23,7 @@ db = MySQL({
23
23
  "database": "my_database"
24
24
  })
25
25
  db.open()
26
- dbWrapper = DBWrapperMySQL(db=db)
26
+ dbWrapper = DBWrapperPgSQLAsync(db=db)
27
27
 
28
28
  # Simple query
29
29
  aModel = MyModel()
@@ -40,7 +40,7 @@ else:
40
40
  # Raw query
41
41
  res = await dbWrapper.getAll(
42
42
  aModel,
43
- """
43
+ customQuery="""
44
44
  SELECT t1.*, t2.name AS other_name
45
45
  FROM my_table AS t1
46
46
  LEFT JOIN other_table AS t2 ON t1.other_id = t2.id
@@ -9,7 +9,8 @@ Part of the database_wrapper package
9
9
  import logging
10
10
 
11
11
  from .db_wrapper_pgsql import DBWrapperPgSQL
12
- from .connector import PgConfig, AsyncPgSQLWithPooling, PgSQL
12
+ from .db_wrapper_pgsql_async import DBWrapperPgSQLAsync
13
+ from .connector import PgConfig, PgSQL, PgSQLWithPoolingAsync
13
14
 
14
15
  # Set the logger to a quiet default, can be enabled if needed
15
16
  logger = logging.getLogger("database_wrapper_pgsql")
@@ -18,8 +19,12 @@ if logger.level == logging.NOTSET:
18
19
 
19
20
 
20
21
  __all__ = [
22
+ # Wrappers
21
23
  "DBWrapperPgSQL",
22
- "PgConfig",
23
- "AsyncPgSQLWithPooling",
24
+ "DBWrapperPgSQLAsync",
25
+ # Connectors
24
26
  "PgSQL",
27
+ "PgSQLWithPoolingAsync",
28
+ # Helpers
29
+ "PgConfig",
25
30
  ]
@@ -115,7 +115,7 @@ class PgSQL(DatabaseBackend):
115
115
  self.connection.rollback()
116
116
 
117
117
 
118
- class AsyncPgSQLWithPooling(DatabaseBackend):
118
+ class PgSQLWithPoolingAsync(DatabaseBackend):
119
119
  """
120
120
  PostgreSQL database implementation with async and connection pooling
121
121
 
@@ -223,7 +223,7 @@ class AsyncPgSQLWithPooling(DatabaseBackend):
223
223
  # Lets do some socket magic
224
224
  self.fixSocketTimeouts(connection.fileno())
225
225
 
226
- async with timer.aenter("AsyncPgSQLWithPooling.__aenter__.ping"):
226
+ async with timer.aenter("PgSQLWithPoolingAsync.__aenter__.ping"):
227
227
  async with connection.transaction():
228
228
  await cursor.execute("SELECT 1")
229
229
  await cursor.fetchone()
@@ -0,0 +1,498 @@
1
+ import logging
2
+ from typing import Any, Generator, overload
3
+
4
+ from psycopg import Cursor, sql
5
+ from psycopg.rows import class_row
6
+
7
+ from database_wrapper import T, OrderByItem, DBWrapper, DBDataModel
8
+
9
+ from .connector import (
10
+ # Sync
11
+ PgConnectionType,
12
+ PgCursorType,
13
+ PgSQL,
14
+ )
15
+
16
+
17
+ class DBWrapperPgSQL(DBWrapper):
18
+ """
19
+ Sync database wrapper for postgres
20
+ """
21
+
22
+ # Override db instance
23
+ db: PgSQL
24
+ """ PostgreSQL database connector """
25
+
26
+ dbConn: PgConnectionType | None = None
27
+ """ PostgreSQL connection object """
28
+
29
+ #######################
30
+ ### Class lifecycle ###
31
+ #######################
32
+
33
+ # Meta methods
34
+ def __init__(
35
+ self,
36
+ db: PgSQL,
37
+ dbConn: PgConnectionType | None = None,
38
+ logger: logging.Logger | None = None,
39
+ ):
40
+ """
41
+ Initializes a new instance of the DBWrapper class.
42
+
43
+ Args:
44
+ db (MySQL): The PostgreSQL connector.
45
+ dbConn (MySqlConnection, optional): The PostgreSQL connection object. Defaults to None.
46
+ logger (logging.Logger, optional): The logger object. Defaults to None.
47
+ """
48
+ super().__init__(db, dbConn, logger)
49
+
50
+ ######################
51
+ ### Helper methods ###
52
+ ######################
53
+
54
+ def makeIdentifier(self, schema: str | None, name: str) -> sql.Identifier | str:
55
+ """
56
+ Creates a SQL identifier object from the given name.
57
+
58
+ Args:
59
+ name (str): The name to create the identifier from.
60
+
61
+ Returns:
62
+ sql.Identifier: The created SQL identifier object.
63
+ """
64
+ if schema:
65
+ return sql.Identifier(schema, name)
66
+
67
+ return sql.Identifier(name)
68
+
69
+ @overload
70
+ def createCursor(self) -> PgCursorType: ...
71
+
72
+ @overload
73
+ def createCursor(
74
+ self,
75
+ emptyDataClass: T,
76
+ ) -> Cursor[T]: ...
77
+
78
+ def createCursor(
79
+ self,
80
+ emptyDataClass: T | None = None,
81
+ ) -> Cursor[T] | PgCursorType:
82
+ """
83
+ Creates a new cursor object.
84
+
85
+ Args:
86
+ emptyDataClass (DBDataModel | None, optional): The data model to use for the cursor.
87
+ Defaults to None.
88
+
89
+ Returns:
90
+ PgAsyncCursorType | AsyncCursor[DBDataModel]: The created cursor object.
91
+ """
92
+ assert self.db is not None, "Database connection is not set"
93
+
94
+ # First we need connection
95
+ if self.dbConn is None:
96
+ self.dbConn = self.db.connection
97
+
98
+ # Lets make sure we have a connection
99
+ if self.dbConn is None:
100
+ raise Exception("Failed to get connection")
101
+
102
+ if emptyDataClass is None:
103
+ return self.dbConn.cursor()
104
+
105
+ return self.dbConn.cursor(row_factory=class_row(emptyDataClass.__class__))
106
+
107
+ def logQuery(
108
+ self,
109
+ cursor: Cursor[Any],
110
+ query: sql.SQL | sql.Composed,
111
+ params: tuple[Any, ...],
112
+ ) -> None:
113
+ """
114
+ Logs the given query and parameters.
115
+
116
+ Args:
117
+ cursor (Any): The database cursor.
118
+ query (Any): The query to log.
119
+ params (tuple[Any, ...]): The parameters to log.
120
+ """
121
+ queryString = query.as_string(self.dbConn)
122
+ self.logger.debug(f"Query: {queryString}")
123
+
124
+ #####################
125
+ ### Query methods ###
126
+ #####################
127
+
128
+ def filterQuery(
129
+ self,
130
+ schemaName: str | None,
131
+ tableName: str,
132
+ ) -> sql.SQL | sql.Composed | str:
133
+ """
134
+ Creates a SQL query to filter data from the given table.
135
+
136
+ Args:
137
+ schemaName (str): The name of the schema to filter data from.
138
+ tableName (str): The name of the table to filter data from.
139
+
140
+ Returns:
141
+ sql.SQL | sql.Composed: The created SQL query object.
142
+ """
143
+ return sql.SQL("SELECT * FROM {table}").format(
144
+ table=self.makeIdentifier(schemaName, tableName)
145
+ )
146
+
147
+ def limitQuery(self, offset: int = 0, limit: int = 100) -> sql.Composed | sql.SQL:
148
+ return sql.SQL("LIMIT {} OFFSET {}").format(limit, offset)
149
+
150
+ # Action methods
151
+ def getOne(
152
+ self,
153
+ emptyDataClass: T,
154
+ customQuery: sql.SQL | sql.Composed | str | None = None,
155
+ ) -> T | None:
156
+ """
157
+ Retrieves a single record from the database.
158
+
159
+ Args:
160
+ emptyDataClass (T): The data model to use for the query.
161
+ customQuery (sql.SQL | sql.Composed | str | None, optional): The custom query to use.
162
+ Defaults to None.
163
+
164
+ Returns:
165
+ T | None: The result of the query.
166
+ """
167
+ # Query
168
+ _query = (
169
+ customQuery
170
+ or emptyDataClass.queryBase()
171
+ or self.filterQuery(emptyDataClass.schemaName, emptyDataClass.tableName)
172
+ )
173
+ idKey = emptyDataClass.idKey
174
+ idValue = emptyDataClass.id
175
+ if not idKey:
176
+ raise ValueError("Id key is not set")
177
+ if not idValue:
178
+ raise ValueError("Id value is not set")
179
+
180
+ # Create a SQL object for the query and format it
181
+ querySql = sql.SQL("{query} WHERE {idkey} = %s").format(
182
+ query=_query, idkey=self.makeIdentifier(emptyDataClass.tableAlias, idKey)
183
+ )
184
+
185
+ # Create a new cursor
186
+ newCursor = self.createCursor(emptyDataClass)
187
+
188
+ # Log
189
+ self.logQuery(newCursor, querySql, (idValue,))
190
+
191
+ # Load data
192
+ try:
193
+
194
+ newCursor.execute(querySql, (idValue,))
195
+ dbData = newCursor.fetchone()
196
+
197
+ return dbData
198
+
199
+ finally:
200
+ # Close the cursor
201
+ newCursor.close()
202
+
203
+ def getByKey(
204
+ self,
205
+ emptyDataClass: T,
206
+ idKey: str,
207
+ idValue: Any,
208
+ customQuery: sql.SQL | sql.Composed | str | None = None,
209
+ ) -> T | None:
210
+ """
211
+ Retrieves a single record from the database using the given key.
212
+
213
+ Args:
214
+ emptyDataClass (T): The data model to use for the query.
215
+ idKey (str): The name of the key to use for the query.
216
+ idValue (Any): The value of the key to use for the query.
217
+ customQuery (sql.SQL | sql.Composed | str | None, optional): The custom query to use.
218
+ Defaults to None.
219
+
220
+ Returns:
221
+ T | None: The result of the query.
222
+ """
223
+ # Query
224
+ _query = (
225
+ customQuery
226
+ or emptyDataClass.queryBase()
227
+ or self.filterQuery(emptyDataClass.schemaName, emptyDataClass.tableName)
228
+ )
229
+
230
+ # Create a SQL object for the query and format it
231
+ querySql = sql.SQL("{} WHERE {} = %s").format(
232
+ _query, self.makeIdentifier(emptyDataClass.tableAlias, idKey)
233
+ )
234
+
235
+ # Create a new cursor
236
+ newCursor = self.createCursor(emptyDataClass)
237
+
238
+ # Log
239
+ self.logQuery(newCursor, querySql, (idValue,))
240
+
241
+ # Load data
242
+ try:
243
+ newCursor.execute(querySql, (idValue,))
244
+ dbData = newCursor.fetchone()
245
+
246
+ return dbData
247
+
248
+ finally:
249
+ # Ensure the cursor is closed after the generator is exhausted or an error occurs
250
+ newCursor.close()
251
+
252
+ def getAll(
253
+ self,
254
+ emptyDataClass: T,
255
+ idKey: str | None = None,
256
+ idValue: Any | None = None,
257
+ orderBy: OrderByItem | None = None,
258
+ offset: int = 0,
259
+ limit: int = 100,
260
+ customQuery: sql.SQL | sql.Composed | str | None = None,
261
+ ) -> Generator[T, None, None]:
262
+ """
263
+ Retrieves all records from the database.
264
+
265
+ Args:
266
+ emptyDataClass (T): The data model to use for the query.
267
+ idKey (str | None, optional): The name of the key to use for filtering. Defaults to None.
268
+ idValue (Any | None, optional): The value of the key to use for filtering. Defaults to None.
269
+ orderBy (OrderByItem | None, optional): The order by item to use for sorting. Defaults to None.
270
+ offset (int, optional): The number of results to skip. Defaults to 0.
271
+ limit (int, optional): The maximum number of results to return. Defaults to 100.
272
+ customQuery (sql.SQL | sql.Composed | str | None, optional): The custom query to use. Defaults to None.
273
+
274
+ Returns:
275
+ Generator[T, None, None]: The result of the query.
276
+ """
277
+ # Query
278
+ _query = (
279
+ customQuery
280
+ or emptyDataClass.queryBase()
281
+ or self.filterQuery(emptyDataClass.schemaName, emptyDataClass.tableName)
282
+ )
283
+ _params: tuple[Any, ...] = ()
284
+
285
+ # Filter
286
+ if idKey and idValue:
287
+ _query = sql.SQL("{} WHERE {} = %s").format(
288
+ _query, self.makeIdentifier(emptyDataClass.tableAlias, idKey)
289
+ )
290
+ _params = (idValue,)
291
+
292
+ # Limits
293
+ _order: sql.Composable = sql.SQL("")
294
+ _limit: sql.Composable = sql.SQL("")
295
+
296
+ if orderBy:
297
+ orderList = [
298
+ f"{item[0]} {item[1] if len(item) > 1 and item[1] != None else 'ASC'}"
299
+ for item in orderBy
300
+ ]
301
+ _order = sql.SQL("ORDER BY %s" % ", ".join(orderList)) # type: ignore
302
+ if offset or limit:
303
+ _limit = sql.SQL("{}").format(self.limitQuery(offset, limit))
304
+
305
+ # Create a SQL object for the query and format it
306
+ querySql = sql.SQL("{query} {order} {limit}").format(
307
+ query=_query, order=_order, limit=_limit
308
+ )
309
+
310
+ # Create a new cursor
311
+ newCursor = self.createCursor(emptyDataClass)
312
+
313
+ # Log
314
+ self.logQuery(newCursor, querySql, _params)
315
+
316
+ # Load data
317
+ try:
318
+ newCursor.execute(querySql, _params)
319
+ while True:
320
+ row = newCursor.fetchone()
321
+ if row is None:
322
+ break
323
+ yield row
324
+
325
+ finally:
326
+ # Close the cursor
327
+ newCursor.close()
328
+
329
+ def getFiltered(
330
+ self,
331
+ emptyDataClass: T,
332
+ filter: dict[str, Any],
333
+ orderBy: OrderByItem | None = None,
334
+ offset: int = 0,
335
+ limit: int = 100,
336
+ customQuery: sql.SQL | sql.Composed | str | None = None,
337
+ ) -> Generator[T, None, None]:
338
+ # Filter
339
+ _query = (
340
+ customQuery
341
+ or emptyDataClass.queryBase()
342
+ or self.filterQuery(emptyDataClass.schemaName, emptyDataClass.tableName)
343
+ )
344
+ (_filter, _params) = self.createFilter(filter)
345
+ _filter = sql.SQL(_filter) # type: ignore
346
+
347
+ # Limits
348
+ _order: sql.Composable = sql.SQL("")
349
+ _limit: sql.Composable = sql.SQL("")
350
+
351
+ if orderBy:
352
+ orderList = [
353
+ f"{item[0]} {item[1] if len(item) > 1 and item[1] != None else 'ASC'}"
354
+ for item in orderBy
355
+ ]
356
+ _order = sql.SQL("ORDER BY %s" % ", ".join(orderList)) # type: ignore
357
+ if offset or limit:
358
+ _limit = sql.SQL("{}").format(self.limitQuery(offset, limit))
359
+
360
+ # Create a SQL object for the query and format it
361
+ querySql = sql.SQL("{query} {filter} {order} {limit}").format(
362
+ query=_query, filter=_filter, order=_order, limit=_limit
363
+ )
364
+
365
+ # Create a new cursor
366
+ newCursor = self.createCursor(emptyDataClass)
367
+
368
+ # Log
369
+ self.logQuery(newCursor, querySql, _params)
370
+
371
+ # Load data
372
+ try:
373
+ newCursor.execute(querySql, _params)
374
+ while True:
375
+ row = newCursor.fetchone()
376
+ if row is None:
377
+ break
378
+ yield row
379
+
380
+ finally:
381
+ # Close the cursor
382
+ newCursor.close()
383
+
384
+ def _store(
385
+ self,
386
+ emptyDataClass: DBDataModel,
387
+ schemaName: str | None,
388
+ tableName: str,
389
+ storeData: dict[str, Any],
390
+ idKey: str,
391
+ ) -> tuple[int, int]:
392
+ keys = storeData.keys()
393
+ values = list(storeData.values())
394
+
395
+ tableIdentifier = self.makeIdentifier(schemaName, tableName)
396
+ returnKey = self.makeIdentifier(emptyDataClass.tableAlias, idKey)
397
+
398
+ insertQuery = sql.SQL(
399
+ "INSERT INTO {table} ({columns}) VALUES ({values}) RETURNING {id_key}"
400
+ ).format(
401
+ table=tableIdentifier,
402
+ columns=sql.SQL(", ").join(map(sql.Identifier, keys)),
403
+ values=sql.SQL(", ").join(sql.Placeholder() * len(values)),
404
+ id_key=returnKey,
405
+ )
406
+
407
+ # Create a new cursor
408
+ newCursor = self.createCursor(emptyDataClass)
409
+
410
+ # Log
411
+ self.logQuery(newCursor, insertQuery, tuple(values))
412
+
413
+ # Insert
414
+ try:
415
+ newCursor.execute(insertQuery, tuple(values))
416
+ affectedRows = newCursor.rowcount
417
+ result = newCursor.fetchone()
418
+
419
+ return (
420
+ result.id if result and hasattr(result, "id") else 0,
421
+ affectedRows,
422
+ )
423
+
424
+ finally:
425
+ # Close the cursor
426
+ newCursor.close()
427
+
428
+ def _update(
429
+ self,
430
+ emptyDataClass: DBDataModel,
431
+ schemaName: str | None,
432
+ tableName: str,
433
+ updateData: dict[str, Any],
434
+ updateId: tuple[str, Any],
435
+ ) -> int:
436
+ (idKey, idValue) = updateId
437
+ keys = updateData.keys()
438
+ values = list(updateData.values())
439
+ values.append(idValue)
440
+
441
+ set_clause = sql.SQL(", ").join(
442
+ sql.Identifier(key) + sql.SQL(" = %s") for key in keys
443
+ )
444
+
445
+ tableIdentifier = self.makeIdentifier(schemaName, tableName)
446
+ updateKey = self.makeIdentifier(emptyDataClass.tableAlias, idKey)
447
+ updateQuery = sql.SQL(
448
+ "UPDATE {table} SET {set_clause} WHERE {id_key} = %s"
449
+ ).format(
450
+ table=tableIdentifier,
451
+ set_clause=set_clause,
452
+ id_key=updateKey,
453
+ )
454
+
455
+ # Create a new cursor
456
+ newCursor = self.createCursor(emptyDataClass)
457
+
458
+ # Log
459
+ self.logQuery(newCursor, updateQuery, tuple(values))
460
+
461
+ # Update
462
+ try:
463
+ newCursor.execute(updateQuery, tuple(values))
464
+ affectedRows = newCursor.rowcount
465
+
466
+ return affectedRows
467
+
468
+ finally:
469
+ # Close the cursor
470
+ newCursor.close()
471
+
472
+ def _delete(
473
+ self,
474
+ emptyDataClass: DBDataModel,
475
+ schemaName: str | None,
476
+ tableName: str,
477
+ deleteId: tuple[str, Any],
478
+ ) -> int:
479
+ (idKey, idValue) = deleteId
480
+
481
+ tableIdentifier = self.makeIdentifier(schemaName, tableName)
482
+ deleteKey = self.makeIdentifier(emptyDataClass.tableAlias, idKey)
483
+
484
+ delete_query = sql.SQL("DELETE FROM {table} WHERE {id_key} = %s").format(
485
+ table=tableIdentifier, id_key=deleteKey
486
+ )
487
+
488
+ # Create a new cursor
489
+ newCursor = self.createCursor(emptyDataClass)
490
+
491
+ # Log
492
+ self.logQuery(newCursor, delete_query, (idValue,))
493
+
494
+ # Delete
495
+ newCursor.execute(delete_query, (idValue,))
496
+ affected_rows = newCursor.rowcount
497
+
498
+ return affected_rows
@@ -1,34 +1,33 @@
1
1
  import logging
2
2
  from typing import Any, AsyncGenerator, overload
3
3
 
4
- from psycopg import Cursor, AsyncCursor, sql
4
+ from psycopg import AsyncCursor, sql
5
5
  from psycopg.rows import class_row
6
6
 
7
- from database_wrapper import T, OrderByItem, DBWrapper, DBDataModel
7
+ from database_wrapper import T, OrderByItem, DBWrapperAsync, DBDataModel
8
8
 
9
9
  from .connector import (
10
- # Sync
11
- PgConnectionType,
12
- PgCursorType,
13
- PgSQL,
14
10
  # Async
15
11
  PgAsyncConnectionType,
16
12
  PgAsyncCursorType,
17
- AsyncPgSQLWithPooling,
13
+ PgSQLWithPoolingAsync,
18
14
  )
19
15
 
20
16
 
21
- class DBWrapperPgSQL(DBWrapper):
17
+ class DBWrapperPgSQLAsync(DBWrapperAsync):
22
18
  """
23
- Database wrapper for postgres
24
-
25
- This is meant to be used in async environments. Also remember to call close() when done.
19
+ Async database wrapper for postgres
26
20
 
21
+ This is meant to be used in async environments.
22
+ Also remember to call close() when done as we cannot do that in __del__.
27
23
  """
28
24
 
29
25
  # Override db instance
30
- db: PgSQL | AsyncPgSQLWithPooling
31
- dbConn: PgConnectionType | PgAsyncConnectionType | None = None
26
+ db: PgSQLWithPoolingAsync
27
+ """ Async PostgreSQL database connector """
28
+
29
+ dbConn: PgAsyncConnectionType | None = None
30
+ """ Async PostgreSQL connection object """
32
31
 
33
32
  #######################
34
33
  ### Class lifecycle ###
@@ -37,24 +36,25 @@ class DBWrapperPgSQL(DBWrapper):
37
36
  # Meta methods
38
37
  def __init__(
39
38
  self,
40
- db: PgSQL | AsyncPgSQLWithPooling,
41
- dbConn: PgConnectionType | PgAsyncConnectionType | None = None,
39
+ db: PgSQLWithPoolingAsync,
40
+ dbConn: PgAsyncConnectionType | None = None,
42
41
  logger: logging.Logger | None = None,
43
42
  ):
44
43
  """
45
44
  Initializes a new instance of the DBWrapper class.
46
45
 
47
46
  Args:
48
- db (MySQL): The MySQL object.
47
+ db (MySQL): The PostgreSQL database connector.
48
+ dbConn (MySqlConnection, optional): The PostgreSQL connection object. Defaults to None.
49
49
  logger (logging.Logger, optional): The logger object. Defaults to None.
50
50
  """
51
51
  super().__init__(db, dbConn, logger)
52
52
 
53
53
  async def close(self) -> None:
54
54
  if hasattr(self, "dbConn") and self.dbConn and hasattr(self, "db") and self.db:
55
- if isinstance(self.db, AsyncPgSQLWithPooling):
56
- await self.db.returnConnection(self.dbConn) # type: ignore
57
- self.dbConn = None
55
+ await self.db.returnConnection(self.dbConn)
56
+
57
+ await super().close()
58
58
 
59
59
  ######################
60
60
  ### Helper methods ###
@@ -76,18 +76,18 @@ class DBWrapperPgSQL(DBWrapper):
76
76
  return sql.Identifier(name)
77
77
 
78
78
  @overload
79
- async def createCursor(self) -> PgCursorType | PgAsyncCursorType: ...
79
+ async def createCursor(self) -> PgAsyncCursorType: ...
80
80
 
81
81
  @overload
82
82
  async def createCursor(
83
83
  self,
84
84
  emptyDataClass: T,
85
- ) -> Cursor[T] | AsyncCursor[T]: ...
85
+ ) -> AsyncCursor[T]: ...
86
86
 
87
87
  async def createCursor(
88
88
  self,
89
89
  emptyDataClass: T | None = None,
90
- ) -> Cursor[T] | PgCursorType | AsyncCursor[T] | PgAsyncCursorType:
90
+ ) -> AsyncCursor[T] | PgAsyncCursorType:
91
91
  """
92
92
  Creates a new cursor object.
93
93
 
@@ -102,20 +102,12 @@ class DBWrapperPgSQL(DBWrapper):
102
102
 
103
103
  # First we need connection
104
104
  if self.dbConn is None:
105
- if isinstance(self.db, PgSQL):
106
- self.dbConn = self.db.connection
107
-
108
- if isinstance(self.db, AsyncPgSQLWithPooling):
109
- status = await self.db.newConnection()
110
- if not status:
111
- raise Exception("Failed to create new connection")
105
+ status = await self.db.newConnection()
106
+ if not status:
107
+ raise Exception("Failed to create new connection")
112
108
 
113
- (pgConn, _pgCur) = status
114
- self.dbConn = pgConn
115
-
116
- # Lets make sure we have a connection
117
- if self.dbConn is None:
118
- raise Exception("Failed to get connection")
109
+ (pgConn, _pgCur) = status
110
+ self.dbConn = pgConn
119
111
 
120
112
  if emptyDataClass is None:
121
113
  return self.dbConn.cursor()
@@ -124,7 +116,7 @@ class DBWrapperPgSQL(DBWrapper):
124
116
 
125
117
  def logQuery(
126
118
  self,
127
- cursor: AsyncCursor[Any] | Cursor[Any],
119
+ cursor: AsyncCursor[Any],
128
120
  query: sql.SQL | sql.Composed,
129
121
  params: tuple[Any, ...],
130
122
  ) -> None:
@@ -208,21 +200,14 @@ class DBWrapperPgSQL(DBWrapper):
208
200
 
209
201
  # Load data
210
202
  try:
211
- if isinstance(newCursor, AsyncCursor):
212
- await newCursor.execute(querySql, (idValue,))
213
- dbData = await newCursor.fetchone()
214
- else:
215
- newCursor.execute(querySql, (idValue,))
216
- dbData = newCursor.fetchone()
203
+ await newCursor.execute(querySql, (idValue,))
204
+ dbData = await newCursor.fetchone()
217
205
 
218
206
  return dbData
219
207
 
220
208
  finally:
221
209
  # Close the cursor
222
- if isinstance(newCursor, AsyncCursor):
223
- await newCursor.close()
224
- else:
225
- newCursor.close()
210
+ await newCursor.close()
226
211
 
227
212
  async def getByKey(
228
213
  self,
@@ -264,21 +249,14 @@ class DBWrapperPgSQL(DBWrapper):
264
249
 
265
250
  # Load data
266
251
  try:
267
- if isinstance(newCursor, AsyncCursor):
268
- await newCursor.execute(querySql, (idValue,))
269
- dbData = await newCursor.fetchone()
270
- else:
271
- newCursor.execute(querySql, (idValue,))
272
- dbData = newCursor.fetchone()
252
+ await newCursor.execute(querySql, (idValue,))
253
+ dbData = await newCursor.fetchone()
273
254
 
274
255
  return dbData
275
256
 
276
257
  finally:
277
258
  # Ensure the cursor is closed after the generator is exhausted or an error occurs
278
- if isinstance(newCursor, AsyncCursor):
279
- await newCursor.close()
280
- else:
281
- newCursor.close()
259
+ await newCursor.close()
282
260
 
283
261
  async def getAll(
284
262
  self,
@@ -346,29 +324,19 @@ class DBWrapperPgSQL(DBWrapper):
346
324
 
347
325
  # Load data
348
326
  try:
349
- if isinstance(newCursor, AsyncCursor):
350
- # Execute the query
351
- await newCursor.execute(querySql, _params)
352
-
353
- # Instead of fetchall(), we'll use a generator to yield results one by one
354
- while True:
355
- row = await newCursor.fetchone()
356
- if row is None:
357
- break
358
- yield row
359
- else:
360
- newCursor.execute(querySql, _params)
361
- while True:
362
- row = newCursor.fetchone()
363
- if row is None:
364
- break
365
- yield row
327
+ # Execute the query
328
+ await newCursor.execute(querySql, _params)
329
+
330
+ # Instead of fetchall(), we'll use a generator to yield results one by one
331
+ while True:
332
+ row = await newCursor.fetchone()
333
+ if row is None:
334
+ break
335
+ yield row
336
+
366
337
  finally:
367
338
  # Ensure the cursor is closed after the generator is exhausted or an error occurs
368
- if isinstance(newCursor, AsyncCursor):
369
- await newCursor.close()
370
- else:
371
- newCursor.close()
339
+ await newCursor.close()
372
340
 
373
341
  async def getFiltered(
374
342
  self,
@@ -414,29 +382,19 @@ class DBWrapperPgSQL(DBWrapper):
414
382
 
415
383
  # Load data
416
384
  try:
417
- if isinstance(newCursor, AsyncCursor):
418
- # Execute the query
419
- await newCursor.execute(querySql, _params)
420
-
421
- # Instead of fetchall(), we'll use a generator to yield results one by one
422
- while True:
423
- row = await newCursor.fetchone()
424
- if row is None:
425
- break
426
- yield row
427
- else:
428
- newCursor.execute(querySql, _params)
429
- while True:
430
- row = newCursor.fetchone()
431
- if row is None:
432
- break
433
- yield row
385
+ # Execute the query
386
+ await newCursor.execute(querySql, _params)
387
+
388
+ # Instead of fetchall(), we'll use a generator to yield results one by one
389
+ while True:
390
+ row = await newCursor.fetchone()
391
+ if row is None:
392
+ break
393
+ yield row
394
+
434
395
  finally:
435
- # Ensure the cursor is closed after the generator is exhausted or an error occurs
436
- if isinstance(newCursor, AsyncCursor):
437
- await newCursor.close()
438
- else:
439
- newCursor.close()
396
+ # Close the cursor
397
+ await newCursor.close()
440
398
 
441
399
  async def _store(
442
400
  self,
@@ -468,19 +426,19 @@ class DBWrapperPgSQL(DBWrapper):
468
426
  self.logQuery(newCursor, insertQuery, tuple(values))
469
427
 
470
428
  # Insert
471
- if isinstance(newCursor, AsyncCursor):
429
+ try:
472
430
  await newCursor.execute(insertQuery, tuple(values))
473
431
  affectedRows = newCursor.rowcount
474
432
  result = await newCursor.fetchone()
475
- else:
476
- newCursor.execute(insertQuery, tuple(values))
477
- affectedRows = newCursor.rowcount
478
- result = newCursor.fetchone()
479
433
 
480
- return (
481
- result.id if result and hasattr(result, "id") else 0,
482
- affectedRows,
483
- )
434
+ return (
435
+ result.id if result and hasattr(result, "id") else 0,
436
+ affectedRows,
437
+ )
438
+
439
+ finally:
440
+ # Close the cursor
441
+ await newCursor.close()
484
442
 
485
443
  async def _update(
486
444
  self,
@@ -516,13 +474,15 @@ class DBWrapperPgSQL(DBWrapper):
516
474
  self.logQuery(newCursor, updateQuery, tuple(values))
517
475
 
518
476
  # Update
519
- if isinstance(newCursor, AsyncCursor):
477
+ try:
520
478
  await newCursor.execute(updateQuery, tuple(values))
521
- else:
522
- newCursor.execute(updateQuery, tuple(values))
523
- affectedRows = newCursor.rowcount
479
+ affectedRows = newCursor.rowcount
524
480
 
525
- return affectedRows
481
+ return affectedRows
482
+
483
+ finally:
484
+ # Close the cursor
485
+ await newCursor.close()
526
486
 
527
487
  async def _delete(
528
488
  self,
@@ -547,10 +507,12 @@ class DBWrapperPgSQL(DBWrapper):
547
507
  self.logQuery(newCursor, delete_query, (idValue,))
548
508
 
549
509
  # Delete
550
- if isinstance(newCursor, AsyncCursor):
510
+ try:
551
511
  await newCursor.execute(delete_query, (idValue,))
552
- else:
553
- newCursor.execute(delete_query, (idValue,))
554
- affected_rows = newCursor.rowcount
512
+ affected_rows = newCursor.rowcount
555
513
 
556
- return affected_rows
514
+ return affected_rows
515
+
516
+ finally:
517
+ # Close the cursor
518
+ await newCursor.close()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: database_wrapper_pgsql
3
- Version: 0.1.28
3
+ Version: 0.1.33
4
4
  Summary: database_wrapper for PostgreSQL database
5
5
  Author-email: Gints Murans <gm@gm.lv>
6
6
  License: GNU General Public License v3.0 (GPL-3.0)
@@ -32,7 +32,7 @@ Classifier: Topic :: Software Development
32
32
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
33
33
  Requires-Python: >=3.8
34
34
  Description-Content-Type: text/markdown
35
- Requires-Dist: database_wrapper==0.1.28
35
+ Requires-Dist: database_wrapper==0.1.33
36
36
  Requires-Dist: psycopg[binary]>=3.2.0
37
37
  Requires-Dist: psycopg[pool]>=3.2.0
38
38
 
@@ -40,7 +40,7 @@ Requires-Dist: psycopg[pool]>=3.2.0
40
40
 
41
41
  _Part of the `database_wrapper` package._
42
42
 
43
- This python package is a database wrapper for [PostgreSQL](https://www.postgresql.org/) (also called pgsql) databases.
43
+ This python package is a database wrapper for [PostgreSQL](https://www.postgresql.org/) (also called pgsql) database.
44
44
 
45
45
  ## Installation
46
46
 
@@ -51,9 +51,9 @@ pip install database_wrapper[pgsql]
51
51
  ## Usage
52
52
 
53
53
  ```python
54
- from database_wrapper_pgsql import AsyncPgSQLWithPooling, DBWrapperPgSQL
54
+ from database_wrapper_pgsql import PgSQLWithPoolingAsync, DBWrapperPgSQLAsync
55
55
 
56
- db = MySQL({
56
+ db = PgSQLWithPoolingAsync({
57
57
  "hostname": "localhost",
58
58
  "port": 3306,
59
59
  "username": "root",
@@ -61,7 +61,7 @@ db = MySQL({
61
61
  "database": "my_database"
62
62
  })
63
63
  db.open()
64
- dbWrapper = DBWrapperMySQL(db=db)
64
+ dbWrapper = DBWrapperPgSQLAsync(db=db)
65
65
 
66
66
  # Simple query
67
67
  aModel = MyModel()
@@ -78,7 +78,7 @@ else:
78
78
  # Raw query
79
79
  res = await dbWrapper.getAll(
80
80
  aModel,
81
- """
81
+ customQuery="""
82
82
  SELECT t1.*, t2.name AS other_name
83
83
  FROM my_table AS t1
84
84
  LEFT JOIN other_table AS t2 ON t1.other_id = t2.id
@@ -3,6 +3,7 @@ pyproject.toml
3
3
  database_wrapper_pgsql/__init__.py
4
4
  database_wrapper_pgsql/connector.py
5
5
  database_wrapper_pgsql/db_wrapper_pgsql.py
6
+ database_wrapper_pgsql/db_wrapper_pgsql_async.py
6
7
  database_wrapper_pgsql/py.typed
7
8
  database_wrapper_pgsql.egg-info/PKG-INFO
8
9
  database_wrapper_pgsql.egg-info/SOURCES.txt
@@ -1,3 +1,3 @@
1
- database_wrapper==0.1.28
1
+ database_wrapper==0.1.33
2
2
  psycopg[binary]>=3.2.0
3
3
  psycopg[pool]>=3.2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "database_wrapper_pgsql"
7
- version = "0.1.28"
7
+ version = "0.1.33"
8
8
  description = "database_wrapper for PostgreSQL database"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -35,7 +35,7 @@ classifiers = [
35
35
  ]
36
36
  keywords = ["database", "wrapper", "python", "postgresql", "pgsql"]
37
37
  dependencies = [
38
- "database_wrapper == 0.1.28",
38
+ "database_wrapper == 0.1.33",
39
39
  "psycopg[binary] >= 3.2.0",
40
40
  "psycopg[pool] >= 3.2.0",
41
41
  ]