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/utils.py ADDED
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env python3
2
+ #-*- coding: UTF-8 -*-
3
+ # ----------------------------------------
4
+ # jjandres 2025
5
+ # ----------------------------------------
6
+
7
+
8
+ import bcrypt
9
+ import hashlib, hmac
10
+ import secrets
11
+ import json
12
+ import logging
13
+ from typing import Optional, List, Dict, Any, Union, Literal, Callable
14
+ from .core_sync import Columna
15
+ from pydantic import BaseModel
16
+ from decimal import Decimal
17
+ from functools import wraps
18
+
19
+
20
+ def hash_password(plain_password: str) -> str:
21
+ """
22
+ Genera un hash seguro de una contraseña en texto plano.
23
+
24
+ Args:
25
+ plain_password (str): Contraseña original en texto plano.
26
+
27
+ Returns:
28
+ str: Hash seguro (en formato string) listo para almacenar en la base de datos.
29
+ """
30
+ salt = bcrypt.gensalt()
31
+ hashed = bcrypt.hashpw(plain_password.encode(), salt)
32
+ return hashed.decode()
33
+
34
+
35
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
36
+ """
37
+ Verifica si una contraseña en texto plano coincide con su hash almacenado.
38
+
39
+ Args:
40
+ plain_password (str): Contraseña introducida por el usuario.
41
+ hashed_password (str): Hash previamente almacenado.
42
+
43
+ Returns:
44
+ bool: True si coinciden, False si no.
45
+ """
46
+ return bcrypt.checkpw(
47
+ plain_password.encode(),
48
+ hashed_password.encode()
49
+ )
50
+
51
+
52
+ def generar_api_key() -> str:
53
+ """
54
+ Genera una api_key aleatoria de ~43 caracteres seguros.
55
+ - Returns:
56
+ - str: cadena api_key aleatoria
57
+ """
58
+ return secrets.token_urlsafe(32)
59
+
60
+
61
+ def generar_api_secret() -> str:
62
+ """
63
+ Genera una api_key aleatoria de ~65 caracteres seguros
64
+ - Returns:
65
+ 2-tuple:
66
+ - str: cadena api_secret aleatoria.
67
+ - str: cadena api_secret hasheada
68
+ """
69
+ secreto = secrets.token_urlsafe(48) # ~65 caracteres
70
+ secreto_hash = hashlib.sha256(secreto.encode()).hexdigest()
71
+ return secreto, secreto_hash # Retorna secreto plano y su hash
72
+
73
+ def verificar_api_secret(api_secret: str, hashed_api_secret: str) -> bool:
74
+ """
75
+ Verifica si un API_SECRET en texto plano coincide con su hash SHA-256.
76
+
77
+ Args:
78
+ api_secret (str): API_SECRET en texto plano.
79
+ hashed_api_secret (str): Hash SHA-256 (hex) previamente almacenado.
80
+
81
+ Returns:
82
+ bool: True si coinciden, False en caso contrario.
83
+ """
84
+ # Calcula el hash del secreto proporcionado
85
+ calc_hash = hashlib.sha256(api_secret.encode()).hexdigest()
86
+ # Compara de forma segura para evitar ataques de tiempo
87
+ return hmac.compare_digest(calc_hash, hashed_api_secret)
88
+
89
+
90
+ def generar_lista_campos(campos, alias=None, excluir=None, prefijo_alias=None):
91
+ """
92
+ Genera una lista de campos SQL a partir de un diccionario, lista de strings o lista de objetos Columna,
93
+ con alias opcional, campos a excluir y alias renombrado con prefijo.
94
+
95
+ :param `campos`: (Requerido) Diccionario {nombre_campo: Column}, lista de strings o lista de objetos Columna
96
+ :param `alias`: (Opcional) Alias para los campos (string), ej: 'u'
97
+ :param `excluir`: (Opcional) Lista de campos a excluir (lista de strings)
98
+ :param `prefijo_alias`: (Opcional) Prefijo para renombrar los campos con alias: <alias>.<campo> AS <prefijo><campo>
99
+ :return: String con lista de campos separados por coma
100
+ """
101
+ excluir = excluir or []
102
+ lista = []
103
+
104
+ if isinstance(campos, dict):
105
+ iterable = campos.items()
106
+ elif isinstance(campos, list):
107
+ if all(hasattr(col, 'nombre') for col in campos):
108
+ iterable = [(col.nombre, col) for col in campos]
109
+ else:
110
+ iterable = [(nombre, None) for nombre in campos]
111
+ else:
112
+ raise TypeError("El parámetro 'campos' debe ser un dict, una lista de strings o una lista de objetos Columna.")
113
+
114
+ for nombre, _ in iterable:
115
+ if nombre in excluir:
116
+ continue
117
+ if alias:
118
+ campo_sql = f"{alias}.{nombre}"
119
+ else:
120
+ campo_sql = nombre
121
+
122
+ if prefijo_alias:
123
+ alias_sql = f"{prefijo_alias}{nombre}"
124
+ lista.append(f"{campo_sql} AS {alias_sql}")
125
+ else:
126
+ lista.append(campo_sql)
127
+
128
+ return ", ".join(lista)
129
+
130
+
131
+ class FiltroCampo(BaseModel):
132
+ """Crea modelo para definir un filtro de campo SQL."""
133
+ op: Literal["=", "!=", ">", "<", ">=", "<=", "like", "in", "between"]
134
+ valor: Any
135
+
136
+
137
+
138
+ def construir_condiciones_sql(filtros: dict[str, dict[str, Union[str, Any]]], alias: Optional[dict[str, str]] = None) -> tuple[str, list[Any]]:
139
+ """
140
+ Construye una cláusula WHERE SQL a partir de un diccionario de filtros.
141
+
142
+ Cada entrada del diccionario debe tener la forma:
143
+ campo: {"op": operador_sql, "valor": valor_o_lista}
144
+
145
+ Operadores soportados (case-insensitive):
146
+ '=', '!=', '>', '<', '>=', '<=', 'like', 'in', 'between'
147
+
148
+ Parámetros:
149
+ - `filtros`: Diccionario de filtros con campo, operador y valor.
150
+ - `alias`: Diccionario opcional con alias por campo, p.ej. {'nombre_campo': 'a.nombre_alias'}
151
+
152
+ Returns:
153
+ (condiciones_sql, valores): la cláusula WHERE (sin "WHERE") y la lista de parámetros.
154
+ """
155
+ condiciones: List[str] = []
156
+ valores: List[Any] = []
157
+
158
+ def _col(campo: str) -> str:
159
+ return alias.get(campo, campo) if alias else campo
160
+
161
+ def _is_seq(x: Any) -> bool:
162
+ return isinstance(x, (list, tuple))
163
+
164
+ for campo, cond in (filtros or {}).items():
165
+ if not isinstance(cond, dict):
166
+ # Compatibilidad: ignorar entradas mal formadas
167
+ continue
168
+
169
+ op_raw = cond.get("op")
170
+ val = cond.get("valor")
171
+ if not isinstance(op_raw, str):
172
+ # Compatibilidad: ignorar operador inválido
173
+ continue
174
+
175
+ op = op_raw.strip().lower()
176
+ campo_sql = _col(campo)
177
+
178
+ # Operadores simples y nulos
179
+ if op in {"=", "!="}:
180
+ # None -> IS NULL / IS NOT NULL
181
+ if val is None:
182
+ condiciones.append(f"{campo_sql} IS {'NOT ' if op == '!=' else ''}NULL")
183
+ continue
184
+
185
+ # List/Tuple -> IN / NOT IN
186
+ if _is_seq(val):
187
+ seq = list(val)
188
+ if not seq:
189
+ # Compatibilidad: ignorar si viene vacío
190
+ continue
191
+ placeholders = ", ".join(["%s"] * len(seq))
192
+ condiciones.append(f"{campo_sql} {'NOT ' if op == '!=' else ''}IN ({placeholders})")
193
+ valores.extend(seq)
194
+ continue
195
+
196
+ # Escalar
197
+ condiciones.append(f"{campo_sql} {op} %s")
198
+ valores.append(val)
199
+ continue
200
+
201
+ if op in {">", "<", ">=", "<="}:
202
+ condiciones.append(f"{campo_sql} {op} %s")
203
+ valores.append(val)
204
+ continue
205
+
206
+ if op == "like":
207
+ if val is None:
208
+ # Compatibilidad: ignorar filtro inválido
209
+ continue
210
+ condiciones.append(f"{campo_sql} LIKE %s")
211
+ valores.append(val)
212
+ continue
213
+
214
+ if op == "in":
215
+ if not _is_seq(val):
216
+ # Compatibilidad: ignorar si no es lista/tupla
217
+ continue
218
+ seq = list(val)
219
+ if not seq:
220
+ # Compatibilidad: ignorar IN vacío
221
+ continue
222
+ placeholders = ", ".join(["%s"] * len(seq))
223
+ condiciones.append(f"{campo_sql} IN ({placeholders})")
224
+ valores.extend(seq)
225
+ continue
226
+
227
+ if op == "between":
228
+ # Acepta [desde, hasta] o {"desde": x, "hasta": y}
229
+ desde = hasta = None
230
+ if isinstance(val, dict):
231
+ desde = val.get("desde", None)
232
+ hasta = val.get("hasta", None)
233
+ elif _is_seq(val) and len(val) >= 2:
234
+ desde, hasta = val[0], val[1]
235
+ elif val is None:
236
+ # Compatibilidad: ignorar
237
+ continue
238
+ else:
239
+ # Compatibilidad/utility: si viene solo un valor, tratar como '='
240
+ condiciones.append(f"{campo_sql} = %s")
241
+ valores.append(val)
242
+ continue
243
+
244
+ if desde is not None and hasta is not None:
245
+ condiciones.append(f"{campo_sql} BETWEEN %s AND %s")
246
+ valores.extend([desde, hasta])
247
+ elif desde is not None:
248
+ condiciones.append(f"{campo_sql} >= %s")
249
+ valores.append(desde)
250
+ elif hasta is not None:
251
+ condiciones.append(f"{campo_sql} <= %s")
252
+ valores.append(hasta)
253
+ # si ambos None, no añadimos nada (compatibilidad)
254
+ continue
255
+
256
+ # Operador no soportado: compatibilidad -> ignorar
257
+ continue
258
+
259
+ condiciones_sql = " AND ".join(condiciones)
260
+ return condiciones_sql, valores
261
+
262
+
263
+ def resolver_orden_sql(orden: str, alias: Optional[dict[str, str]] = None) -> str:
264
+ """
265
+ Convierte una cadena de orden (como 'nombre DESC') en una cláusula SQL segura,
266
+ aplicando alias si están definidos.
267
+
268
+ Ejemplo:
269
+ orden = "nombre DESC"
270
+ alias = {"nombre": "a.nombre"}
271
+ => "a.nombre DESC"
272
+
273
+ Si el campo no está en alias, se usa tal cual (bajo tu responsabilidad).
274
+
275
+ Args:
276
+ orden (str): Campo o campos por los que ordenar, separados por coma.
277
+ alias (dict[str, str], optional): Diccionario de alias.
278
+
279
+ Returns:
280
+ str: Expresión segura de ordenación SQL.
281
+ """
282
+ if not orden:
283
+ return ""
284
+
285
+ campos_orden = []
286
+ for parte in orden.split(","):
287
+ tokens = parte.strip().split()
288
+ campo = tokens[0]
289
+ direccion = tokens[1].upper() if len(tokens) > 1 and tokens[1].upper() in {"ASC", "DESC"} else "ASC"
290
+
291
+ campo_sql = alias.get(campo, campo) if alias else campo
292
+ campos_orden.append(f"{campo_sql} {direccion}")
293
+
294
+ return ", ".join(campos_orden)
@@ -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,12 @@
1
+ mtpk_postgres/__init__.py,sha256=GOvt_tgF0FaeTLJ5UKYawMTkQcEAzB4CSMrVHO_HxwI,104
2
+ mtpk_postgres/async_adapter.py,sha256=625g5_ObLuwdZ4Ck5sGW9ZgwLz34UIi8zlRNGsDESv8,13332
3
+ mtpk_postgres/core_sync.py,sha256=bspjGTZi3eoUE3o2yXirSW6yBJuveflMeZHTa017PFQ,74153
4
+ mtpk_postgres/crud.py,sha256=ks3Wewwp-1LAvfaDAcz665gI4I_-Nx_xTw3h3VC9cNE,14348
5
+ mtpk_postgres/excepciones.py,sha256=e3QUo5yOCeIpAMl9T7d5vVLLRievEDy0KYk3NnsuKvY,1486
6
+ mtpk_postgres/interface.py,sha256=ke9CJKuvVcwWKG1kEXrPuPl0Tb5A_jjMPJTctV6y7zs,2039
7
+ mtpk_postgres/utils.py,sha256=pG_wU1JMpfUbSE_90X4TIu5Ind41o0h3j3gaYUFlhfI,9869
8
+ mtpk_postgres-0.1.0.dist-info/licenses/LICENSE,sha256=9P-e1cTXCTuepzDnhWFnAyWg-SiAsVzPTZymN2nH4Pw,1086
9
+ mtpk_postgres-0.1.0.dist-info/METADATA,sha256=81x8zseU_2Ogek5uvuSnKavNajwAe5rXx8THgkW6TKE,2325
10
+ mtpk_postgres-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
11
+ mtpk_postgres-0.1.0.dist-info/top_level.txt,sha256=UqdjUccq2zmrlXW616wuwUrog-SRqL0JouIKg5-eMPw,14
12
+ mtpk_postgres-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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 @@
1
+ mtpk_postgres