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
mtpk_postgres/crud.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#-*- coding: UTF-8 -*-
|
|
3
|
+
# --------------------------
|
|
4
|
+
# jjandres 2025
|
|
5
|
+
# Adaptación a Postgres de mtpk_mariadb.crud
|
|
6
|
+
# --------------------------
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import is_dataclass, asdict
|
|
11
|
+
from datetime import date, datetime
|
|
12
|
+
from decimal import Decimal
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from typing import Any, Optional, Tuple, Mapping, Dict, Type, Callable
|
|
17
|
+
from .core_sync import Tabla
|
|
18
|
+
from .async_adapter import AsyncDatabase
|
|
19
|
+
from .utils import construir_condiciones_sql, resolver_orden_sql
|
|
20
|
+
from .excepciones import MtpkErrorValidacionDb, MtpkErrorDb
|
|
21
|
+
from uuid import UUID
|
|
22
|
+
import uuid
|
|
23
|
+
|
|
24
|
+
class AsyncCrudBase:
|
|
25
|
+
def __init__(self, tabla: Tabla, config: dict | Callable[[], dict], logger: Optional[Any] = None):
|
|
26
|
+
"""
|
|
27
|
+
Inicializa una instancia CRUD para la tabla especificada.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
tabla (Tabla): Objeto de definición de tabla.
|
|
31
|
+
config (dict): Configuración de conexión a la base de datos.
|
|
32
|
+
"""
|
|
33
|
+
self.tabla = tabla
|
|
34
|
+
self._config_factory: Optional[Callable[[], dict]] = config if callable(config) else None
|
|
35
|
+
self.config = config() if callable(config) else config
|
|
36
|
+
self.logger = logger
|
|
37
|
+
|
|
38
|
+
def _resolver_config_actual(self) -> dict:
|
|
39
|
+
if self._config_factory:
|
|
40
|
+
self.config = self._config_factory() or {}
|
|
41
|
+
return self.config
|
|
42
|
+
|
|
43
|
+
async def _inst_db_si_necesario(self, db: Optional[AsyncDatabase]) -> AsyncDatabase:
|
|
44
|
+
"""
|
|
45
|
+
Si `db` ya es una instancia AsyncDatabase, lo reutiliza.
|
|
46
|
+
Si no, la crea para usar la tabla actual.
|
|
47
|
+
La conexión no se establece aquí, solo se registra la tabla.
|
|
48
|
+
Args:
|
|
49
|
+
db (Optional[AsyncDatabase]): Instancia AsyncDatabase existente o None.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
AsyncDatabase: Instancia AsyncDatabase con la tabla registrada.
|
|
53
|
+
"""
|
|
54
|
+
if db is not None:
|
|
55
|
+
return db
|
|
56
|
+
db = AsyncDatabase(logger=self.logger, **self._resolver_config_actual())
|
|
57
|
+
db.add_tabla(self.tabla)
|
|
58
|
+
return db
|
|
59
|
+
|
|
60
|
+
async def insertar(self, datos: dict, aplicar_protegidos: bool = False, db: Optional[AsyncDatabase] = None, conexion: Any = None) -> int:
|
|
61
|
+
"""
|
|
62
|
+
OBSOLETO: Usa el método insert().
|
|
63
|
+
Inserta un nuevo registro en la tabla.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
db (Optional[AsyncDatabase]): Conexión activa o None.
|
|
67
|
+
datos (dict): Datos a insertar.
|
|
68
|
+
aplicar_protegidos (bool): Si se deben excluir columnas protegidas.
|
|
69
|
+
conexion (Any): Conexión reutilizable, si aplica.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
int: ID del nuevo registro insertado (vía RETURNING id).
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
db = await self._inst_db_si_necesario(db)
|
|
76
|
+
campos_validos = {
|
|
77
|
+
col.nombre for col in self.tabla.columnas
|
|
78
|
+
if not (aplicar_protegidos and getattr(col, "protegido_insertar", False))
|
|
79
|
+
}
|
|
80
|
+
datos_filtrados = {k: v for k, v in datos.items() if k in campos_validos}
|
|
81
|
+
columnas = ", ".join(f'"{k}"' for k in datos_filtrados)
|
|
82
|
+
valores = ", ".join(["%s"] * len(datos_filtrados))
|
|
83
|
+
sql = f'INSERT INTO "{self.tabla.nombre}" ({columnas}) VALUES ({valores}) RETURNING id'
|
|
84
|
+
await db.query(sql, tuple(datos_filtrados.values()), conexion=conexion)
|
|
85
|
+
return db.ultimo_insert_id
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
identificador = str(uuid.uuid4())
|
|
89
|
+
raise MtpkErrorDb(f"Error insertando registro: {e}", identificador, logger=self.logger) from e
|
|
90
|
+
|
|
91
|
+
async def insert(self, datos: dict, db: Optional[AsyncDatabase] = None, conexion: Any = None) -> int:
|
|
92
|
+
"""
|
|
93
|
+
Inserta un nuevo registro en la tabla.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
db (Optional[AsyncDatabase]): Conexión activa o None.
|
|
97
|
+
datos (dict): Datos a insertar.
|
|
98
|
+
conexion (Any): Conexión reutilizable, si aplica.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
int: ID del nuevo registro insertado (vía RETURNING id).
|
|
102
|
+
"""
|
|
103
|
+
try:
|
|
104
|
+
db = await self._inst_db_si_necesario(db)
|
|
105
|
+
|
|
106
|
+
# Tomamos únicamente las columnas que existen en la tabla
|
|
107
|
+
campos_validos = {col.nombre for col in self.tabla.columnas}
|
|
108
|
+
datos_filtrados = {k: v for k, v in datos.items() if k in campos_validos}
|
|
109
|
+
|
|
110
|
+
columnas = ", ".join(f'"{k}"' for k in datos_filtrados)
|
|
111
|
+
valores = ", ".join(["%s"] * len(datos_filtrados))
|
|
112
|
+
sql = f'INSERT INTO "{self.tabla.nombre}" ({columnas}) VALUES ({valores}) RETURNING id'
|
|
113
|
+
|
|
114
|
+
await db.query(sql, tuple(datos_filtrados.values()), conexion=conexion)
|
|
115
|
+
return db.ultimo_insert_id
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
identificador = str(uuid.uuid4())
|
|
119
|
+
raise MtpkErrorDb(f"Error insertando registro: {e}", identificador, logger=self.logger) from e
|
|
120
|
+
|
|
121
|
+
async def actualizar(self, id: int, datos: dict, aplicar_protegidos: bool = False, db: Optional[AsyncDatabase] = None, conexion: Any = None) -> int:
|
|
122
|
+
"""
|
|
123
|
+
OBSOLETO: Usa el método update().
|
|
124
|
+
Actualiza un registro por ID.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
id (int): ID del registro a actualizar.
|
|
128
|
+
datos (dict): Campos a modificar.
|
|
129
|
+
aplicar_protegidos (bool): Si se deben excluir columnas protegidas.
|
|
130
|
+
db (Optional[AsyncDatabase]): Conexión activa o None.
|
|
131
|
+
conexion (Any): Conexión reutilizable, si aplica.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
int: Número de filas afectadas.
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
db = await self._inst_db_si_necesario(db)
|
|
138
|
+
campos_validos = {
|
|
139
|
+
col.nombre for col in self.tabla.columnas
|
|
140
|
+
if not (aplicar_protegidos and getattr(col, "protegido_actualizar", False))
|
|
141
|
+
}
|
|
142
|
+
datos_filtrados = {k: v for k, v in datos.items() if k in campos_validos}
|
|
143
|
+
asignaciones = ", ".join(f'"{k}" = %s' for k in datos_filtrados)
|
|
144
|
+
sql = f'UPDATE "{self.tabla.nombre}" SET {asignaciones} WHERE id = %s'
|
|
145
|
+
num_filas = await db.query(sql, tuple(datos_filtrados.values()) + (id,), conexion=conexion)
|
|
146
|
+
return num_filas
|
|
147
|
+
except Exception as e:
|
|
148
|
+
identificador = str(uuid.uuid4())
|
|
149
|
+
raise MtpkErrorDb(f"Error actualizando registro: {e}", identificador, logger=self.logger) from e
|
|
150
|
+
|
|
151
|
+
async def update(self, id: int, datos: dict, db: Optional[AsyncDatabase] = None, conexion: Any = None) -> int:
|
|
152
|
+
"""
|
|
153
|
+
Actualiza un registro por ID.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
id (int): ID del registro a actualizar.
|
|
157
|
+
datos (dict): Campos a modificar.
|
|
158
|
+
db (Optional[AsyncDatabase]): Conexión activa o None.
|
|
159
|
+
conexion (Any): Conexión reutilizable, si aplica.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
int: Número de filas afectadas.
|
|
163
|
+
"""
|
|
164
|
+
try:
|
|
165
|
+
db = await self._inst_db_si_necesario(db)
|
|
166
|
+
|
|
167
|
+
# Filtramos solo columnas válidas de la tabla
|
|
168
|
+
campos_validos = {col.nombre for col in self.tabla.columnas}
|
|
169
|
+
datos_filtrados = {k: v for k, v in datos.items() if k in campos_validos}
|
|
170
|
+
|
|
171
|
+
asignaciones = ", ".join(f'"{k}" = %s' for k in datos_filtrados)
|
|
172
|
+
sql = f'UPDATE "{self.tabla.nombre}" SET {asignaciones} WHERE id = %s'
|
|
173
|
+
|
|
174
|
+
num_filas = await db.query(sql, tuple(datos_filtrados.values()) + (id,), conexion=conexion)
|
|
175
|
+
return num_filas
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
identificador = str(uuid.uuid4())
|
|
179
|
+
raise MtpkErrorDb(f"Error actualizando registro: {e}", identificador, logger=self.logger) from e
|
|
180
|
+
|
|
181
|
+
async def eliminar(self, id: int, db: Optional[AsyncDatabase] = None, conexion: Any = None) -> int:
|
|
182
|
+
"""
|
|
183
|
+
Elimina un registro por ID.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
db (Optional[AsyncDatabase]): Conexión activa o None.
|
|
187
|
+
id (int): ID del registro a eliminar.
|
|
188
|
+
conexion (Any): Conexión reutilizable, si aplica.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
int: Número de filas eliminadas.
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
db = await self._inst_db_si_necesario(db)
|
|
195
|
+
sql = f'DELETE FROM "{self.tabla.nombre}" WHERE id = %s'
|
|
196
|
+
num_filas = await db.query(sql, (id,), conexion=conexion)
|
|
197
|
+
return num_filas
|
|
198
|
+
except Exception as e:
|
|
199
|
+
identificador = str(uuid.uuid4())
|
|
200
|
+
raise MtpkErrorDb(f"Error eliminando registro: {e}", identificador, logger=self.logger) from e
|
|
201
|
+
|
|
202
|
+
async def obtener(self, id: Any, alias: Optional[dict[str, str]] = None, db: Optional[AsyncDatabase] = None, conexion: Any = None) -> Optional[dict]:
|
|
203
|
+
"""
|
|
204
|
+
Obtiene un registro por su ID.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
`db` (Optional[AsyncDatabase]): Conexión activa o None.
|
|
208
|
+
`id` (Any): ID del registro a buscar.
|
|
209
|
+
`conexion` (Any): Conexión reutilizable, si aplica.
|
|
210
|
+
`alias` (Optional[dict[str, str]]): Alias SQL por campo.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Optional[dict]: Registro encontrado o None si no existe.
|
|
214
|
+
"""
|
|
215
|
+
try:
|
|
216
|
+
db = await self._inst_db_si_necesario(db)
|
|
217
|
+
sql = f'SELECT * FROM "{self.tabla.nombre}" WHERE id = %s'
|
|
218
|
+
return await db.query(sql, (id,), uno=True, conexion=conexion)
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
identificador = str(uuid.uuid4())
|
|
222
|
+
if self.logger:
|
|
223
|
+
self.logger.error("%s] Error obteniendo registro: %s", identificador, e, exc_info=True)
|
|
224
|
+
raise MtpkErrorDb("Error al obtener registro", identificador, logger=self.logger) from e
|
|
225
|
+
|
|
226
|
+
async def listar(self, offset: int = 0, limit: int = 100, orden: str = "id ASC", filtros: Optional[dict] = None, alias: Optional[dict[str, str]] = None, db: Optional[Any] = None, conexion: Any = None) -> Tuple[list[dict], int]:
|
|
227
|
+
"""
|
|
228
|
+
Lista registros aplicando filtros, alias, ordenación y paginación.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
`offset` (int): Desplazamiento inicial.
|
|
232
|
+
`limit` (int): Número máximo de registros a devolver.
|
|
233
|
+
`orden` (str): Orden SQL, como "a.nombre DESC", por defecto "id ASC".
|
|
234
|
+
`filtros` (Optional[dict]): Diccionario con condiciones de filtrado.
|
|
235
|
+
`alias` (Optional[dict[str, str]]): Alias SQL por campo.
|
|
236
|
+
Diccionario opcional con alias por campo, por ejemplo {'nombre_campo': 'a.nombre_alias'}
|
|
237
|
+
Los campos incluidos en el alias se refiere a los que afectan a la condición de filtro.
|
|
238
|
+
`db` (Optional[AsyncDatabase]): Conexión existente, si aplica.
|
|
239
|
+
`conexion` (Any): Conexión interna reutilizable.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Tuple[list[dict], int]: Lista de registros y total de registros sin paginar.
|
|
243
|
+
"""
|
|
244
|
+
try:
|
|
245
|
+
db = await self._inst_db_si_necesario(db)
|
|
246
|
+
condiciones_sql, valores = construir_condiciones_sql(filtros or {}, alias)
|
|
247
|
+
orden_sql = resolver_orden_sql(orden, alias)
|
|
248
|
+
|
|
249
|
+
where_sql = f"WHERE {condiciones_sql}" if condiciones_sql else ""
|
|
250
|
+
|
|
251
|
+
sql_base = f"""
|
|
252
|
+
SELECT * FROM "{self.tabla.nombre}"
|
|
253
|
+
{where_sql}
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
sql_final = f"""{sql_base} ORDER BY {orden_sql} LIMIT %s OFFSET %s"""
|
|
257
|
+
|
|
258
|
+
valores_total = valores.copy()
|
|
259
|
+
valores += [limit, offset]
|
|
260
|
+
filas = await db.query(sql_final, valores, conexion=conexion)
|
|
261
|
+
|
|
262
|
+
total = len(filas)
|
|
263
|
+
if total == limit:
|
|
264
|
+
sql_total = f"SELECT COUNT(*) as total FROM ({sql_base}) as subquery"
|
|
265
|
+
resultado = await db.query(sql_total, valores_total, uno=True, conexion=conexion)
|
|
266
|
+
total = resultado.get("total", total)
|
|
267
|
+
|
|
268
|
+
return filas, total
|
|
269
|
+
|
|
270
|
+
except Exception as e:
|
|
271
|
+
identificador = str(uuid.uuid4())
|
|
272
|
+
raise MtpkErrorDb("Error al obtener registro", identificador, logger=self.logger) from e
|
|
273
|
+
|
|
274
|
+
# Opcional: ganchos para tipos personalizados (se accede como self.NORMALIZADORES_EXTRA)
|
|
275
|
+
NORMALIZADORES_EXTRA: Dict[Type[Any], Callable[[Any], Any]] = {}
|
|
276
|
+
|
|
277
|
+
def _normalizar_valores(self, obj: Any) -> Any:
|
|
278
|
+
"""
|
|
279
|
+
Normaliza recursivamente los valores de un objeto para adaptarlos a tipos aceptados por BD.
|
|
280
|
+
|
|
281
|
+
- Convierte UUID, Decimal, Enum, Path, date y datetime a string (isoformat en fechas).
|
|
282
|
+
- Recorre diccionarios y colecciones (list/tuple/set/frozenset) profundamente.
|
|
283
|
+
- Dataclasses -> dict (asdict) y normaliza.
|
|
284
|
+
- Modelos Pydantic v2 -> model_dump() y normaliza.
|
|
285
|
+
- bytes/bytearray/memoryview -> se devuelven tal cual (útil para BYTEA).
|
|
286
|
+
- Permite añadir normalizadores personalizados vía NORMALIZADORES_EXTRA.
|
|
287
|
+
"""
|
|
288
|
+
# Primitivos
|
|
289
|
+
if obj is None or isinstance(obj, (str, int, float, bool)):
|
|
290
|
+
return obj
|
|
291
|
+
|
|
292
|
+
# Diccionarios
|
|
293
|
+
if isinstance(obj, Mapping):
|
|
294
|
+
return {k: self._normalizar_valores(v) for k, v in obj.items()}
|
|
295
|
+
|
|
296
|
+
# Secuencias / conjuntos (excluye str/bytes ya tratados)
|
|
297
|
+
if isinstance(obj, (list, tuple, set, frozenset)):
|
|
298
|
+
return [self._normalizar_valores(v) for v in obj]
|
|
299
|
+
|
|
300
|
+
# Enum
|
|
301
|
+
if isinstance(obj, Enum):
|
|
302
|
+
return obj.value
|
|
303
|
+
|
|
304
|
+
# UUID / Decimal / Path -> str
|
|
305
|
+
if isinstance(obj, (UUID, Decimal, Path)):
|
|
306
|
+
return str(obj)
|
|
307
|
+
|
|
308
|
+
# Fechas
|
|
309
|
+
if isinstance(obj, (date, datetime)):
|
|
310
|
+
return obj.isoformat()
|
|
311
|
+
|
|
312
|
+
# Dataclasses
|
|
313
|
+
if is_dataclass(obj):
|
|
314
|
+
return self._normalizar_valores(asdict(obj))
|
|
315
|
+
|
|
316
|
+
# Pydantic v2 (duck-typing)
|
|
317
|
+
if hasattr(obj, "model_dump") and callable(getattr(obj, "model_dump")):
|
|
318
|
+
try:
|
|
319
|
+
return self._normalizar_valores(obj.model_dump())
|
|
320
|
+
except Exception:
|
|
321
|
+
pass # si no es un BaseModel real, continúa
|
|
322
|
+
|
|
323
|
+
# Binarios -> deja tal cual (para JSON tendrías que base64-encode en otro método)
|
|
324
|
+
if isinstance(obj, (bytes, bytearray, memoryview)):
|
|
325
|
+
return bytes(obj) if not isinstance(obj, bytes) else obj
|
|
326
|
+
|
|
327
|
+
# Normalizadores extra registrados por el usuario
|
|
328
|
+
for tipo, fn in self.NORMALIZADORES_EXTRA.items():
|
|
329
|
+
if isinstance(obj, tipo):
|
|
330
|
+
return fn(obj)
|
|
331
|
+
|
|
332
|
+
# Fallback: sin tocar (mejor no forzar str() para no ocultar problemas)
|
|
333
|
+
return obj
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#-*- coding: UTF-8 -*-
|
|
3
|
+
# ----------------------------------------
|
|
4
|
+
# jjandres 2025
|
|
5
|
+
# ----------------------------------------
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
import traceback
|
|
10
|
+
|
|
11
|
+
class MtpkErrorValidacionDb(Exception):
|
|
12
|
+
"""
|
|
13
|
+
Excepción específica para errores de validación dentro del ámbito de base de datos,
|
|
14
|
+
como restricciones, formatos inválidos o violaciones lógicas de consistencia.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
mensaje (str): Mensaje del error.
|
|
18
|
+
codigo (str): Código de referencia opcional.
|
|
19
|
+
"""
|
|
20
|
+
def __init__(self, mensaje: str, codigo: str = ""):
|
|
21
|
+
self.mensaje = mensaje
|
|
22
|
+
self.codigo = codigo
|
|
23
|
+
texto = f"{mensaje} [{codigo}]" if codigo else mensaje
|
|
24
|
+
super().__init__(texto)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MtpkErrorDb(Exception):
|
|
28
|
+
"""
|
|
29
|
+
Excepción base para errores críticos relacionados con operaciones de base de datos.
|
|
30
|
+
Registra automáticamente el error y su traza si se proporciona un logger.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
mensaje (str): Descripción del error.
|
|
34
|
+
codigo (str): Código de error opcional.
|
|
35
|
+
logger (Optional[Any]): Logger opcional para capturar el evento.
|
|
36
|
+
"""
|
|
37
|
+
def __init__(self, mensaje: str, codigo: str = "", logger: Optional[Any] = None):
|
|
38
|
+
self.mensaje = mensaje
|
|
39
|
+
self.codigo = codigo
|
|
40
|
+
texto_error = f"{mensaje} [{codigo}]"
|
|
41
|
+
|
|
42
|
+
if logger:
|
|
43
|
+
logger.error(texto_error)
|
|
44
|
+
logger.error(traceback.format_exc())
|
|
45
|
+
|
|
46
|
+
super().__init__(texto_error)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#-*- coding: UTF-8 -*-
|
|
3
|
+
# ----------------------------------------
|
|
4
|
+
# jjandres 2025
|
|
5
|
+
# ----------------------------------------
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
import asyncio
|
|
9
|
+
from importlib import import_module
|
|
10
|
+
from .core_sync import SQLLiteral
|
|
11
|
+
|
|
12
|
+
# Solo para análisis de tipos
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .core_sync import Tabla, Database, Columna, ForeignKey, Index
|
|
15
|
+
|
|
16
|
+
# Importa ambos backends
|
|
17
|
+
_sync = import_module("mtpk_postgres.core_sync")
|
|
18
|
+
_async = import_module("mtpk_postgres.async_adapter")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Modo por defecto
|
|
22
|
+
DEFAULT_ASYNC_MODE = False
|
|
23
|
+
|
|
24
|
+
def set_async_mode(value: bool):
|
|
25
|
+
"""Permite cambiar dinámicamente el modo por defecto (por código o startup FastAPI)."""
|
|
26
|
+
global DEFAULT_ASYNC_MODE # pylint: disable=global-statement
|
|
27
|
+
DEFAULT_ASYNC_MODE = value
|
|
28
|
+
|
|
29
|
+
def is_async_context():
|
|
30
|
+
"""Detecta si estamos dentro de un bucle async activo."""
|
|
31
|
+
try:
|
|
32
|
+
return asyncio.current_task() is not None
|
|
33
|
+
except RuntimeError:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
def resolve_backend(async_mode=None):
|
|
37
|
+
"""Decide qué backend usar."""
|
|
38
|
+
if async_mode is None:
|
|
39
|
+
async_mode = DEFAULT_ASYNC_MODE or is_async_context()
|
|
40
|
+
return _async if async_mode else _sync
|
|
41
|
+
|
|
42
|
+
# Fábricas para clases
|
|
43
|
+
def get_Tabla(async_mode=None):
|
|
44
|
+
return resolve_backend(async_mode).Tabla
|
|
45
|
+
|
|
46
|
+
def get_Columna(async_mode=None):
|
|
47
|
+
return resolve_backend(async_mode).Columna
|
|
48
|
+
|
|
49
|
+
def get_ForeignKey(async_mode=None):
|
|
50
|
+
return resolve_backend(async_mode).ForeignKey
|
|
51
|
+
|
|
52
|
+
def get_Index(async_mode=None):
|
|
53
|
+
return resolve_backend(async_mode).Index
|
|
54
|
+
|
|
55
|
+
def get_Database(async_mode=None):
|
|
56
|
+
return resolve_backend(async_mode).Database
|
|
57
|
+
|
|
58
|
+
# Exposición por defecto usando el backend predeterminado (útil para autocompletado)
|
|
59
|
+
Tabla = get_Tabla()
|
|
60
|
+
Columna = get_Columna()
|
|
61
|
+
ForeignKey = get_ForeignKey()
|
|
62
|
+
Index = get_Index()
|
|
63
|
+
Database = get_Database()
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
"Tabla", "Columna", "ForeignKey", "Index", "Database",
|
|
67
|
+
"get_Tabla", "get_Columna", "get_ForeignKey", "get_Index", "get_Database",
|
|
68
|
+
"set_async_mode", "is_async_context", "SQLLiteral"
|
|
69
|
+
]
|