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.
@@ -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
+ [![Hecho por Chaska](https://img.shields.io/badge/hecho_por-Ch'aska-303030.svg)](https://cajadeideas.ar)
26
+ [![Versión: 0.1.0](https://img.shields.io/badge/version-v0.1.0-green.svg)](https://github.com/hernanatn/github.com/hernanatn/bdd.py/releases/latest)
27
+ [![Verisón de Python: 3.12](https://img.shields.io/badge/Python-3.12-blue?logo=python)](https://www.python.org/downloads/release/python-3120/)
28
+ [![Licencia: MIT](https://img.shields.io/badge/Licencia-MIT-lightgrey.svg)](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
+ [![Hecho por Chaska](https://img.shields.io/badge/hecho_por-Ch'aska-303030.svg)](https://cajadeideas.ar)
6
+ [![Versión: 0.1.0](https://img.shields.io/badge/version-v0.1.0-green.svg)](https://github.com/hernanatn/github.com/hernanatn/bdd.py/releases/latest)
7
+ [![Verisón de Python: 3.12](https://img.shields.io/badge/Python-3.12-blue?logo=python)](https://www.python.org/downloads/release/python-3120/)
8
+ [![Licencia: MIT](https://img.shields.io/badge/Licencia-MIT-lightgrey.svg)](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
+ [![Hecho por Chaska](https://img.shields.io/badge/hecho_por-Ch'aska-303030.svg)](https://cajadeideas.ar)
26
+ [![Versión: 0.1.0](https://img.shields.io/badge/version-v0.1.0-green.svg)](https://github.com/hernanatn/github.com/hernanatn/bdd.py/releases/latest)
27
+ [![Verisón de Python: 3.12](https://img.shields.io/badge/Python-3.12-blue?logo=python)](https://www.python.org/downloads/release/python-3120/)
28
+ [![Licencia: MIT](https://img.shields.io/badge/Licencia-MIT-lightgrey.svg)](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,3 @@
1
+ sobrecargar
2
+ solteron
3
+ mysql-connector-python
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )