eco-back 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.
@@ -0,0 +1,198 @@
1
+ """
2
+ Gestión de conexiones a base de datos
3
+ """
4
+ from typing import Optional, Any, Dict, List, Union
5
+ from contextlib import contextmanager
6
+ import logging
7
+
8
+ try:
9
+ import psycopg2
10
+ from psycopg2 import sql
11
+ from psycopg2.extras import RealDictCursor
12
+ PSYCOPG2_AVAILABLE = True
13
+ except ImportError:
14
+ PSYCOPG2_AVAILABLE = False
15
+
16
+ from .config import DatabaseConfig
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class DatabaseConnection:
22
+ """
23
+ Clase para gestionar conexiones a base de datos
24
+
25
+ Ejemplo:
26
+ >>> config = DatabaseConfig(
27
+ ... host="localhost",
28
+ ... port=3306,
29
+ ... database="mi_db",
30
+ ... user="usuario",
31
+ ... password="password"
32
+ ... )
33
+ >>> db = DatabaseConnection(config)
34
+ >>> db.connect()
35
+ >>> resultado = db.execute_query("SELECT * FROM tabla")
36
+ >>> db.close()
37
+
38
+ O usando context manager:
39
+ >>> with DatabaseConnection(config) as db:
40
+ ... resultado = db.execute_query("SELECT * FROM tabla")
41
+ """
42
+
43
+ def __init__(self, config: DatabaseConfig):
44
+ """
45
+ Inicializa la conexión a la base de datos
46
+
47
+ Args:
48
+ config: Configuración de la base de datos
49
+ """
50
+ self.config = config
51
+ self.connection: Optional[Any] = None
52
+ self.cursor: Optional[Any] = None
53
+
54
+ def connect(self) -> None:
55
+ """
56
+ Establece la conexión a PostgreSQL
57
+
58
+ Raises:
59
+ ImportError: Si psycopg2 no está instalado
60
+ Exception: Si no se puede establecer la conexión
61
+ """
62
+ if not PSYCOPG2_AVAILABLE:
63
+ raise ImportError(
64
+ "psycopg2 no está instalado. Instálalo con: pip install psycopg2-binary"
65
+ )
66
+
67
+ try:
68
+ self.connection = psycopg2.connect(
69
+ host=self.config.host,
70
+ port=self.config.port,
71
+ dbname=self.config.database,
72
+ user=self.config.user,
73
+ password=self.config.password,
74
+ connect_timeout=self.config.connect_timeout,
75
+ options=f"-c client_encoding={self.config.charset}"
76
+ )
77
+ self.connection.autocommit = False
78
+
79
+ logger.info(f"Conectado a PostgreSQL: {self.config.database}")
80
+ except Exception as e:
81
+ logger.error(f"Error al conectar a PostgreSQL: {e}")
82
+ raise
83
+
84
+ def close(self) -> None:
85
+ """
86
+ Cierra la conexión a la base de datos
87
+ """
88
+ if self.cursor:
89
+ self.cursor.close()
90
+ self.cursor = None
91
+
92
+ if self.connection:
93
+ self.connection.close()
94
+ self.connection = None
95
+ logger.info("Conexión cerrada")
96
+
97
+ def execute_query(self, query: str, params: Optional[Union[tuple, dict]] = None) -> List[Dict[str, Any]]:
98
+ """
99
+ Ejecuta una consulta SELECT y retorna los resultados
100
+
101
+ Args:
102
+ query: Consulta SQL a ejecutar
103
+ params: Parámetros para la consulta (opcional)
104
+
105
+ Returns:
106
+ Lista de diccionarios con los resultados
107
+
108
+ Raises:
109
+ Exception: Si hay un error en la consulta
110
+ """
111
+ if not self.connection:
112
+ raise Exception("No hay conexión a la base de datos")
113
+
114
+ try:
115
+ cursor = self.connection.cursor(cursor_factory=RealDictCursor)
116
+
117
+ if params:
118
+ cursor.execute(query, params)
119
+ else:
120
+ cursor.execute(query)
121
+
122
+ results = cursor.fetchall()
123
+ # Convertir RealDictRow a dict normal
124
+ results = [dict(row) for row in results]
125
+ cursor.close()
126
+
127
+ logger.debug(f"Consulta ejecutada: {query}")
128
+ return results
129
+ except Exception as e:
130
+ logger.error(f"Error al ejecutar consulta: {e}")
131
+ raise
132
+
133
+ def execute_update(self, query: str, params: Optional[Union[tuple, dict]] = None) -> int:
134
+ """
135
+ Ejecuta una consulta INSERT, UPDATE o DELETE
136
+
137
+ Args:
138
+ query: Consulta SQL a ejecutar
139
+ params: Parámetros para la consulta (opcional)
140
+
141
+ Returns:
142
+ Número de filas afectadas
143
+
144
+ Raises:
145
+ Exception: Si hay un error en la consulta
146
+ """
147
+ if not self.connection:
148
+ raise Exception("No hay conexión a la base de datos")
149
+
150
+ try:
151
+ cursor = self.connection.cursor()
152
+
153
+ if params:
154
+ cursor.execute(query, params)
155
+ else:
156
+ cursor.execute(query)
157
+
158
+ self.connection.commit()
159
+ affected_rows = cursor.rowcount
160
+ cursor.close()
161
+
162
+ logger.debug(f"Actualización ejecutada: {query}, filas afectadas: {affected_rows}")
163
+ return affected_rows
164
+ except Exception as e:
165
+ self.connection.rollback()
166
+ logger.error(f"Error al ejecutar actualización: {e}")
167
+ raise
168
+
169
+ @contextmanager
170
+ def transaction(self):
171
+ """
172
+ Context manager para transacciones
173
+
174
+ Ejemplo:
175
+ >>> with db.transaction():
176
+ ... db.execute_update("INSERT INTO tabla VALUES (...)")
177
+ ... db.execute_update("UPDATE tabla SET ...")
178
+ """
179
+ try:
180
+ yield self
181
+ if self.connection:
182
+ self.connection.commit()
183
+ logger.debug("Transacción confirmada")
184
+ except Exception as e:
185
+ if self.connection:
186
+ self.connection.rollback()
187
+ logger.error(f"Transacción revertida: {e}")
188
+ raise
189
+
190
+ def __enter__(self):
191
+ """Context manager entry"""
192
+ self.connect()
193
+ return self
194
+
195
+ def __exit__(self, exc_type, exc_val, exc_tb):
196
+ """Context manager exit"""
197
+ self.close()
198
+ return False
@@ -0,0 +1,49 @@
1
+ """
2
+ Modelos base para la base de datos
3
+ """
4
+ from abc import ABC, abstractmethod
5
+ from typing import Dict, Any, Optional, List
6
+
7
+
8
+ class BaseModel(ABC):
9
+ """
10
+ Clase base para modelos de base de datos
11
+ """
12
+
13
+ @abstractmethod
14
+ def to_dict(self) -> Dict[str, Any]:
15
+ """
16
+ Convierte el modelo a diccionario
17
+
18
+ Returns:
19
+ Diccionario con los datos del modelo
20
+ """
21
+ pass
22
+
23
+ @classmethod
24
+ @abstractmethod
25
+ def from_dict(cls, data: Dict[str, Any]) -> "BaseModel":
26
+ """
27
+ Crea una instancia del modelo desde un diccionario
28
+
29
+ Args:
30
+ data: Diccionario con los datos
31
+
32
+ Returns:
33
+ Instancia del modelo
34
+ """
35
+ pass
36
+
37
+ @classmethod
38
+ @abstractmethod
39
+ def from_db_row(cls, row: Dict[str, Any]) -> "BaseModel":
40
+ """
41
+ Crea una instancia del modelo desde una fila de la base de datos
42
+
43
+ Args:
44
+ row: Diccionario con los datos de la fila
45
+
46
+ Returns:
47
+ Instancia del modelo
48
+ """
49
+ pass
@@ -0,0 +1,350 @@
1
+ """
2
+ Utilidades para trabajar con PostGIS en PostgreSQL
3
+ """
4
+ from typing import Optional, Dict, Any, List, Tuple, Union
5
+ import logging
6
+
7
+ from .connection import DatabaseConnection
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ try:
12
+ from shapely.geometry import Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon
13
+ from shapely import wkt, wkb
14
+ SHAPELY_AVAILABLE = True
15
+ except ImportError:
16
+ SHAPELY_AVAILABLE = False
17
+
18
+
19
+ class PostGISHelper:
20
+ """
21
+ Clase helper para operaciones con PostGIS
22
+ """
23
+
24
+ def __init__(self, db_connection: DatabaseConnection):
25
+ """
26
+ Inicializa el helper de PostGIS
27
+
28
+ Args:
29
+ db_connection: Conexión a la base de datos PostgreSQL
30
+ """
31
+ self.db = db_connection
32
+
33
+ def enable_postgis(self) -> bool:
34
+ """
35
+ Habilita la extensión PostGIS en la base de datos
36
+
37
+ Returns:
38
+ True si se habilitó correctamente
39
+
40
+ Raises:
41
+ Exception: Si hay un error al habilitar PostGIS
42
+ """
43
+ try:
44
+ self.db.execute_update("CREATE EXTENSION IF NOT EXISTS postgis")
45
+ logger.info("Extensión PostGIS habilitada")
46
+ return True
47
+ except Exception as e:
48
+ logger.error(f"Error al habilitar PostGIS: {e}")
49
+ raise
50
+
51
+ def get_postgis_version(self) -> str:
52
+ """
53
+ Obtiene la versión de PostGIS instalada
54
+
55
+ Returns:
56
+ Versión de PostGIS
57
+ """
58
+ result = self.db.execute_query("SELECT PostGIS_Version()")
59
+ return result[0]['postgis_version'] if result else "No disponible"
60
+
61
+ def create_spatial_index(self, table_name: str, column_name: str = "geom") -> bool:
62
+ """
63
+ Crea un índice espacial en una columna de geometría
64
+
65
+ Args:
66
+ table_name: Nombre de la tabla
67
+ column_name: Nombre de la columna de geometría
68
+
69
+ Returns:
70
+ True si se creó correctamente
71
+ """
72
+ try:
73
+ index_name = f"idx_{table_name}_{column_name}"
74
+ query = f"""
75
+ CREATE INDEX IF NOT EXISTS {index_name}
76
+ ON {table_name} USING GIST ({column_name})
77
+ """
78
+ self.db.execute_update(query)
79
+ logger.info(f"Índice espacial creado: {index_name}")
80
+ return True
81
+ except Exception as e:
82
+ logger.error(f"Error al crear índice espacial: {e}")
83
+ raise
84
+
85
+ def point_to_geometry(self, lat: float, lon: float, srid: int = 4326) -> str:
86
+ """
87
+ Convierte coordenadas lat/lon a geometría PostGIS
88
+
89
+ Args:
90
+ lat: Latitud
91
+ lon: Longitud
92
+ srid: Sistema de referencia espacial (default: 4326 - WGS84)
93
+
94
+ Returns:
95
+ String de geometría PostGIS
96
+ """
97
+ return f"ST_SetSRID(ST_MakePoint({lon}, {lat}), {srid})"
98
+
99
+ def insert_point(
100
+ self,
101
+ table_name: str,
102
+ lat: float,
103
+ lon: float,
104
+ data: Optional[Dict[str, Any]] = None,
105
+ geom_column: str = "geom",
106
+ srid: int = 4326
107
+ ) -> int:
108
+ """
109
+ Inserta un punto geográfico en una tabla
110
+
111
+ Args:
112
+ table_name: Nombre de la tabla
113
+ lat: Latitud
114
+ lon: Longitud
115
+ data: Datos adicionales a insertar
116
+ geom_column: Nombre de la columna de geometría
117
+ srid: Sistema de referencia espacial
118
+
119
+ Returns:
120
+ ID del registro insertado
121
+ """
122
+ data = data or {}
123
+ columns = list(data.keys()) + [geom_column]
124
+ placeholders = [f"%s" for _ in data] + [f"ST_SetSRID(ST_MakePoint(%s, %s), {srid})"]
125
+ values = list(data.values()) + [lon, lat]
126
+
127
+ query = f"""
128
+ INSERT INTO {table_name} ({', '.join(columns)})
129
+ VALUES ({', '.join(placeholders)})
130
+ RETURNING id
131
+ """
132
+
133
+ result = self.db.execute_query(query, tuple(values))
134
+ return result[0]['id'] if result else None
135
+
136
+ def find_within_distance(
137
+ self,
138
+ table_name: str,
139
+ lat: float,
140
+ lon: float,
141
+ distance_meters: float,
142
+ geom_column: str = "geom",
143
+ srid: int = 4326
144
+ ) -> List[Dict[str, Any]]:
145
+ """
146
+ Busca puntos dentro de una distancia específica
147
+
148
+ Args:
149
+ table_name: Nombre de la tabla
150
+ lat: Latitud del punto central
151
+ lon: Longitud del punto central
152
+ distance_meters: Distancia en metros
153
+ geom_column: Nombre de la columna de geometría
154
+ srid: Sistema de referencia espacial
155
+
156
+ Returns:
157
+ Lista de registros encontrados
158
+ """
159
+ query = f"""
160
+ SELECT *,
161
+ ST_Distance(
162
+ {geom_column}::geography,
163
+ ST_SetSRID(ST_MakePoint(%s, %s), {srid})::geography
164
+ ) as distance
165
+ FROM {table_name}
166
+ WHERE ST_DWithin(
167
+ {geom_column}::geography,
168
+ ST_SetSRID(ST_MakePoint(%s, %s), {srid})::geography,
169
+ %s
170
+ )
171
+ ORDER BY distance
172
+ """
173
+
174
+ return self.db.execute_query(
175
+ query,
176
+ (lon, lat, lon, lat, distance_meters)
177
+ )
178
+
179
+ def find_within_bbox(
180
+ self,
181
+ table_name: str,
182
+ min_lat: float,
183
+ min_lon: float,
184
+ max_lat: float,
185
+ max_lon: float,
186
+ geom_column: str = "geom",
187
+ srid: int = 4326
188
+ ) -> List[Dict[str, Any]]:
189
+ """
190
+ Busca puntos dentro de un bounding box
191
+
192
+ Args:
193
+ table_name: Nombre de la tabla
194
+ min_lat: Latitud mínima
195
+ min_lon: Longitud mínima
196
+ max_lat: Latitud máxima
197
+ max_lon: Longitud máxima
198
+ geom_column: Nombre de la columna de geometría
199
+ srid: Sistema de referencia espacial
200
+
201
+ Returns:
202
+ Lista de registros encontrados
203
+ """
204
+ query = f"""
205
+ SELECT *
206
+ FROM {table_name}
207
+ WHERE {geom_column} && ST_MakeEnvelope(%s, %s, %s, %s, {srid})
208
+ """
209
+
210
+ return self.db.execute_query(
211
+ query,
212
+ (min_lon, min_lat, max_lon, max_lat)
213
+ )
214
+
215
+ def get_coordinates(
216
+ self,
217
+ table_name: str,
218
+ id: int,
219
+ geom_column: str = "geom"
220
+ ) -> Optional[Tuple[float, float]]:
221
+ """
222
+ Obtiene las coordenadas lat/lon de un registro
223
+
224
+ Args:
225
+ table_name: Nombre de la tabla
226
+ id: ID del registro
227
+ geom_column: Nombre de la columna de geometría
228
+
229
+ Returns:
230
+ Tupla (latitud, longitud) o None
231
+ """
232
+ query = f"""
233
+ SELECT ST_Y({geom_column}) as lat, ST_X({geom_column}) as lon
234
+ FROM {table_name}
235
+ WHERE id = %s
236
+ """
237
+
238
+ result = self.db.execute_query(query, (id,))
239
+ if result:
240
+ return (result[0]['lat'], result[0]['lon'])
241
+ return None
242
+
243
+ def calculate_distance(
244
+ self,
245
+ lat1: float,
246
+ lon1: float,
247
+ lat2: float,
248
+ lon2: float,
249
+ srid: int = 4326
250
+ ) -> float:
251
+ """
252
+ Calcula la distancia entre dos puntos en metros
253
+
254
+ Args:
255
+ lat1: Latitud del punto 1
256
+ lon1: Longitud del punto 1
257
+ lat2: Latitud del punto 2
258
+ lon2: Longitud del punto 2
259
+ srid: Sistema de referencia espacial
260
+
261
+ Returns:
262
+ Distancia en metros
263
+ """
264
+ query = """
265
+ SELECT ST_Distance(
266
+ ST_SetSRID(ST_MakePoint(%s, %s), %s)::geography,
267
+ ST_SetSRID(ST_MakePoint(%s, %s), %s)::geography
268
+ ) as distance
269
+ """
270
+
271
+ result = self.db.execute_query(
272
+ query,
273
+ (lon1, lat1, srid, lon2, lat2, srid)
274
+ )
275
+
276
+ return result[0]['distance'] if result else 0.0
277
+
278
+ def insert_polygon(
279
+ self,
280
+ table_name: str,
281
+ coordinates: List[Tuple[float, float]],
282
+ data: Optional[Dict[str, Any]] = None,
283
+ geom_column: str = "geom",
284
+ srid: int = 4326
285
+ ) -> int:
286
+ """
287
+ Inserta un polígono en la base de datos
288
+
289
+ Args:
290
+ table_name: Nombre de la tabla
291
+ coordinates: Lista de tuplas (lat, lon) que forman el polígono
292
+ data: Datos adicionales a insertar
293
+ geom_column: Nombre de la columna de geometría
294
+ srid: Sistema de referencia espacial
295
+
296
+ Returns:
297
+ ID del registro insertado
298
+ """
299
+ data = data or {}
300
+
301
+ # Convertir coordenadas a formato WKT
302
+ points = ", ".join([f"{lon} {lat}" for lat, lon in coordinates])
303
+ # Cerrar el polígono si no está cerrado
304
+ if coordinates[0] != coordinates[-1]:
305
+ points += f", {coordinates[0][1]} {coordinates[0][0]}"
306
+
307
+ columns = list(data.keys()) + [geom_column]
308
+ placeholders = ["%s" for _ in data] + [f"ST_GeomFromText('POLYGON(({points}))', {srid})"]
309
+ values = list(data.values())
310
+
311
+ query = f"""
312
+ INSERT INTO {table_name} ({', '.join(columns)})
313
+ VALUES ({', '.join(placeholders)})
314
+ RETURNING id
315
+ """
316
+
317
+ result = self.db.execute_query(query, tuple(values) if values else None)
318
+ return result[0]['id'] if result else None
319
+
320
+ def point_in_polygon(
321
+ self,
322
+ table_name: str,
323
+ lat: float,
324
+ lon: float,
325
+ geom_column: str = "geom",
326
+ srid: int = 4326
327
+ ) -> List[Dict[str, Any]]:
328
+ """
329
+ Encuentra polígonos que contienen un punto específico
330
+
331
+ Args:
332
+ table_name: Nombre de la tabla con polígonos
333
+ lat: Latitud del punto
334
+ lon: Longitud del punto
335
+ geom_column: Nombre de la columna de geometría
336
+ srid: Sistema de referencia espacial
337
+
338
+ Returns:
339
+ Lista de polígonos que contienen el punto
340
+ """
341
+ query = f"""
342
+ SELECT *
343
+ FROM {table_name}
344
+ WHERE ST_Contains(
345
+ {geom_column},
346
+ ST_SetSRID(ST_MakePoint(%s, %s), {srid})
347
+ )
348
+ """
349
+
350
+ return self.db.execute_query(query, (lon, lat))