chastack-bdd 0.1.1__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.
- chastack_bdd-0.1.1/LICENSE +43 -0
- chastack_bdd-0.1.1/PKG-INFO +34 -0
- chastack_bdd-0.1.1/README.md +14 -0
- chastack_bdd-0.1.1/chastack_bdd/__init__.py +23 -0
- chastack_bdd-0.1.1/chastack_bdd/bdd.py +405 -0
- chastack_bdd-0.1.1/chastack_bdd/errores/__init__.py +15 -0
- chastack_bdd-0.1.1/chastack_bdd/pruebas.py +24 -0
- chastack_bdd-0.1.1/chastack_bdd/registro.py +121 -0
- chastack_bdd-0.1.1/chastack_bdd/tabla.py +133 -0
- chastack_bdd-0.1.1/chastack_bdd/tipos/__init__.py +24 -0
- chastack_bdd-0.1.1/chastack_bdd/tipos/enum_sql.py +56 -0
- chastack_bdd-0.1.1/chastack_bdd/utiles.py +39 -0
- chastack_bdd-0.1.1/chastack_bdd.egg-info/PKG-INFO +34 -0
- chastack_bdd-0.1.1/chastack_bdd.egg-info/SOURCES.txt +18 -0
- chastack_bdd-0.1.1/chastack_bdd.egg-info/dependency_links.txt +1 -0
- chastack_bdd-0.1.1/chastack_bdd.egg-info/requires.txt +3 -0
- chastack_bdd-0.1.1/chastack_bdd.egg-info/top_level.txt +1 -0
- chastack_bdd-0.1.1/pyproject.toml +36 -0
- chastack_bdd-0.1.1/setup.cfg +4 -0
- chastack_bdd-0.1.1/setup.py +11 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Licencia MIT
|
|
2
|
+
|
|
3
|
+
Derechos de autor (c) 2023 Hernán A. Teszkiewicz Novick
|
|
4
|
+
|
|
5
|
+
Se concede permiso, de forma gratuita, a cualquier persona que obtenga una copia
|
|
6
|
+
de este software y los archivos de documentación asociados (el "Software"), para utilizar
|
|
7
|
+
el Software sin restricción, incluyendo, sin limitación, los derechos
|
|
8
|
+
para usar, copiar, modificar, fusionar, publicar, distribuir, sublicenciar y / o vender
|
|
9
|
+
copias del Software, y para permitir a las personas a quienes se les proporcione el Software
|
|
10
|
+
hacerlo, sujeto a las siguientes condiciones:
|
|
11
|
+
|
|
12
|
+
El aviso de derechos de autor anterior y este aviso de permiso se incluirán en todos
|
|
13
|
+
las copias o partes sustanciales del Software.
|
|
14
|
+
|
|
15
|
+
EL SOFTWARE SE PROPORCIONA "TAL CUAL", SIN GARANTÍA DE NINGÚN TIPO, EXPRESA O
|
|
16
|
+
IMPLÍCITA, INCLUYENDO, PERO NO LIMITADO A, LAS GARANTÍAS DE COMERCIALIZACIÓN,
|
|
17
|
+
ADECUACIÓN PARA UN PROPÓSITO PARTICULAR Y NO INFRACCIÓN. EN NINGÚN CASO
|
|
18
|
+
LOS TITULARES DE LOS DERECHOS DE AUTOR O LOS AUTORES SERÁN RESPONSABLES DE
|
|
19
|
+
NINGUNA RECLAMACIÓN, DAÑOS U OTRAS RESPONSABILIDADES, YA SEA EN UNA ACCIÓN DE
|
|
20
|
+
CONTRATO, AGRAVIO O DE CUALQUIER OTRA NATURALEZA, DERIVADAS DE, FUERA DE O EN CONEXIÓN CON EL
|
|
21
|
+
SOFTWARE O EL USO U OTROS VERSIONES, DISTRIBUCIONES Y ACUERDOS CONCERNIENTES AL SOFTWARE.
|
|
22
|
+
|
|
23
|
+
MIT License
|
|
24
|
+
|
|
25
|
+
Copyright (c) 2023 Hernan A. Teszkiewicz Novick
|
|
26
|
+
|
|
27
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
28
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
29
|
+
in the Software without restriction, including without limitation the rights
|
|
30
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
31
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
32
|
+
furnished to do so, subject to the following conditions:
|
|
33
|
+
|
|
34
|
+
The above copyright notice and this permission notice shall be included in all
|
|
35
|
+
copies or substantial portions of the Software.
|
|
36
|
+
|
|
37
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
38
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
39
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
40
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
41
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
42
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
43
|
+
SOFTWARE.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: chastack-bdd
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Abstracción de Bases de Datos en Python y constructor de modelos.
|
|
5
|
+
Home-page: https://github.com/Hernanatn/bdd.py
|
|
6
|
+
Author: Hernán A. Teszkiewicz Novick
|
|
7
|
+
Author-email: "Hernán A.T.N." <herni@cajadeideas.ar>, Joaquín Correa <correajoaquin7@gmail.com>, "Camila M.T.N." <cmteszkiewicz@gmail.com>
|
|
8
|
+
Project-URL: Homepage, https://github.com/Hernanatn/bdd.py
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: sobrecargar
|
|
16
|
+
Requires-Dist: solteron
|
|
17
|
+
Requires-Dist: mysql-connector-python
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# BDD
|
|
24
|
+
|
|
25
|
+
[](https://cajadeideas.ar)
|
|
26
|
+
[](https://github.com/hernanatn/github.com/hernanatn/bdd.py/releases/latest)
|
|
27
|
+
[](https://www.python.org/downloads/release/python-3120/)
|
|
28
|
+
[](LICENSE)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Descripción
|
|
32
|
+
Módulo de Python 3.x con abstracciones útiles para gestión de Bases de Datos y construcción de modelos.
|
|
33
|
+
|
|
34
|
+
## [Documentación](/docs)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# BDD
|
|
4
|
+
|
|
5
|
+
[](https://cajadeideas.ar)
|
|
6
|
+
[](https://github.com/hernanatn/github.com/hernanatn/bdd.py/releases/latest)
|
|
7
|
+
[](https://www.python.org/downloads/release/python-3120/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## Descripción
|
|
12
|
+
Módulo de Python 3.x con abstracciones útiles para gestión de Bases de Datos y construcción de modelos.
|
|
13
|
+
|
|
14
|
+
## [Documentación](/docs)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
===============
|
|
3
|
+
chastack-bdd
|
|
4
|
+
===============
|
|
5
|
+
Módulo de Python 3.x con abstracciones útiles para gestión de Bases de Datos y construcción de modelos.
|
|
6
|
+
|
|
7
|
+
* Repositorio del proyecto: https://github.com/Hernanatn/bdd.py
|
|
8
|
+
|
|
9
|
+
Derechos de autor (c) 2025 Ch'aska SRL. Distribuído bajo licencia MIT.
|
|
10
|
+
Autores:
|
|
11
|
+
- Hernan ATN | herni@cajadeideas.ar
|
|
12
|
+
- Joaquín C. | correajoaquin7@gmail.com
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from chastack_bdd.tipos import *
|
|
16
|
+
from chastack_bdd.errores import *
|
|
17
|
+
from chastack_bdd.utiles import *
|
|
18
|
+
from chastack_bdd.bdd import *
|
|
19
|
+
from chastack_bdd.tabla import *
|
|
20
|
+
from chastack_bdd.registro import *
|
|
21
|
+
|
|
22
|
+
if __name__ == '__main__':
|
|
23
|
+
print(__doc__)
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
from chastack_bdd.tipos import *
|
|
2
|
+
from chastack_bdd.errores import *
|
|
3
|
+
from chastack_bdd.utiles import *
|
|
4
|
+
from mysql.connector import connect
|
|
5
|
+
|
|
6
|
+
@runtime_checkable
|
|
7
|
+
class ProtocoloBaseDeDatos(Protocol):
|
|
8
|
+
def DESCRIBE(self: Self, tabla :str) -> Self: ...
|
|
9
|
+
def SELECT(self : Self, tabla : str, columnas : list[str], columnasSecundarias: Optional[dict[str, list[str]] ] = {}) -> Self:...
|
|
10
|
+
def DELETE(self : Self, tabla : str) -> Self: ...
|
|
11
|
+
def INSET(self : Self, tabla : str, **asignaciones : Unpack[dict[str, Any]]) -> Self: ...
|
|
12
|
+
def UPDATE(self : Self, tabla : str, **asignaciones : Unpack[dict[str, Any]]) -> Self: ...
|
|
13
|
+
def WHERE(self : Self, tipoCondicion : TipoCondicion = TipoCondicion.IGUAL , **columnaValor : Unpack[dict[str, Any]]) -> Self: ...
|
|
14
|
+
def JOIN(self : Self, tablaSecundaria, columnaPrincipal, columnaSecundaria, tipoUnion : TipoUnion = TipoUnion.INNER) -> Self: ...
|
|
15
|
+
def LIMIT(self : Self, desplazamiento: int , limite : int) -> Self: ...
|
|
16
|
+
def __enter__(self) -> Self: ...
|
|
17
|
+
def __exit__(self, exc_type,excl_val,exc_tb) -> None: ...
|
|
18
|
+
def ejecutar(self: Self) -> Self :...
|
|
19
|
+
def devolverUnResultado(self: Self) -> Resultado :...
|
|
20
|
+
def devolverResultados(self: Self) -> tuple[Resultado] :...
|
|
21
|
+
def devolverIdUltimaInsercion(self:Self) -> int :...
|
|
22
|
+
|
|
23
|
+
class InstruccionPrincipal():
|
|
24
|
+
'''
|
|
25
|
+
Clase que permite definir la clausula principal de una consulta SQL.
|
|
26
|
+
Altamente acoplada con la clase Consulta y dependiente de la misma.
|
|
27
|
+
'''
|
|
28
|
+
|
|
29
|
+
_slots__ = \
|
|
30
|
+
(
|
|
31
|
+
'__instruccion'
|
|
32
|
+
)
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self.__instruccion = ''
|
|
35
|
+
|
|
36
|
+
def chequearOcupado(self):
|
|
37
|
+
if self.__instruccion: raise ErrorMalaSintaxisSQL("La clausula principal ya ha sido definida.")
|
|
38
|
+
def esDescribe(self):
|
|
39
|
+
self.__instruccion = 'DESCRIBE'
|
|
40
|
+
def esInsert(self):
|
|
41
|
+
self.__instruccion = 'INSERT'
|
|
42
|
+
def esSelect(self):
|
|
43
|
+
self.__instruccion = 'SELECT'
|
|
44
|
+
def esDelete(self):
|
|
45
|
+
self.__instruccion = 'DELETE'
|
|
46
|
+
def esUpdate(self):
|
|
47
|
+
self.__instruccion = 'UPDATE'
|
|
48
|
+
def construirConsulta(self, parametrosPrincipales, condicion, union, limite):
|
|
49
|
+
if not self.__instruccion: raise ErrorMalaSintaxisSQL("No se ha definido una clausula principal.")
|
|
50
|
+
if self.__instruccion == 'INSERT':
|
|
51
|
+
if condicion or union or limite: raise ErrorMalaSintaxisSQL("Las instrucciones INSERT no pueden tener clausulas WHERE, JOIN o LIMIT.")
|
|
52
|
+
return self.__instruccion + '\n' + parametrosPrincipales + condicion + union + limite + ';'
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Consulta():
|
|
56
|
+
'''
|
|
57
|
+
Clase que permite generar consultas SQL de forma programática. Las consultas se construyen concatenando
|
|
58
|
+
las clausulas principales (SELECT, DELETE, INSET, UPDATE) y las clausulas secundarias (WHERE, JOIN). Luego
|
|
59
|
+
se espera que el objeto sea convertido a string para obtener la consulta SQL.
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
METODOS PUBLICOS
|
|
63
|
+
- SELECT(tabla : str, columnas : list[str] columnasSecundarias: Optional[Dict[str, List[str]] ] = {}) -> Self
|
|
64
|
+
- DELETE(tabla : str) -> Self
|
|
65
|
+
- INSET(tabla : str, **asignaciones : Unpack[dict[str, Any]]) -> Self
|
|
66
|
+
- UPDATE(tabla : str, **asignaciones : Unpack[dict[str, Any]]) -> Self
|
|
67
|
+
- WHERE(tipoCondicion : TipoCondicion = TipoCondicion.IGUAL , **columnaValor : Unpack[dict[str, Any]]) -> Self
|
|
68
|
+
- JOIN(tablaSecundaria, columnaPrincipal, columnaSecundaria, tipoUnion : TipoUnion = TipoUnion.INNER) -> Self
|
|
69
|
+
- LIMIT(desplazamiento: int , limite : int) -> Self
|
|
70
|
+
Aclaracion: Los metodos From, Set y LIMIT no son metodos publicos, ya que son llamados internamente por los metodos que invocan clausulas principales.
|
|
71
|
+
|
|
72
|
+
ATRIBUTOS PUBLICOS
|
|
73
|
+
- TipoCondicion: Enumeracion que contiene los tipos de condiciones posibles.
|
|
74
|
+
- TipoUnion: Enumeracion que contiene los tipos de joins posibles.
|
|
75
|
+
|
|
76
|
+
EJEMPLOS DE USO:
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
> consulta = Consulta().SELECT(tabla='Usuarios', columnas=['nombreUsuario', 'correo']).WHERE(id=1).LIMIT(10, 5)
|
|
80
|
+
> print(consulta)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
SELECT
|
|
84
|
+
Usuarios.nombreUsuario, Usuarios.correo
|
|
85
|
+
FROM Usuarios
|
|
86
|
+
WHERE Usuarios.id = 1
|
|
87
|
+
LIMIT 10, 5
|
|
88
|
+
;
|
|
89
|
+
|
|
90
|
+
> consulta = Consulta().DELETE(tabla='Usuarios').WHERE(id=1)
|
|
91
|
+
> print(consulta)
|
|
92
|
+
|
|
93
|
+
DELETE
|
|
94
|
+
FROM Usuarios
|
|
95
|
+
WHERE Usuarios.PRIMARY_KEY != 'NULL'
|
|
96
|
+
AND Usuarios.id = 1
|
|
97
|
+
;
|
|
98
|
+
|
|
99
|
+
> consulta = Consulta().INSET(tabla='Usuarios', nombreUsuario='Juan')
|
|
100
|
+
|
|
101
|
+
> consulta = Consulta().SELECT(tabla='Usuarios', columnas=['nombreUsuario', 'correo'], columnasSecundarias={'Discos': 'autor'})
|
|
102
|
+
> consulta.JOIN(tablaSecundaria='Discos', columnaPrincipal='esPremium', columnaSecundaria ='esPremium', tipoUnion=TipoUnion.INNER)
|
|
103
|
+
> print(consulta)
|
|
104
|
+
|
|
105
|
+
SELECT Usuarios.nombreUsuario, Usuarios.correo, Discos.a, Discos.u, Discos.t, Discos.o, Discos.r
|
|
106
|
+
FROM Usuarios
|
|
107
|
+
INNER JOIN Discos ON Usuarios.esPremium = Discos.esPremium
|
|
108
|
+
;
|
|
109
|
+
|
|
110
|
+
CASOS DE ERROR:
|
|
111
|
+
|
|
112
|
+
La clase levanta errores de tipo ErrorMalaSintaxisSQL en los siguientes casos:
|
|
113
|
+
- Se intenta invocar una clausula principal (SELECT, DELETE, INSET, UPDATE) más de una vez.
|
|
114
|
+
- Se intenta convertir a string sin clausula principal
|
|
115
|
+
- Se intenta pedir columnas secundarias de una tabla que no ha sido unida.
|
|
116
|
+
|
|
117
|
+
La clase levanta errores de tipo ErrorMalaSolicitud en los siguientes casos:
|
|
118
|
+
|
|
119
|
+
CASOS
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
'''
|
|
123
|
+
|
|
124
|
+
_slots__ = \
|
|
125
|
+
( '__parametros_principales',
|
|
126
|
+
'__instruccionPrincipal',
|
|
127
|
+
'__tabla_principal',
|
|
128
|
+
'__tablas_secundarias',
|
|
129
|
+
'__condicion',
|
|
130
|
+
'__union',
|
|
131
|
+
'__limite')
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def __init__(self):
|
|
136
|
+
self.__instruccionPrincipal = InstruccionPrincipal()
|
|
137
|
+
self.__parametros_principales = ''
|
|
138
|
+
self.__condicion = ''
|
|
139
|
+
self.__union = ''
|
|
140
|
+
self.__limite = ''
|
|
141
|
+
|
|
142
|
+
self.__tabla_principal = ''
|
|
143
|
+
self.__tablas_secundarias = {}
|
|
144
|
+
|
|
145
|
+
def SELECT(self, tabla : str, columnas : list[str], columnasSecundarias: Optional[dict[str, list[str]] ] = {}) -> Self:
|
|
146
|
+
self.__tabla_principal = tabla
|
|
147
|
+
self.__instruccionPrincipal.esSelect()
|
|
148
|
+
self.__parametros_principales = self.etiquetar(tabla, columnas)
|
|
149
|
+
for t2, c2 in columnasSecundarias.items():
|
|
150
|
+
self.__tablas_secundarias[t2] = 0
|
|
151
|
+
self.__parametros_principales += ', ' + self.etiquetar(t2, c2)
|
|
152
|
+
self.__parametros_principales += '\n'
|
|
153
|
+
self.__FROM(tabla)
|
|
154
|
+
return self
|
|
155
|
+
def DESCRIBE(self: Self, tabla :str) -> Self:
|
|
156
|
+
self.__tabla_principal = tabla
|
|
157
|
+
self.__instruccionPrincipal.esDescribe()
|
|
158
|
+
self.__parametros_principales += self.__tabla_principal
|
|
159
|
+
return self
|
|
160
|
+
def DELETE(self, tabla : str):
|
|
161
|
+
self.__tabla_principal = tabla
|
|
162
|
+
self.__instruccionPrincipal.esDelete()
|
|
163
|
+
self.__FROM(tabla)
|
|
164
|
+
self.WHERE(TipoCondicion.NO_ES, id = None)
|
|
165
|
+
return self
|
|
166
|
+
def INSET(self, tabla : str, **asignaciones : Unpack[dict[str, Any]]):
|
|
167
|
+
self.__tabla_principal = tabla
|
|
168
|
+
self.__instruccionPrincipal.esInsert()
|
|
169
|
+
self.__parametros_principales = 'INTO ' + tabla + '\n'
|
|
170
|
+
self.__SET(**asignaciones)
|
|
171
|
+
return self
|
|
172
|
+
def UPDATE(self, tabla : str, **asignaciones : Unpack[dict[str, Any]]):
|
|
173
|
+
self.__tabla_principal = tabla
|
|
174
|
+
self.__instruccionPrincipal.esUpdate()
|
|
175
|
+
self.__parametros_principales = tabla + '\n'
|
|
176
|
+
self.__SET(**asignaciones)
|
|
177
|
+
self.WHERE(TipoCondicion.NO_ES, id = None)
|
|
178
|
+
|
|
179
|
+
return self
|
|
180
|
+
def WHERE(self, tipoCondicion : TipoCondicion = TipoCondicion.IGUAL , **columnaValor : Unpack[dict[str, Any]]):
|
|
181
|
+
condiciones : str = ' AND '.join(f"{self.etiquetar(self.__tabla_principal, [columna]) } {tipoCondicion} {self.adaptar(valor)}" for columna, valor in columnaValor.items())
|
|
182
|
+
if not self.__condicion: self.__condicion = f'WHERE {condiciones}\n'
|
|
183
|
+
else: self.__condicion += f' AND {condiciones}\n'
|
|
184
|
+
return self
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def JOIN(self, tablaSecundaria, columnaPrincipal, columnaSecundaria, tipoUnion : TipoUnion = TipoUnion.INNER):
|
|
188
|
+
self.__tablas_secundarias[tablaSecundaria] = 1
|
|
189
|
+
nuevoJoin : str = tipoUnion + ' JOIN ' + tablaSecundaria + ' ON ' + self.etiquetar(self.__tabla_principal, [columnaPrincipal]) + ' = ' + self.etiquetar(tablaSecundaria, [columnaSecundaria]) + '\n'
|
|
190
|
+
self.__union += nuevoJoin
|
|
191
|
+
return self
|
|
192
|
+
|
|
193
|
+
def LIMIT(self, desplazamiento: int , limite : int):
|
|
194
|
+
if self.__limite : raise ErrorMalaSintaxisSQL("La clausula LIMIT ya ha sido definida.")
|
|
195
|
+
self.__limite = 'LIMIT ' + str(desplazamiento) + ', ' + str(limite) + '\n'
|
|
196
|
+
return self
|
|
197
|
+
def __FROM(self, tabla : str):
|
|
198
|
+
self.__parametros_principales += 'FROM ' + tabla + '\n'
|
|
199
|
+
return self
|
|
200
|
+
def __SET(self, **columnaValor : Unpack[dict[str, Any]]):
|
|
201
|
+
asignaciones = ', '.join(f"{self.etiquetar(self.__tabla_principal, [columna]) } = {self.adaptar(valor)}" for columna, valor in columnaValor.items())
|
|
202
|
+
self.__parametros_principales += f'SET {asignaciones}\n'
|
|
203
|
+
return self
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def etiquetar(self, tabla: str, columnas : list[str]):
|
|
208
|
+
# Recibe una tabla y columnas. devuelve cada columna en el namespace de la tabla
|
|
209
|
+
return ', '.join([tabla + '.' + columna for columna in columnas])
|
|
210
|
+
|
|
211
|
+
def adaptar(self, valor : Any) -> str:
|
|
212
|
+
#En caso de que el valor sea de un tipo estpecial debe convertirse a string fuera del llamado para que la consulta no falle
|
|
213
|
+
if not valor:
|
|
214
|
+
return "NULL"
|
|
215
|
+
if type(valor) == int:
|
|
216
|
+
return str(valor)
|
|
217
|
+
else:
|
|
218
|
+
return "'" + str(valor) + "'"
|
|
219
|
+
|
|
220
|
+
def reiniciar(self):
|
|
221
|
+
self.consulta = ''
|
|
222
|
+
|
|
223
|
+
def __str__(self):
|
|
224
|
+
if not self.__parametros_principales: raise ErrorMalaSintaxisSQL("No se ha definido una clausula principal.")
|
|
225
|
+
for tabla, valor in self.__tablas_secundarias.items():
|
|
226
|
+
if valor == 0:
|
|
227
|
+
raise ErrorMalaSintaxisSQL(f"La tabla {tabla} no ha sido unida.")
|
|
228
|
+
|
|
229
|
+
return self.__instruccionPrincipal.construirConsulta(self.__parametros_principales, self.__condicion, self.__union, self.__limite)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class ConfigMySQL(metaclass=Solteron):
|
|
234
|
+
|
|
235
|
+
__slots__ = (
|
|
236
|
+
'__HOST',
|
|
237
|
+
'__USUARIO',
|
|
238
|
+
'__CONTRASENA',
|
|
239
|
+
'__NOMBRE_BDD'
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def __init__(self, host, usuario, contrasena, bdd):
|
|
243
|
+
self.__HOST = host
|
|
244
|
+
self.__USUARIO = usuario
|
|
245
|
+
self.__CONTRASENA = contrasena
|
|
246
|
+
self.__NOMBRE_BDD = bdd
|
|
247
|
+
@property
|
|
248
|
+
def PARAMETROS_CONEXION(self) -> dict:
|
|
249
|
+
return \
|
|
250
|
+
{
|
|
251
|
+
"host" : self.__HOST,
|
|
252
|
+
"user" : self.__USUARIO,
|
|
253
|
+
"password" : self.__CONTRASENA,
|
|
254
|
+
"database" : self.__NOMBRE_BDD,
|
|
255
|
+
"use_pure" : False
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def OPCION_CURSOR(self) -> dict:
|
|
260
|
+
return \
|
|
261
|
+
{
|
|
262
|
+
"dictionary" : True,
|
|
263
|
+
"named_tuple" : False,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
class BaseDeDatos_MySQL():
|
|
267
|
+
_slots__ = \
|
|
268
|
+
(
|
|
269
|
+
"__config",
|
|
270
|
+
"__conexion",
|
|
271
|
+
"__cursor",
|
|
272
|
+
"__consulta"
|
|
273
|
+
)
|
|
274
|
+
def __init__(self, configuracion : ConfigMySQL = None) -> None:
|
|
275
|
+
self.__conexion = None
|
|
276
|
+
self.__cursor = None
|
|
277
|
+
self.configurar(configuracion)
|
|
278
|
+
self.__consulta = Consulta()
|
|
279
|
+
|
|
280
|
+
def configurar(self, configuracion : ConfigMySQL = None) -> None:
|
|
281
|
+
if configuracion:
|
|
282
|
+
self.__config = configuracion
|
|
283
|
+
return self
|
|
284
|
+
# Agregar comportamiento usando las variables de ambiente
|
|
285
|
+
|
|
286
|
+
def conectar(self) -> Self:
|
|
287
|
+
if self.__conexion: return self
|
|
288
|
+
self.__conexion = connect(**self.__config.PARAMETROS_CONEXION)
|
|
289
|
+
self.__cursor = self.__conexion.cursor(buffered=True, **self.__config.OPCION_CURSOR)
|
|
290
|
+
return self
|
|
291
|
+
|
|
292
|
+
def desconectar(self) -> None:
|
|
293
|
+
if self.__cursor: self.__cursor.close()
|
|
294
|
+
if self.__conexion: self.__conexion.close()
|
|
295
|
+
self.__cursor = None
|
|
296
|
+
self.__conexion = None
|
|
297
|
+
|
|
298
|
+
def reconectar(self) -> Self:
|
|
299
|
+
self.desconectar()
|
|
300
|
+
self.conectar()
|
|
301
|
+
return self
|
|
302
|
+
|
|
303
|
+
def DESCRIBE(self: Self, tabla :str) -> Self:
|
|
304
|
+
self.__consulta.DESCRIBE(tabla)
|
|
305
|
+
return self
|
|
306
|
+
def SELECT(self, tabla : str, columnas : list[str], columnasSecundarias: Optional[dict[str, list[str]] ] = {}) -> Self:
|
|
307
|
+
self.__consulta.SELECT(tabla, columnas, columnasSecundarias)
|
|
308
|
+
return self
|
|
309
|
+
def DELETE(self, tabla : str) -> Self:
|
|
310
|
+
self.__consulta.DELETE(tabla)
|
|
311
|
+
return self
|
|
312
|
+
def INSET(self, tabla : str, **asignaciones : Unpack[dict[str, Any]]) -> Self:
|
|
313
|
+
self.__consulta.INSET(tabla, **asignaciones)
|
|
314
|
+
return self
|
|
315
|
+
def UPDATE(self, tabla : str, **asignaciones : Unpack[dict[str, Any]]) -> Self:
|
|
316
|
+
self.__consulta.UPDATE( tabla, **asignaciones)
|
|
317
|
+
return self
|
|
318
|
+
def WHERE(self, tipoCondicion : TipoCondicion = TipoCondicion.IGUAL , **columnaValor : Unpack[dict[str, Any]]) -> Self:
|
|
319
|
+
self.__consulta.WHERE(tipoCondicion, **columnaValor)
|
|
320
|
+
return self
|
|
321
|
+
def JOIN(self, tablaSecundaria, columnaPrincipal, columnaSecundaria, tipoUnion : TipoUnion = TipoUnion.INNER) -> Self:
|
|
322
|
+
self.__consulta.JOIN(tablaSecundaria, columnaPrincipal, columnaSecundaria, tipoUnion)
|
|
323
|
+
return self
|
|
324
|
+
def LIMIT(self, desplazamiento: int , limite : int) -> Self:
|
|
325
|
+
self.__consulta.LIMIT(desplazamiento, limite)
|
|
326
|
+
return self
|
|
327
|
+
|
|
328
|
+
@sobrecargar
|
|
329
|
+
def ejecutar(self, consulta : Union[Consulta,str]) -> Optional[list[Resultado]] :
|
|
330
|
+
if isinstance(consulta,Consulta):
|
|
331
|
+
consulta = str(consulta)
|
|
332
|
+
try:
|
|
333
|
+
self.__cursor.execute(consulta)
|
|
334
|
+
self.__conexion.commit()
|
|
335
|
+
except ErrorBDD as e:
|
|
336
|
+
###print(f"[ERROR] {e}")
|
|
337
|
+
self.reconectar()
|
|
338
|
+
self.__cursor.execute(consulta)
|
|
339
|
+
self.__conexion.commit()
|
|
340
|
+
except AttributeError as e:
|
|
341
|
+
###print(f"[ERROR] {e}")
|
|
342
|
+
self = BaseDeDatos_MySQL()
|
|
343
|
+
self.conectar()
|
|
344
|
+
self.__cursor.execute(consulta)
|
|
345
|
+
self.__conexion.commit()
|
|
346
|
+
except Exception as f:
|
|
347
|
+
raise type(f)(f"No se pudo completar la consulta.\n Es probable que la consulta incluya carácteres prohibidos. \n {consulta.encode('utf-8').decode('unicode_escape')}\n") from f
|
|
348
|
+
return self
|
|
349
|
+
|
|
350
|
+
@sobrecargar
|
|
351
|
+
def ejecutar(self) :
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
self.__cursor.execute(str(self.__consulta))
|
|
355
|
+
self.__conexion.commit()
|
|
356
|
+
except ErrorBDD as e:
|
|
357
|
+
###print(f"[ERROR] {e}")
|
|
358
|
+
self.reconectar()
|
|
359
|
+
self.__cursor.execute(str(self.__consulta))
|
|
360
|
+
self.__conexion.commit()
|
|
361
|
+
except AttributeError as e:
|
|
362
|
+
###print(f"[ERROR] {e}")
|
|
363
|
+
self = BaseDeDatos_MySQL()
|
|
364
|
+
self.conectar()
|
|
365
|
+
self.__cursor.execute(str(self.__consulta))
|
|
366
|
+
self.__conexion.commit()
|
|
367
|
+
except Exception as f:
|
|
368
|
+
raise type(f)(f"No se pudo completar la consulta.\n Es probable que la consulta incluya carácteres prohibidos. \n {str(self.__consulta).encode('utf-8').decode('unicode_escape')}\n") from f
|
|
369
|
+
|
|
370
|
+
self.__consulta.reiniciar()
|
|
371
|
+
return self
|
|
372
|
+
|
|
373
|
+
def devolverIdUltimaInsercion(self) -> Optional[int]:
|
|
374
|
+
return self.__cursor.lastrowid
|
|
375
|
+
|
|
376
|
+
def devolverResultados(self, cantidad : Optional[int] = None) -> Optional[List[Dict[str, Any]]]:
|
|
377
|
+
resultados = self.__cursor.fetchall()
|
|
378
|
+
|
|
379
|
+
if not resultados: return None
|
|
380
|
+
elif cantidad is None: return resultados
|
|
381
|
+
elif cantidad == 0: return []
|
|
382
|
+
elif cantidad > 0: return resultados[0:cantidad-1]
|
|
383
|
+
else: raise IndexError("Se solicitó una cantidad negativa de resultados, lo cual es un sinsentido.")
|
|
384
|
+
def devolverUnResultado(self) -> Optional[Dict[str, Any]]:
|
|
385
|
+
"""
|
|
386
|
+
Devuelve el primer resultado de la última consulta.
|
|
387
|
+
"""
|
|
388
|
+
return self.__cursor.fetchone()
|
|
389
|
+
|
|
390
|
+
# Estados
|
|
391
|
+
def estaConectado (self):
|
|
392
|
+
return self.__conexion.is_connected() if self.__conexion else False
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# with BaseDeDatos() as bdd
|
|
397
|
+
def __enter__(self) -> 'BaseDeDatos_MySQL':
|
|
398
|
+
if self.__conexion is None:
|
|
399
|
+
return self.conectar()
|
|
400
|
+
# ###print(f"[DEBUG] Entrando {self.__cursor=}{self.__conexion=}{self.__pool=}")
|
|
401
|
+
return self
|
|
402
|
+
|
|
403
|
+
def __exit__(self, exc_type,excl_val,exc_tb) -> None:
|
|
404
|
+
# ###print(f"[DEBUG] Saliendo {self.__cursor=}{self.__conexion=}{self.__pool=}")
|
|
405
|
+
self.desconectar()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#Errores
|
|
2
|
+
class ErrorBDD(Exception):...
|
|
3
|
+
|
|
4
|
+
class ErrorMalaSolicitud(ErrorBDD):
|
|
5
|
+
"""Excepción para errores relacionados con solicitudes"""
|
|
6
|
+
class SinResultado(ErrorMalaSolicitud): ...
|
|
7
|
+
class ErrorTablaNoExiste(ErrorBDD): ...
|
|
8
|
+
class ErrorBaseDeDatosNoExiste(ErrorBDD): ...
|
|
9
|
+
|
|
10
|
+
class ErrorPoolLlena(ErrorBDD): ...
|
|
11
|
+
class ErrorDemasiadasConexiones(ErrorBDD): ...
|
|
12
|
+
|
|
13
|
+
class IdsNoCoinciden(ErrorBDD): ...
|
|
14
|
+
|
|
15
|
+
class ErrorAPI(Exception): ...
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from chastack_bdd.tipos import *
|
|
2
|
+
from chastack_bdd.utiles import *
|
|
3
|
+
from chastack_bdd.bdd import ConfigMySQL, BaseDeDatos_MySQL
|
|
4
|
+
from chastack_bdd.tabla import Tabla
|
|
5
|
+
from chastack_bdd.registro import Registro
|
|
6
|
+
|
|
7
|
+
class Discos(metaclass=Tabla):
|
|
8
|
+
def devolverArtista(self):
|
|
9
|
+
return self.idAutor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
config = ConfigMySQL("localhost", "servidor_local", "Servidor!1234", "BaseDePrueba")
|
|
13
|
+
bdd = BaseDeDatos_MySQL(config)
|
|
14
|
+
disco = Discos(bdd=bdd,id=2)
|
|
15
|
+
|
|
16
|
+
#print(disco.nombreUsuario)
|
|
17
|
+
print(disco.tabla)
|
|
18
|
+
print(disco.id)
|
|
19
|
+
print(disco.tipo.haciaCadena())
|
|
20
|
+
print(disco.soporte)
|
|
21
|
+
print(disco.devolverArtista())
|
|
22
|
+
|
|
23
|
+
print(f"{Discos.TipoSoporte.desdeCadena("DIGITAL").value=}")
|
|
24
|
+
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from chastack_bdd.tipos import *
|
|
2
|
+
from chastack_bdd.utiles import *
|
|
3
|
+
from chastack_bdd.bdd import ProtocoloBaseDeDatos
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Registro: ...
|
|
8
|
+
class Registro:
|
|
9
|
+
__slots__ = (
|
|
10
|
+
'__bdd',
|
|
11
|
+
'__id',
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__bdd : ProtocoloBaseDeDatos
|
|
15
|
+
__id : int
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def id(self):
|
|
19
|
+
return self.__id
|
|
20
|
+
|
|
21
|
+
def __new__(cls, *posicionales,**nominales):
|
|
22
|
+
obj = super(Registro, cls).__new__(cls)
|
|
23
|
+
obj.__tabla = cls.__name__
|
|
24
|
+
return obj
|
|
25
|
+
|
|
26
|
+
@sobrecargar
|
|
27
|
+
def __init__(self, bdd : ProtocoloBaseDeDatos, valores : dict):
|
|
28
|
+
for atributo in self.__slots__:
|
|
29
|
+
nombre = atributoPublico(atributo)
|
|
30
|
+
valor_SQL : Any = valores.get(nombre,None)
|
|
31
|
+
if valor_SQL is not None:
|
|
32
|
+
valor = valor_SQL
|
|
33
|
+
tipo_esperado : type = self.__class__.__annotations__[atributo]
|
|
34
|
+
if issubclass(tipo_esperado, Decimal):
|
|
35
|
+
valor : Decimal = Decimal(valor_SQL)
|
|
36
|
+
elif issubclass(tipo_esperado, dict):
|
|
37
|
+
valor : dict = loads(valor_SQL)
|
|
38
|
+
elif issubclass(tipo_esperado,bool):
|
|
39
|
+
valor : bool = bool(valor_SQL)
|
|
40
|
+
elif issubclass(tipo_esperado,EnumSQL):
|
|
41
|
+
valor : tipo_esperado = tipo_esperado.desdeCadena(valor_SQL)
|
|
42
|
+
else:
|
|
43
|
+
valor = valor_SQL
|
|
44
|
+
setattr(self, atributoPrivado(self,atributo) if '__' in atributo else atributo, valor)
|
|
45
|
+
|
|
46
|
+
@sobrecargar
|
|
47
|
+
def __init__(self, bdd : ProtocoloBaseDeDatos, id : int):
|
|
48
|
+
resultado : Resultado
|
|
49
|
+
atributos : tuple[str] = (atributoPublico(atr) for atr in self.__slots__ if atr not in ('__bdd','__tabla'))
|
|
50
|
+
|
|
51
|
+
with bdd as bdd:
|
|
52
|
+
resultado = bdd\
|
|
53
|
+
.SELECT(self.tabla,atributos)\
|
|
54
|
+
.WHERE(id=id)\
|
|
55
|
+
.ejecutar()\
|
|
56
|
+
.devolverUnResultado()
|
|
57
|
+
|
|
58
|
+
self.__init__(
|
|
59
|
+
bdd,
|
|
60
|
+
resultado
|
|
61
|
+
)
|
|
62
|
+
self.__id = id
|
|
63
|
+
|
|
64
|
+
def guardar(self) -> int:
|
|
65
|
+
"""Guarda el registro en la tabla correspondiente.
|
|
66
|
+
Si tiene id, se edita un registro existente,
|
|
67
|
+
de lo contrario se agrega uno nuevo.
|
|
68
|
+
|
|
69
|
+
Devuelve:
|
|
70
|
+
:arg Id int:
|
|
71
|
+
El Id del registro.
|
|
72
|
+
|
|
73
|
+
Levanta:
|
|
74
|
+
:arg Exception: Propaga errores de la conexión con la BDD
|
|
75
|
+
:arg Exception: Levanta error si al editar la base con coinciden los id
|
|
76
|
+
"""
|
|
77
|
+
match self.__id:
|
|
78
|
+
case None:
|
|
79
|
+
self.__id : int = self.__crear()
|
|
80
|
+
case _:
|
|
81
|
+
self.__editar()
|
|
82
|
+
|
|
83
|
+
return self.__id
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def __crear(self) -> int:
|
|
87
|
+
"""Crea un nuevo registro en la tabla correspondiente"""
|
|
88
|
+
|
|
89
|
+
atributos : tuple[str] = (atr for atr in self.__slots__ if '__' not in atr)
|
|
90
|
+
ediciones : dict[str,Any] = {
|
|
91
|
+
atributo : getattr(self,atributo)
|
|
92
|
+
for atributo in atributos
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
with self.__bdd as bdd:
|
|
96
|
+
id : int = bdd\
|
|
97
|
+
.UPDATE(self.tabla,**ediciones)\
|
|
98
|
+
.WHERE(id=self.__id)\
|
|
99
|
+
.ejecutar()\
|
|
100
|
+
.devolverIdUltimaInsercion()
|
|
101
|
+
self.__id = id
|
|
102
|
+
|
|
103
|
+
return self.__id
|
|
104
|
+
|
|
105
|
+
def __editar(self) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Edita un registro ya existente, dado por el ID, en la tabla correspondiente.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
atributos : tuple[str] = (atr for atr in self.__slots__ if '__' not in atr)
|
|
112
|
+
ediciones : dict[str,Any] = {
|
|
113
|
+
atributo : getattr(self,atributo)
|
|
114
|
+
for atributo in atributos
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
with self.__bdd as bdd:
|
|
118
|
+
bdd\
|
|
119
|
+
.UPDATE(self.tabla,**ediciones)\
|
|
120
|
+
.WHERE(id=self.__id)\
|
|
121
|
+
.ejecutar()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from chastack_bdd.tipos import *
|
|
2
|
+
from chastack_bdd.utiles import *
|
|
3
|
+
from chastack_bdd.bdd import ProtocoloBaseDeDatos
|
|
4
|
+
from chastack_bdd.registro import Registro
|
|
5
|
+
|
|
6
|
+
class Tabla(type):
|
|
7
|
+
def __new__(mcs, nombre, bases, atributos):
|
|
8
|
+
if Registro not in bases and nombre != 'Registro':
|
|
9
|
+
bases = (Registro,) + bases
|
|
10
|
+
|
|
11
|
+
cls = super().__new__(mcs, nombre, bases, atributos)
|
|
12
|
+
|
|
13
|
+
cls.__tabla = nombre
|
|
14
|
+
setattr(cls, "tabla", property(lambda cls : cls.__tabla))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if not hasattr(cls, '__annotations__'):
|
|
18
|
+
cls.__annotations__ = {}
|
|
19
|
+
|
|
20
|
+
return cls
|
|
21
|
+
|
|
22
|
+
def __init__(cls, nombre, bases, atributos): ...
|
|
23
|
+
#print(cls.__slots__)
|
|
24
|
+
|
|
25
|
+
def __call__(cls, bdd: ProtocoloBaseDeDatos, *posicionales, **nominales):
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
slots :list[str] = []
|
|
29
|
+
anotaciones : dict[str,type] = {}
|
|
30
|
+
with bdd as bdd:
|
|
31
|
+
resultados = bdd.DESCRIBE(cls.__tabla).ejecutar().devolverResultados()
|
|
32
|
+
|
|
33
|
+
for columna in resultados:
|
|
34
|
+
nombre_campo = columna.get('Field')
|
|
35
|
+
es_clave = columna.get('Key') == "PRI"
|
|
36
|
+
#print(columna.get('Extra'))
|
|
37
|
+
es_auto = "auto_increment" in columna.get("Extra", "").lower() or "default_generated" in columna.get("Extra", "").lower() or "auto_generated" in columna.get("Extra", "").lower()
|
|
38
|
+
|
|
39
|
+
nombre_attr = f"__{nombre_campo}" if es_clave or es_auto else nombre_campo
|
|
40
|
+
|
|
41
|
+
tipo = cls.__resolverTipo(columna.get('Type'), nombre_campo)
|
|
42
|
+
|
|
43
|
+
if nombre_attr not in cls.__slots__:
|
|
44
|
+
slots.append(nombre_attr)
|
|
45
|
+
anotaciones.update({
|
|
46
|
+
nombre_attr : tipo
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if es_clave:
|
|
51
|
+
setattr(cls, nombre_campo, property(lambda self, name=nombre_campo: getattr(self, atributoPrivado(self,name))))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
cls.__slots__ = cls.__slots__ + tuple(slots)
|
|
55
|
+
cls.__annotations__.update(anotaciones)
|
|
56
|
+
print(cls.__slots__)
|
|
57
|
+
instancia = super().__call__(bdd, *posicionales, **nominales)
|
|
58
|
+
setattr(instancia, atributoPrivado(instancia,"__bdd"),bdd)
|
|
59
|
+
return instancia
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def __resolverTipo(cls, tipo_sql: str, nombre_columna: Optional[str]) -> type:
|
|
63
|
+
"""
|
|
64
|
+
Deduce y devuelve un tipo de Python en base al tipo declarado en MySQL para la columna.
|
|
65
|
+
Si encuentra un ENUM, crea un enum de Python y lo guarda como una constante de la clase.
|
|
66
|
+
|
|
67
|
+
Parámetros:
|
|
68
|
+
:arg tipo_sql str: El tipo definido en MySQL
|
|
69
|
+
:arg nombre_columna Optional[str]: El nombre de la columna (útil para enums)
|
|
70
|
+
|
|
71
|
+
Devuelve:
|
|
72
|
+
:arg tipo `type`: el tipo python correspondiente (o `Any`)
|
|
73
|
+
"""
|
|
74
|
+
tipo_declarado: Optional[Match[AnyStr]] = match(r'([a-z]+)(\(.*\))?', tipo_sql.lower())
|
|
75
|
+
if not tipo_declarado:
|
|
76
|
+
return Any
|
|
77
|
+
|
|
78
|
+
tipo_base: str = tipo_declarado.group(1)
|
|
79
|
+
parametros: str = tipo_declarado.group(2) if tipo_declarado.group(2) else ""
|
|
80
|
+
tipo_completo: str = tipo_base + parametros
|
|
81
|
+
|
|
82
|
+
tipos: dict[str, type] = {
|
|
83
|
+
'tinyint': int,
|
|
84
|
+
'smallint': int,
|
|
85
|
+
'mediumint': int,
|
|
86
|
+
'int': int,
|
|
87
|
+
'bigint': int,
|
|
88
|
+
'float': float,
|
|
89
|
+
'double': float,
|
|
90
|
+
'decimal': Decimal,
|
|
91
|
+
'datetime': datetime,
|
|
92
|
+
'timestamp': datetime,
|
|
93
|
+
'date': date,
|
|
94
|
+
'time': time,
|
|
95
|
+
'char': str,
|
|
96
|
+
'varchar': str,
|
|
97
|
+
'text': str,
|
|
98
|
+
'mediumtext': str,
|
|
99
|
+
'longtext': str,
|
|
100
|
+
'tinytext': str,
|
|
101
|
+
'boolean': bool,
|
|
102
|
+
'bool': bool,
|
|
103
|
+
'tinyint(1)': bool,
|
|
104
|
+
'blob': bytearray,
|
|
105
|
+
'mediumblob': bytearray,
|
|
106
|
+
'longblob': bytearray,
|
|
107
|
+
'tinyblob': bytearray,
|
|
108
|
+
'binary': bytearray,
|
|
109
|
+
'varbinary': bytearray,
|
|
110
|
+
'json': dict,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
if tipo_base == 'enum':
|
|
115
|
+
valores_enum: list[Any] = findall(r"'([^']*)'", tipo_sql)
|
|
116
|
+
dicc_enum: dict[str, int] = {'_invalido': 0}
|
|
117
|
+
for i, val in enumerate(valores_enum, 1):
|
|
118
|
+
dicc_enum[val] = i
|
|
119
|
+
|
|
120
|
+
nombre_enum: str = f"Tipo{nombre_columna.capitalize()}" if nombre_columna else f"__ENUM_{token_urlsafe(4)}"
|
|
121
|
+
clase_enum: type = type(
|
|
122
|
+
nombre_enum,
|
|
123
|
+
(EnumSQL, Enum),
|
|
124
|
+
dicc_enum
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
setattr(cls,nombre_enum,clase_enum)
|
|
128
|
+
return clase_enum
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if tipo_completo in tipos:
|
|
132
|
+
return tipos[tipo_completo]
|
|
133
|
+
return tipos.get(tipo_base, Any)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Protocol, runtime_checkable, Self, TypeAlias, Optional, Any, AnyStr, Unpack,Union,Dict,List, get_origin
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from datetime import datetime,date,time,timedelta,timezone
|
|
4
|
+
from re import Match
|
|
5
|
+
|
|
6
|
+
from chastack_bdd.tipos.enum_sql import *
|
|
7
|
+
from solteron import Solteron
|
|
8
|
+
### BDD
|
|
9
|
+
Resultado : TypeAlias = dict[str,Any]
|
|
10
|
+
|
|
11
|
+
class TipoCondicion:
|
|
12
|
+
IGUAL = '='
|
|
13
|
+
DIFERENTE = '!='
|
|
14
|
+
MAYOR = '>'
|
|
15
|
+
MENOR = '<'
|
|
16
|
+
MAYOR_O_IGUAL = '>='
|
|
17
|
+
MENOR_O_IGUAL = '<='
|
|
18
|
+
NO_ES = 'IS NOT'
|
|
19
|
+
|
|
20
|
+
class TipoUnion:
|
|
21
|
+
INNER = 'INNER'
|
|
22
|
+
LEFT = 'LEFT'
|
|
23
|
+
RIGHT = 'RIGHT'
|
|
24
|
+
FULL = 'FULL'
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from enum import Enum, EnumType as _EnumMeta, EnumDict as _EnumDict
|
|
2
|
+
|
|
3
|
+
class EnumSQLMeta(_EnumMeta):
|
|
4
|
+
"""
|
|
5
|
+
Metaclase para controlar las reglas personalizadas sobre las enumeraciones.
|
|
6
|
+
Asegura que `_invalido` siempre tenga el valor 0 y ningún otro miembro lo tenga.
|
|
7
|
+
"""
|
|
8
|
+
@classmethod
|
|
9
|
+
def __prepare__(metacls, cls, bases, **kwds):
|
|
10
|
+
"""Copiado de enum.EnumType con la modificación de que esta metaclase no inhibe la herencia"""
|
|
11
|
+
#metacls._check_for_existing_members_(cls, bases)
|
|
12
|
+
enum_dict = _EnumDict()
|
|
13
|
+
enum_dict._cls_name = cls
|
|
14
|
+
member_type, first_enum = metacls._get_mixins_(cls, bases)
|
|
15
|
+
if first_enum is not None:
|
|
16
|
+
enum_dict['_generate_next_value_'] = getattr(
|
|
17
|
+
first_enum, '_generate_next_value_', None,
|
|
18
|
+
)
|
|
19
|
+
return enum_dict
|
|
20
|
+
def __new__(cls, nombre, bases, diccionario):
|
|
21
|
+
diccionario_enum = EnumSQLMeta.__prepare__(nombre,bases,**diccionario)
|
|
22
|
+
diccionario_enum.update(diccionario)
|
|
23
|
+
nueva_clase = _EnumMeta.__new__(cls, nombre, bases, diccionario_enum)
|
|
24
|
+
|
|
25
|
+
# Verificar que la clase tiene '_invalido' con valor 0
|
|
26
|
+
if '_invalido' not in nueva_clase.__members__: ...
|
|
27
|
+
#raise ValueError(f"La enumeración {nombre} debe tener un miembro '_invalido' con valor 0.")
|
|
28
|
+
|
|
29
|
+
# Verificar que ningún otro miembro tenga valor 0.
|
|
30
|
+
for miembro in nueva_clase.__members__.values():
|
|
31
|
+
if miembro.value == 0 and miembro.name != '_invalido':
|
|
32
|
+
raise ValueError(f"{nombre} no puede tener otro miembro con valor 0 (reservado para '_invalido').")
|
|
33
|
+
|
|
34
|
+
return nueva_clase
|
|
35
|
+
|
|
36
|
+
class EnumSQL(Enum,metaclass=EnumSQLMeta):
|
|
37
|
+
"""
|
|
38
|
+
Clase base para enumeraciones que se lleva bien con SQL.
|
|
39
|
+
"""
|
|
40
|
+
_invalido = 0 # Siempre debe ser 0 y no debe haber otro miembro con este valor.
|
|
41
|
+
|
|
42
|
+
def __init__(self, *args, **kwds):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def desdeCadena(cls, cadena: str) -> 'EnumSQL':
|
|
47
|
+
return cls.__members__.get(cadena, cls._invalido)
|
|
48
|
+
|
|
49
|
+
def haciaCadena(self) -> str:
|
|
50
|
+
return self.name
|
|
51
|
+
|
|
52
|
+
def __str__(self) -> str:
|
|
53
|
+
return self.haciaCadena()
|
|
54
|
+
|
|
55
|
+
def __repr__(self) -> str:
|
|
56
|
+
return self.haciaCadena()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from json import dumps,loads
|
|
2
|
+
from chastack_bdd.tipos import *
|
|
3
|
+
from solteron import Solteron
|
|
4
|
+
from sobrecargar import sobrecargar
|
|
5
|
+
from secrets import token_urlsafe
|
|
6
|
+
from re import findall,match
|
|
7
|
+
|
|
8
|
+
def formatearValorParaSQL(valor: Any, html : bool = False) -> str:
|
|
9
|
+
"""
|
|
10
|
+
Formatea un valor de Python a una representación adecuada para SQL.
|
|
11
|
+
"""
|
|
12
|
+
if valor is None:
|
|
13
|
+
return "NULL"
|
|
14
|
+
if isinstance(valor, bool):
|
|
15
|
+
return "1" if valor else "0"
|
|
16
|
+
if isinstance(valor, (int, float)):
|
|
17
|
+
return str(valor)
|
|
18
|
+
if isinstance(valor, (list, tuple)):
|
|
19
|
+
return f"'[{','.join(f"\"{str(v)}\"" for v in valor)}]'"
|
|
20
|
+
if isinstance(valor, Decimal):
|
|
21
|
+
return str(valor.to_eng_string())
|
|
22
|
+
if isinstance(valor, (date, datetime, time)):
|
|
23
|
+
return f"'{valor.isoformat()}'"
|
|
24
|
+
if isinstance(valor, dict):
|
|
25
|
+
return f"'{dumps(valor)}'"
|
|
26
|
+
if isinstance(valor, bytes):
|
|
27
|
+
return f"X'{valor.hex()}'"
|
|
28
|
+
if isinstance(valor, Enum):
|
|
29
|
+
return str(valor.value) if isinstance(valor.value, int) else f"'{valor.name}'"
|
|
30
|
+
if isinstance(valor, str):
|
|
31
|
+
return f"'{valor.replace("'", "''")}'"
|
|
32
|
+
|
|
33
|
+
return f"'{str(valor).replace("'", "''")}'"
|
|
34
|
+
|
|
35
|
+
def atributoPublico(nombreAtributo: str) -> str:
|
|
36
|
+
return nombreAtributo.replace('__','',1)
|
|
37
|
+
|
|
38
|
+
def atributoPrivado(obj: Any, nombreAtributo: str) -> str:
|
|
39
|
+
return f"_{obj.__class__.__name__}__{atributoPublico(nombreAtributo)}"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: chastack-bdd
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Abstracción de Bases de Datos en Python y constructor de modelos.
|
|
5
|
+
Home-page: https://github.com/Hernanatn/bdd.py
|
|
6
|
+
Author: Hernán A. Teszkiewicz Novick
|
|
7
|
+
Author-email: "Hernán A.T.N." <herni@cajadeideas.ar>, Joaquín Correa <correajoaquin7@gmail.com>, "Camila M.T.N." <cmteszkiewicz@gmail.com>
|
|
8
|
+
Project-URL: Homepage, https://github.com/Hernanatn/bdd.py
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: sobrecargar
|
|
16
|
+
Requires-Dist: solteron
|
|
17
|
+
Requires-Dist: mysql-connector-python
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# BDD
|
|
24
|
+
|
|
25
|
+
[](https://cajadeideas.ar)
|
|
26
|
+
[](https://github.com/hernanatn/github.com/hernanatn/bdd.py/releases/latest)
|
|
27
|
+
[](https://www.python.org/downloads/release/python-3120/)
|
|
28
|
+
[](LICENSE)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Descripción
|
|
32
|
+
Módulo de Python 3.x con abstracciones útiles para gestión de Bases de Datos y construcción de modelos.
|
|
33
|
+
|
|
34
|
+
## [Documentación](/docs)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
chastack_bdd/__init__.py
|
|
6
|
+
chastack_bdd/bdd.py
|
|
7
|
+
chastack_bdd/pruebas.py
|
|
8
|
+
chastack_bdd/registro.py
|
|
9
|
+
chastack_bdd/tabla.py
|
|
10
|
+
chastack_bdd/utiles.py
|
|
11
|
+
chastack_bdd.egg-info/PKG-INFO
|
|
12
|
+
chastack_bdd.egg-info/SOURCES.txt
|
|
13
|
+
chastack_bdd.egg-info/dependency_links.txt
|
|
14
|
+
chastack_bdd.egg-info/requires.txt
|
|
15
|
+
chastack_bdd.egg-info/top_level.txt
|
|
16
|
+
chastack_bdd/errores/__init__.py
|
|
17
|
+
chastack_bdd/tipos/__init__.py
|
|
18
|
+
chastack_bdd/tipos/enum_sql.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
chastack_bdd
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "chastack-bdd"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Hernán A.T.N.", email="herni@cajadeideas.ar" },
|
|
10
|
+
{ name="Joaquín Correa", email="correajoaquin7@gmail.com" },
|
|
11
|
+
{ name="Camila M.T.N.", email="cmteszkiewicz@gmail.com" },
|
|
12
|
+
]
|
|
13
|
+
description = "Abstracción de Bases de Datos en Python y constructor de modelos."
|
|
14
|
+
readme = "README.md"
|
|
15
|
+
requires-python = ">=3.11"
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
dependencies = [
|
|
23
|
+
'sobrecargar',
|
|
24
|
+
'solteron',
|
|
25
|
+
"mysql-connector-python",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools]
|
|
29
|
+
packages = [
|
|
30
|
+
"chastack_bdd",
|
|
31
|
+
"chastack_bdd.errores",
|
|
32
|
+
"chastack_bdd.tipos",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
"Homepage" = "https://github.com/Hernanatn/bdd.py"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name='chastack-bdd',
|
|
5
|
+
description="setuptools.build_meta",
|
|
6
|
+
version='0.1.1',
|
|
7
|
+
author = 'Hernán A. Teszkiewicz Novick',
|
|
8
|
+
author_email = 'herni@cajadeideas.ar',
|
|
9
|
+
url= 'https://github.com/Hernanatn/bdd.py',
|
|
10
|
+
packages=['bdd', 'bdd.errores','bdd.tipos'],
|
|
11
|
+
)
|