mtpk-postgres 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mtpk_postgres/__init__.py +5 -0
- mtpk_postgres/async_adapter.py +333 -0
- mtpk_postgres/core_sync.py +1688 -0
- mtpk_postgres/crud.py +333 -0
- mtpk_postgres/excepciones.py +46 -0
- mtpk_postgres/interface.py +69 -0
- mtpk_postgres/utils.py +294 -0
- mtpk_postgres-0.1.0.dist-info/METADATA +47 -0
- mtpk_postgres-0.1.0.dist-info/RECORD +12 -0
- mtpk_postgres-0.1.0.dist-info/WHEEL +5 -0
- mtpk_postgres-0.1.0.dist-info/licenses/LICENSE +21 -0
- mtpk_postgres-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#-*- coding: UTF-8 -*-
|
|
3
|
+
# ----------------------------------------
|
|
4
|
+
# jjandres 2025
|
|
5
|
+
# Adaptación a Postgres (psycopg, modo async) de mtpk_mariadb.async_adapter
|
|
6
|
+
# ----------------------------------------
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Esta librería es un wrapper (envolvente) del conector psycopg para manejo de Postgres con funciones asíncronas.
|
|
10
|
+
|
|
11
|
+
import psycopg
|
|
12
|
+
from psycopg.rows import dict_row
|
|
13
|
+
from typing import Optional, Union, List, Dict
|
|
14
|
+
from .core_sync import Tabla, SQLLiteral
|
|
15
|
+
from logging import Logger
|
|
16
|
+
from contextlib import asynccontextmanager
|
|
17
|
+
from contextvars import ContextVar
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _PseudoPool:
|
|
21
|
+
"""Sustituto de un pool real que NO reutiliza conexiones.
|
|
22
|
+
- Mantiene interfaz mínima usada: acquire(), close(), wait_closed(), _closed.
|
|
23
|
+
- Cada acquire() abre conexión con psycopg.AsyncConnection.connect() y la cierra al salir del contexto.
|
|
24
|
+
"""
|
|
25
|
+
def __init__(self, host: str, user: str, password: str, db: str, port: int, autocommit: bool = False):
|
|
26
|
+
self._cfg = dict(host=host, user=user, password=password, dbname=db, port=port)
|
|
27
|
+
self._autocommit = autocommit
|
|
28
|
+
self._closed = False
|
|
29
|
+
|
|
30
|
+
@asynccontextmanager
|
|
31
|
+
async def acquire(self):
|
|
32
|
+
cx = await psycopg.AsyncConnection.connect(**self._cfg, autocommit=self._autocommit, row_factory=dict_row)
|
|
33
|
+
try:
|
|
34
|
+
yield cx
|
|
35
|
+
finally:
|
|
36
|
+
try:
|
|
37
|
+
if not cx.autocommit:
|
|
38
|
+
try:
|
|
39
|
+
await cx.rollback()
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
finally:
|
|
43
|
+
await cx.close()
|
|
44
|
+
|
|
45
|
+
def close(self):
|
|
46
|
+
self._closed = True
|
|
47
|
+
|
|
48
|
+
async def wait_closed(self):
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AsyncDatabase:
|
|
53
|
+
"""
|
|
54
|
+
Clase AsyncDatabase para entornos asíncronos como FastAPI con Uvicorn.
|
|
55
|
+
Usa psycopg (modo async) como backend de conexión.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, host: str, user: str, password: str, db: str, port: int = 5432, logger: Optional[Logger] = None, **kwargs):
|
|
59
|
+
self.host = host
|
|
60
|
+
self.user = user
|
|
61
|
+
self.password = password
|
|
62
|
+
self.db = db
|
|
63
|
+
self.port = port
|
|
64
|
+
self.logger = logger
|
|
65
|
+
self.name = kwargs.get("name") # Compatibilidad con configuraciones que incluyen 'name'
|
|
66
|
+
|
|
67
|
+
# Mantenemos el atributo pool por compatibilidad
|
|
68
|
+
self.pool = None
|
|
69
|
+
|
|
70
|
+
# Para compatibilidad con la gestión de tablas / sync
|
|
71
|
+
self.tablas: Dict[str, Tabla] = {}
|
|
72
|
+
|
|
73
|
+
# Último insert id (cuando la sentencia usa RETURNING)
|
|
74
|
+
self.ultimo_insert_id: Optional[int] = None
|
|
75
|
+
|
|
76
|
+
# Bandera de transacción por coroutine
|
|
77
|
+
self._tx_flag: ContextVar[bool] = ContextVar("mtpk_tx_flag", default=False)
|
|
78
|
+
|
|
79
|
+
def en_transaccion(self) -> bool:
|
|
80
|
+
"""Indica si hay una transacción abierta en esta coroutine."""
|
|
81
|
+
return self._tx_flag.get()
|
|
82
|
+
|
|
83
|
+
@asynccontextmanager
|
|
84
|
+
async def transaccion(self, autocommit: bool = False):
|
|
85
|
+
"""
|
|
86
|
+
Contexto de transacción con compatibilidad hacia atrás.
|
|
87
|
+
|
|
88
|
+
Params:
|
|
89
|
+
autocommit (bool): Se mantiene por compatibilidad con transaccion_simple().
|
|
90
|
+
- False (por defecto): se hace COMMIT/ROLLBACK al salir del bloque.
|
|
91
|
+
- True: NO se hace COMMIT/ROLLBACK; se cede la conexión tal cual.
|
|
92
|
+
Útil para operaciones sueltas que quieren controlar su propio commit,
|
|
93
|
+
o para simples lecturas.
|
|
94
|
+
|
|
95
|
+
Nota:
|
|
96
|
+
Aunque internamente no hay pool real, `self.pool.acquire()` usa _PseudoPool
|
|
97
|
+
y abre/cierra una conexión por bloque.
|
|
98
|
+
"""
|
|
99
|
+
if self.pool is None:
|
|
100
|
+
await self.conectar()
|
|
101
|
+
|
|
102
|
+
token = self._tx_flag.set(True)
|
|
103
|
+
try:
|
|
104
|
+
async with self.pool.acquire() as cx:
|
|
105
|
+
try:
|
|
106
|
+
await cx.set_autocommit(autocommit)
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
yield cx
|
|
112
|
+
if not autocommit:
|
|
113
|
+
await cx.commit()
|
|
114
|
+
except Exception:
|
|
115
|
+
if not autocommit:
|
|
116
|
+
try:
|
|
117
|
+
await cx.rollback()
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
raise
|
|
121
|
+
finally:
|
|
122
|
+
self._tx_flag.reset(token)
|
|
123
|
+
|
|
124
|
+
transaccion_simple = transaccion # Alias de transaccion
|
|
125
|
+
|
|
126
|
+
async def conectar(self):
|
|
127
|
+
"""
|
|
128
|
+
Crea un 'pool' compatible si no está ya activo. En realidad es un _PseudoPool:
|
|
129
|
+
cada acquire() abre/cierra su propia conexión (sin reutilización).
|
|
130
|
+
"""
|
|
131
|
+
if self.pool is None or getattr(self.pool, "_closed", False):
|
|
132
|
+
self.pool = _PseudoPool(
|
|
133
|
+
host=self.host, user=self.user, password=self.password,
|
|
134
|
+
db=self.db, port=self.port, autocommit=False
|
|
135
|
+
)
|
|
136
|
+
return self.pool
|
|
137
|
+
|
|
138
|
+
async def cerrar(self) -> None:
|
|
139
|
+
"""
|
|
140
|
+
No-op seguro: mantiene compatibilidad con código que esperaba cerrar un pool.
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
if hasattr(self, "pool") and self.pool:
|
|
144
|
+
try:
|
|
145
|
+
self.pool.close()
|
|
146
|
+
await self.pool.wait_closed()
|
|
147
|
+
except Exception:
|
|
148
|
+
pass
|
|
149
|
+
self.pool = None
|
|
150
|
+
except Exception as e:
|
|
151
|
+
if self.logger:
|
|
152
|
+
self.logger.error("Error cerrando pool: %s", e, exc_info=True)
|
|
153
|
+
|
|
154
|
+
def add_tabla(self, tabla: Tabla):
|
|
155
|
+
"""
|
|
156
|
+
Añade un objeto `Tabla` a la base de datos.
|
|
157
|
+
|
|
158
|
+
- Args:
|
|
159
|
+
- `tabla` (Tabla): Objeto que representa una tabla definida por el usuario.
|
|
160
|
+
"""
|
|
161
|
+
self.tablas[tabla.nombre] = tabla
|
|
162
|
+
|
|
163
|
+
def get_tabla(self, nombre: str) -> Tabla:
|
|
164
|
+
"""
|
|
165
|
+
Recupera un objeto `Tabla` anteriormente añadido (o lanza KeyError si no existe).
|
|
166
|
+
"""
|
|
167
|
+
if nombre not in self.tablas:
|
|
168
|
+
raise KeyError(f"La tabla '{nombre}' no está registrada.")
|
|
169
|
+
return self.tablas[nombre]
|
|
170
|
+
|
|
171
|
+
async def query_multi_action(self, sql: str, lista_valores: List[tuple], conexion=None) -> int:
|
|
172
|
+
"""
|
|
173
|
+
Ejecuta múltiples acciones SQL con la misma sentencia y diferentes valores usando `executemany`.
|
|
174
|
+
|
|
175
|
+
- Si se pasa `conexion`, NO se hace commit ni rollback aquí (compatibilidad con transacciones externas).
|
|
176
|
+
- Si NO se pasa `conexion`, se abre una conexión propia (vía _PseudoPool.acquire()) y se hace commit.
|
|
177
|
+
|
|
178
|
+
Nota: a diferencia de MariaDB, Postgres no tiene un `lastrowid` global tras un `executemany`,
|
|
179
|
+
así que este método no actualiza `self.ultimo_insert_id`.
|
|
180
|
+
"""
|
|
181
|
+
if not lista_valores:
|
|
182
|
+
return 0
|
|
183
|
+
|
|
184
|
+
if conexion is not None:
|
|
185
|
+
async with conexion.cursor() as cursor:
|
|
186
|
+
await cursor.executemany(sql, lista_valores)
|
|
187
|
+
return cursor.rowcount
|
|
188
|
+
else:
|
|
189
|
+
if self.pool is None:
|
|
190
|
+
await self.conectar()
|
|
191
|
+
|
|
192
|
+
async with self.pool.acquire() as cx:
|
|
193
|
+
try:
|
|
194
|
+
async with cx.cursor() as cursor:
|
|
195
|
+
await cursor.executemany(sql, lista_valores)
|
|
196
|
+
total_filas = cursor.rowcount
|
|
197
|
+
await cx.commit()
|
|
198
|
+
return total_filas
|
|
199
|
+
except Exception:
|
|
200
|
+
try:
|
|
201
|
+
await cx.rollback()
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
raise
|
|
205
|
+
|
|
206
|
+
async def query(self, sql: str, params=None, conexion=None, uno: bool = False):
|
|
207
|
+
"""
|
|
208
|
+
Ejecuta una consulta SQL detectando si es de lectura o de acción según la primera palabra del SQL.
|
|
209
|
+
|
|
210
|
+
Si comienza por SELECT/SHOW/EXPLAIN → delega en `_query_select`.
|
|
211
|
+
En caso contrario → delega en `_query_accion`.
|
|
212
|
+
|
|
213
|
+
Gestión de conexión:
|
|
214
|
+
- Si `conexion` es None, abre y cierra conexión internamente y gestiona commit/rollback cuando proceda.
|
|
215
|
+
- Si `conexion` no es None, reutiliza la transacción/conn proporcionada y NO hace commit/rollback.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
sql (str): Sentencia SQL completa.
|
|
219
|
+
params (Any, opcional): Parámetros de la consulta (tuple/list/dict según driver).
|
|
220
|
+
conexion (Any, opcional): Conexión o transacción activa a reutilizar.
|
|
221
|
+
uno (bool, opcional): Solo para lecturas; si True retorna una sola fila.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Any:
|
|
225
|
+
- Lectura: lista de filas o una sola fila si `uno=True` (formato según `_query_select`).
|
|
226
|
+
- Acción: resultado/contador según `_query_accion` (filas afectadas, y `ultimo_insert_id` si hay RETURNING).
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
Exception: Re-lanza errores del driver/adapter subyacente.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
comando = sql.strip().split()[0].upper() if sql else ""
|
|
233
|
+
if comando in {"SELECT", "SHOW", "EXPLAIN"}:
|
|
234
|
+
return await self._query_select(sql, params, conexion, uno)
|
|
235
|
+
else:
|
|
236
|
+
return await self._query_accion(sql, params, conexion)
|
|
237
|
+
|
|
238
|
+
async def _query_select(self, sql: str, params=None, conexion=None, uno: bool = False) -> Union[List[Dict], Dict, None]:
|
|
239
|
+
"""
|
|
240
|
+
SELECT/SHOW/etc. Si `conexion` es None, usa una conexión propia vía _PseudoPool.
|
|
241
|
+
Devuelve el número de filas obtenidas
|
|
242
|
+
"""
|
|
243
|
+
if conexion is not None:
|
|
244
|
+
async with conexion.cursor(row_factory=dict_row) as cursor:
|
|
245
|
+
await cursor.execute(sql, params)
|
|
246
|
+
if uno:
|
|
247
|
+
return await cursor.fetchone()
|
|
248
|
+
return await cursor.fetchall()
|
|
249
|
+
else:
|
|
250
|
+
if self.pool is None:
|
|
251
|
+
await self.conectar()
|
|
252
|
+
async with self.pool.acquire() as cx:
|
|
253
|
+
async with cx.cursor(row_factory=dict_row) as cursor:
|
|
254
|
+
await cursor.execute(sql, params)
|
|
255
|
+
if uno:
|
|
256
|
+
return await cursor.fetchone()
|
|
257
|
+
return await cursor.fetchall()
|
|
258
|
+
|
|
259
|
+
async def _query_accion(self, sql: str, params=None, conexion=None) -> int:
|
|
260
|
+
"""
|
|
261
|
+
INSERT/UPDATE/DELETE/etc. Postgres no tiene `lastrowid`: si la sentencia incluye una
|
|
262
|
+
cláusula RETURNING, el primer valor de la primera fila devuelta se guarda en
|
|
263
|
+
`self.ultimo_insert_id`. Devuelve el número de filas afectadas.
|
|
264
|
+
"""
|
|
265
|
+
if conexion is not None:
|
|
266
|
+
async with conexion.cursor() as cursor:
|
|
267
|
+
await cursor.execute(sql, params)
|
|
268
|
+
filas = cursor.rowcount
|
|
269
|
+
if cursor.description:
|
|
270
|
+
fila = await cursor.fetchone()
|
|
271
|
+
if fila:
|
|
272
|
+
self.ultimo_insert_id = fila[0]
|
|
273
|
+
return filas
|
|
274
|
+
else:
|
|
275
|
+
if self.pool is None:
|
|
276
|
+
await self.conectar()
|
|
277
|
+
|
|
278
|
+
async with self.pool.acquire() as cx:
|
|
279
|
+
try:
|
|
280
|
+
async with cx.cursor() as cursor:
|
|
281
|
+
await cursor.execute(sql, params)
|
|
282
|
+
filas = cursor.rowcount
|
|
283
|
+
if cursor.description:
|
|
284
|
+
fila = await cursor.fetchone()
|
|
285
|
+
if fila:
|
|
286
|
+
self.ultimo_insert_id = fila[0]
|
|
287
|
+
await cx.commit()
|
|
288
|
+
return filas
|
|
289
|
+
except Exception as e:
|
|
290
|
+
try:
|
|
291
|
+
await cx.rollback()
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
294
|
+
if self.logger:
|
|
295
|
+
self.logger.error("Error en _query_accion: %s", e)
|
|
296
|
+
raise
|
|
297
|
+
|
|
298
|
+
async def call_proc(self, nombre: str, parametros: tuple = (), conexion=None, uno: bool = True) -> Optional[Union[dict, List[dict]]]:
|
|
299
|
+
"""
|
|
300
|
+
Ejecuta un procedimiento almacenado.
|
|
301
|
+
- Si devuelve SELECT, retorna dict/list; si no devuelve nada, None.
|
|
302
|
+
- Si `conexion` es None, abre/cierra conexión propia.
|
|
303
|
+
"""
|
|
304
|
+
sql = f"CALL {nombre}({', '.join(['%s'] * len(parametros))})" if parametros else f"CALL {nombre}()"
|
|
305
|
+
|
|
306
|
+
if conexion is not None:
|
|
307
|
+
async with conexion.cursor(row_factory=dict_row) as cursor:
|
|
308
|
+
await cursor.execute(sql, parametros)
|
|
309
|
+
try:
|
|
310
|
+
return await cursor.fetchone() if uno else await cursor.fetchall()
|
|
311
|
+
except Exception:
|
|
312
|
+
return None
|
|
313
|
+
else:
|
|
314
|
+
if self.pool is None:
|
|
315
|
+
await self.conectar()
|
|
316
|
+
async with self.pool.acquire() as cx:
|
|
317
|
+
try:
|
|
318
|
+
async with cx.cursor(row_factory=dict_row) as cursor:
|
|
319
|
+
await cursor.execute(sql, parametros)
|
|
320
|
+
try:
|
|
321
|
+
resultado = await cursor.fetchone() if uno else await cursor.fetchall()
|
|
322
|
+
except Exception:
|
|
323
|
+
resultado = None
|
|
324
|
+
await cx.commit()
|
|
325
|
+
return resultado
|
|
326
|
+
except Exception as e:
|
|
327
|
+
try:
|
|
328
|
+
await cx.rollback()
|
|
329
|
+
except Exception:
|
|
330
|
+
pass
|
|
331
|
+
if self.logger:
|
|
332
|
+
self.logger.error("Error en call_proc('%s'): %s", nombre, e)
|
|
333
|
+
raise
|