mtpk-postgres 0.1.0__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.
- mtpk_postgres-0.1.0/LICENSE +21 -0
- mtpk_postgres-0.1.0/PKG-INFO +47 -0
- mtpk_postgres-0.1.0/README.md +27 -0
- mtpk_postgres-0.1.0/mtpk_postgres/__init__.py +5 -0
- mtpk_postgres-0.1.0/mtpk_postgres/async_adapter.py +333 -0
- mtpk_postgres-0.1.0/mtpk_postgres/core_sync.py +1688 -0
- mtpk_postgres-0.1.0/mtpk_postgres/crud.py +333 -0
- mtpk_postgres-0.1.0/mtpk_postgres/excepciones.py +46 -0
- mtpk_postgres-0.1.0/mtpk_postgres/interface.py +69 -0
- mtpk_postgres-0.1.0/mtpk_postgres/utils.py +294 -0
- mtpk_postgres-0.1.0/mtpk_postgres.egg-info/PKG-INFO +47 -0
- mtpk_postgres-0.1.0/mtpk_postgres.egg-info/SOURCES.txt +15 -0
- mtpk_postgres-0.1.0/mtpk_postgres.egg-info/dependency_links.txt +1 -0
- mtpk_postgres-0.1.0/mtpk_postgres.egg-info/requires.txt +3 -0
- mtpk_postgres-0.1.0/mtpk_postgres.egg-info/top_level.txt +1 -0
- mtpk_postgres-0.1.0/pyproject.toml +32 -0
- mtpk_postgres-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 José Jesús Andrés Zambrana
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mtpk_postgres
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Librería para sincronización estructural de bases de datos Postgres con modelos Python (adaptación de mtpk_mariadb)
|
|
5
|
+
Author-email: José Jesús Andrés Zambrana <jjandres@multiplika.es>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jjandres/mtpk_postgres
|
|
8
|
+
Project-URL: Repository, https://github.com/jjandres/mtpk_postgres
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Topic :: Database
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: psycopg[binary,pool]
|
|
17
|
+
Requires-Dist: pydantic
|
|
18
|
+
Requires-Dist: bcrypt
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# mtpk_postgres
|
|
22
|
+
|
|
23
|
+
Adaptación a Postgres (vía [psycopg 3](https://www.psycopg.org/psycopg3/)) de `mtpk_mariadb`: define tus tablas con
|
|
24
|
+
dataclasses (`Tabla`, `Columna`, `ForeignKey`, `Index`), sincroniza la estructura de la base de datos automáticamente
|
|
25
|
+
(CREATE/ALTER TABLE, índices, claves foráneas) y usa `AsyncCrudBase` para CRUD asíncrono.
|
|
26
|
+
|
|
27
|
+
No modifica ni depende de `mtpk_mariadb`; es un paquete independiente. Ver `docs/mtpk-postgres-adaptacion.md` en el
|
|
28
|
+
repo para el detalle de las diferencias frente a la versión MariaDB.
|
|
29
|
+
|
|
30
|
+
## Instalación
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
pip install -e .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Notas de la adaptación
|
|
37
|
+
|
|
38
|
+
- Identificadores citados con `"comillas dobles"` en vez de backticks.
|
|
39
|
+
- `auto_increment=True` genera `GENERATED ALWAYS AS IDENTITY` en vez de `AUTO_INCREMENT`; como Postgres no tiene
|
|
40
|
+
`lastrowid`, las inserciones vía `AsyncCrudBase.insert()`/`insertar()` añaden `RETURNING id`.
|
|
41
|
+
- `Tabla.to_sql()` ya no incluye índices ni comentarios inline (Postgres no lo permite dentro de `CREATE TABLE`):
|
|
42
|
+
usa `Tabla.to_sql_indices()` y `Tabla.to_sql_comentarios()` para las sentencias `CREATE INDEX` y `COMMENT ON`
|
|
43
|
+
correspondientes. `ManagerDB` ya las ejecuta automáticamente al crear tablas.
|
|
44
|
+
- `ENUM`/`SET` se traducen a `TEXT` + `CHECK`; al reconstruir la estructura desde la base de datos ambos se
|
|
45
|
+
reportan como `ENUM` (Postgres no permite distinguirlos después del hecho).
|
|
46
|
+
- A diferencia de MariaDB, el DDL en Postgres es transaccional: los métodos de `ManagerDB` confirman (`COMMIT`)
|
|
47
|
+
explícitamente tras crear tablas, triggers y procedimientos.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# mtpk_postgres
|
|
2
|
+
|
|
3
|
+
Adaptación a Postgres (vía [psycopg 3](https://www.psycopg.org/psycopg3/)) de `mtpk_mariadb`: define tus tablas con
|
|
4
|
+
dataclasses (`Tabla`, `Columna`, `ForeignKey`, `Index`), sincroniza la estructura de la base de datos automáticamente
|
|
5
|
+
(CREATE/ALTER TABLE, índices, claves foráneas) y usa `AsyncCrudBase` para CRUD asíncrono.
|
|
6
|
+
|
|
7
|
+
No modifica ni depende de `mtpk_mariadb`; es un paquete independiente. Ver `docs/mtpk-postgres-adaptacion.md` en el
|
|
8
|
+
repo para el detalle de las diferencias frente a la versión MariaDB.
|
|
9
|
+
|
|
10
|
+
## Instalación
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
pip install -e .
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Notas de la adaptación
|
|
17
|
+
|
|
18
|
+
- Identificadores citados con `"comillas dobles"` en vez de backticks.
|
|
19
|
+
- `auto_increment=True` genera `GENERATED ALWAYS AS IDENTITY` en vez de `AUTO_INCREMENT`; como Postgres no tiene
|
|
20
|
+
`lastrowid`, las inserciones vía `AsyncCrudBase.insert()`/`insertar()` añaden `RETURNING id`.
|
|
21
|
+
- `Tabla.to_sql()` ya no incluye índices ni comentarios inline (Postgres no lo permite dentro de `CREATE TABLE`):
|
|
22
|
+
usa `Tabla.to_sql_indices()` y `Tabla.to_sql_comentarios()` para las sentencias `CREATE INDEX` y `COMMENT ON`
|
|
23
|
+
correspondientes. `ManagerDB` ya las ejecuta automáticamente al crear tablas.
|
|
24
|
+
- `ENUM`/`SET` se traducen a `TEXT` + `CHECK`; al reconstruir la estructura desde la base de datos ambos se
|
|
25
|
+
reportan como `ENUM` (Postgres no permite distinguirlos después del hecho).
|
|
26
|
+
- A diferencia de MariaDB, el DDL en Postgres es transaccional: los métodos de `ManagerDB` confirman (`COMMIT`)
|
|
27
|
+
explícitamente tras crear tablas, triggers y procedimientos.
|
|
@@ -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
|