apb_extra_utils.postgres_pckg.psql_alchemy
1# coding=utf-8 2# 3# Author: Ernesto Arredondo Martinez (ernestone@gmail.com) 4# Created: 31/03/2019 5# Copyright (c) 6from __future__ import annotations 7import re 8from threading import Lock 9 10from apb_extra_utils.utils_logging import get_base_logger 11from sqlalchemy.orm import sessionmaker 12from sqlalchemy.engine import create_engine 13from sqlalchemy.engine.url import make_url, URL 14from sqlalchemy import MetaData, Table, text, Engine, inspect 15from collections import namedtuple 16 17TYPE_UNKNOWN = 'UNKNOWN' 18PG_DRIVER = 'postgresql+psycopg2' 19 20# Intenta importar soporte para tipos geométricos PostGIS 21try: 22 import geoalchemy2 # noqa: F401 23 24 HAS_GEOALCHEMY2 = True 25except ImportError: 26 HAS_GEOALCHEMY2 = False 27 28 29def url_pg_string_connection(user='', psw='', host='localhost', port=5432, db='postgres', url_conn_string=None, 30 schemas='') -> URL: 31 """ 32 Retorna una URL pg_connection parseada. 33 Se pueden informar los parámetros uno a uno o directamente con la cadena de conexión completa (conn_string). 34 En este ultimo caso se ignoran los parámetros individuales excepto el 'schemas' que se añade a la cadena de conexión si no estuviera 35 36 Args: 37 user (str): usuario 38 psw (str): password 39 host (str): host 40 port (int): port 41 db (str): database 42 url_conn_string (str | URL): connection string or sqlalchemy URL 43 schemas (str): schemas separados por comas 44 45 Returns: 46 URL 47 """ 48 if url_conn_string: 49 url = make_url(url_conn_string) 50 else: 51 url = URL.create( 52 drivername=PG_DRIVER, 53 username=user or '', password=psw or '', 54 host=host or '', port=port, database=db or '', 55 ) 56 57 if schemas: 58 normalized_schemas = ','.join([s.strip() for s in str(schemas).split(',') if s.strip()]) 59 if normalized_schemas: 60 url = url.update_query_dict(dict(options=f"-csearch_path={normalized_schemas}")) 61 62 return url 63 64 65class EngPsqlAlchemy(object): 66 """ 67 Clase que gestiona conexion sqlalchemy a Postgres 68 """ 69 __slots__ = 'url_con_db', 'eng_db', 'logger', 'session_db', 'schemas' 70 _CACHE = {} 71 _CACHE_LOCK = Lock() 72 73 @classmethod 74 def get_cached(cls, user=None, psw=None, 75 srvr_db='localhost', port_db=5432, 76 db='postgres', schemas=None, 77 a_logger=None, url_conn_string=None, force_new=False): 78 """ 79 Recupera una instancia cacheada por conexión o crea una nueva si no existe. 80 81 La clave de caché se basa en user/host/port/db/schemas (o conn_string). 82 Antes de devolver una instancia cacheada, valida y recupera su session si es necesario. 83 """ 84 cache_key = url_pg_string_connection(user=user, psw=psw, host=srvr_db, port=port_db, db=db, 85 url_conn_string=url_conn_string, schemas=schemas) 86 87 with cls._CACHE_LOCK: 88 if force_new: 89 cls._CACHE.pop(cache_key, None) 90 91 inst = cls._CACHE.get(cache_key) 92 if inst is None: 93 inst = cls(user=user, psw=psw, srvr_db=srvr_db, port_db=port_db, db=db, 94 schemas=schemas, a_logger=a_logger, url_conn_string=url_conn_string) 95 cls._CACHE[cache_key] = inst 96 else: 97 inst.ensure_session() 98 99 return inst 100 101 def __init__(self, user=None, psw=None, srvr_db='localhost', port_db=5432, db='postgres', schemas=None, 102 a_logger=None, url_conn_string=None): 103 """ 104 Retorna engine para database postgres. Si no se informa ningun argumento retorna el cacheado 105 106 Args: 107 user (str=None): 108 psw (str=None): 109 srvr_db (str=None): 110 port_db (int=None): 111 db (str=None): 112 schemas (str=None): schemas separated by comma 113 a_logger (logging.Logger=None): 114 url_conn_string (str|URL=None): connection string or sqlalchemy URL 115 116 Returns: 117 sqlalchemy.engine.base.Engine 118 """ 119 if (user is None or psw is None) and url_conn_string is None: 120 raise ValueError("Debe informar user y psw, o alternativamente conn_string") 121 122 self.url_con_db = url_pg_string_connection(user=user, psw=psw, host=srvr_db, port=port_db, db=db, 123 url_conn_string=url_conn_string, schemas=schemas) 124 125 if schemas: 126 self.schemas = [s.strip() for s in str(schemas).split(',') if s.strip()] 127 128 self.logger = a_logger 129 self.__set_logger() 130 131 self.logger.debug(f"Initializing EngPsqlAlchemy '{self.url_con_db}'") 132 133 extra_args = {} 134 if HAS_GEOALCHEMY2: 135 extra_args['plugins'] = ['geoalchemy2'] 136 137 eng_db: Engine = create_engine( 138 self.url_con_db, 139 **extra_args 140 ) 141 142 eng_db.connect() 143 144 self.eng_db = eng_db 145 # self.eng_db.logger = self.logger 146 self._set_session() 147 148 @classmethod 149 def from_url_conn_string(cls, url_conn_string, a_logger=None): 150 """ 151 Constructor alternativo a partir de cadena de conexion. 152 153 Args: 154 url_conn_string (str | URL): 155 a_logger (logging.Logger=None): 156 157 Returns: 158 EngPsqlAlchemy 159 """ 160 return cls(url_conn_string=url_conn_string, a_logger=a_logger) 161 162 def __set_logger(self): 163 """ 164 Asigna el LOGGER po defecto si este no se ha informado al inicializar el gestor 165 166 Returns: 167 """ 168 if self.logger is None: 169 self.logger = get_base_logger(f'{self.__class__.__name__}({self.url_con_db})') 170 171 def _set_session(self, eng_db=None): 172 """ 173 Configura session para controlar workflow de transacciones (commit, rollback, close...) 174 175 Args: 176 eng_db: 177 """ 178 session = sessionmaker() 179 session.configure(bind=eng_db if eng_db else self.eng_db) 180 self.session_db = session() 181 182 def is_session_alive(self): 183 """Valida si la session actual responde correctamente con un ping simple.""" 184 if not getattr(self, 'session_db', None): 185 return False 186 187 try: 188 self.session_db.execute(text('SELECT 1')) 189 return True 190 except Exception as exc: 191 if self.logger: 192 self.logger.warning(f"Session no disponible, se intentará recuperar: {exc}") 193 return False 194 195 def recover_session(self, dispose_engine=False): 196 """Recrea la sesión, opcionalmente reciclando el pool del engine.""" 197 try: 198 if getattr(self, 'session_db', None): 199 self.session_db.close() 200 except Exception: 201 pass 202 203 if dispose_engine and getattr(self, 'eng_db', None): 204 self.eng_db.dispose() 205 206 self._set_session() 207 return self.session_db 208 209 def ensure_session(self): 210 """Garantiza que la session está operativa; si no, la recupera.""" 211 if self.is_session_alive(): 212 return self.session_db 213 214 self.recover_session(dispose_engine=False) 215 if self.is_session_alive(): 216 return self.session_db 217 218 # Último intento reciclando conexiones del pool. 219 self.recover_session(dispose_engine=True) 220 return self.session_db 221 222 def __del__(self): 223 """ 224 Cierra la conexion al matar la instancia 225 """ 226 try: 227 if hasattr(self, 'session_db'): 228 self.session_db.close() 229 if hasattr(self, "con_db"): 230 self.eng_db = None 231 except: 232 pass 233 234 def commit(self): 235 """ 236 Hace commit sobre la sesion actual 237 """ 238 if self.session_db: 239 self.session_db.commit() 240 241 def rollback(self): 242 """ 243 Hace rollback sobre la sesion actual 244 """ 245 if self.session_db: 246 self.session_db.rollback() 247 248 def iter_rows_result(self, query_res, nom_row=None): 249 """ 250 Itera las filas del resultado como namedtuple con las claves de los campos seleccionados 251 252 Args: 253 query_res (sqlalchemy.engine.result.ResultProxy) 254 nom_row (str=None): nombre de la clase namedtuple si aplica 255 256 Yields: 257 namedtuple o tuple 258 """ 259 row_dd = None 260 if query_res.keys(): 261 row_dd = namedtuple(nom_row if nom_row else 'row_result', 262 [n_col.replace(" ", "_") for n_col in query_res.keys()]) 263 264 for row in query_res: 265 yield row_dd(*row) if row_dd else row 266 267 def table_view(self, nom_tab_view, schema=None): 268 """ 269 Retorna acceso a tabla sobre engine DB ( 270 271 Args: 272 nom_tab_view (str): 273 schema (str=None): 274 275 Returns: 276 277 """ 278 schema = self.check_exists_table_view(nom_tab_view, schema=schema) 279 280 meta = MetaData() 281 282 extra_args = {} 283 if schema: 284 extra_args['schema'] = schema 285 286 a_tab = Table(nom_tab_view, meta, autoload_with=self.eng_db, **extra_args) 287 288 return a_tab 289 290 def tables_views(self, schema=None, only_tables=False) -> dict[str, list[str]]: 291 """ 292 Lista las tablas/views disponibles para la conexion actual. 293 294 Args: 295 only_tables: 296 schema (str=None): esquema a consultar; si es None, usa el search_path/por defecto. 297 298 Returns: 299 dict[str, list[str]]: diccionario con las tablas/views disponibles por esquema 300 """ 301 inspector = inspect(self.eng_db) 302 if not schema: 303 schemas = inspector.get_schema_names() 304 else: 305 schemas = [schema] 306 307 tables_views = {} 308 for schema in schemas: 309 tabs_schema = sorted(inspector.get_table_names(schema=schema)) 310 if not only_tables: 311 tabs_schema.extend(sorted(inspector.get_view_names(schema=schema))) 312 if not tabs_schema: 313 continue 314 tables_views[schema] = tabs_schema 315 316 return tables_views 317 318 def columns_table_view(self, nom_tab, schema=None): 319 """ 320 Retorna columnas y tipos de una tabla o vista. 321 322 Args: 323 nom_tab (str): nombre de tabla o vista. 324 schema (str=None): esquema a consultar. 325 326 Returns: 327 dict[str, str]: diccionario indexado por nombre de columna con el tipo. 328 """ 329 schema = self.check_exists_table_view(nom_tab, schema=schema) 330 331 # noinspection SqlResolve,SqlNoDataSourceInspection 332 # language=PostgreSQL 333 sql_query = text(""" 334 SELECT a.attname AS column_name, 335 pg_catalog.format_type(a.atttypid, a.atttypmod) AS column_type 336 FROM pg_catalog.pg_attribute a 337 INNER JOIN pg_catalog.pg_class c 338 ON c.oid = a.attrelid 339 INNER JOIN pg_catalog.pg_namespace n 340 ON n.oid = c.relnamespace 341 WHERE a.attnum > 0 342 AND NOT a.attisdropped 343 AND c.relkind IN ('r', 'p', 'v', 'm', 'f') 344 AND c.relname = :table_name 345 AND ( 346 (:schema IS NOT NULL AND n.nspname = :schema) 347 OR (:schema IS NULL AND pg_catalog.pg_table_is_visible(c.oid)) 348 ) 349 ORDER BY a.attnum 350 """) 351 352 res = self.ensure_session().execute(sql_query, { 353 'table_name': nom_tab, 354 'schema': schema, 355 }) 356 357 cols = {} 358 for row in res: 359 cols[row.column_name] = row.column_type.replace('public.', '') if row.column_type else TYPE_UNKNOWN 360 361 return cols 362 363 def check_exists_table_view(self, nom_tab_or_view, schema=None) -> str: 364 """ 365 Valida que exista una tabla o vista para la conexión actual y devuelve su esquema 366 367 Args: 368 nom_tab_or_view (str): nombre de tabla o vista. 369 schema (str=None): esquema a consultar. 370 371 Returns: 372 str: schema de la tabla o vista. 373 """ 374 schema_tab_view = None 375 for sch, tabs_views in self.tables_views(schema=schema).items(): 376 if nom_tab_or_view in tabs_views: 377 schema_tab_view = sch 378 break 379 380 if not schema_tab_view: 381 raise ValueError(f"No existe la tabla/vista '{nom_tab_or_view}' " 382 f"{'para el esquema {} '.format(schema) if schema else ''}" 383 f"sobre la conexión '{self.url_con_db}'") 384 385 return schema_tab_view 386 387 def geoms_table_view(self, nom_tab, schema=None): 388 """ 389 Retorna columnas geométricas/geográficas de una tabla o vista con metadatos detallados. 390 391 Args: 392 nom_tab (str): nombre de tabla o vista. 393 schema (str=None): esquema a consultar 394 395 Returns: 396 dict[str, dict]: diccionario indexado por nombre de columna con metadatos geométricos. 397 """ 398 schema = self.check_exists_table_view(nom_tab, schema=schema) 399 400 # noinspection SqlResolve,SqlNoDataSourceInspection 401 # language=PostgreSQL 402 sql_query = text(""" 403 SELECT a.attname AS column_name, 404 REPLACE(pg_catalog.format_type(a.atttypid, a.atttypmod), 'public.', '') AS column_type, 405 t.typname AS base_type 406 FROM pg_catalog.pg_attribute a 407 INNER JOIN pg_catalog.pg_class c 408 ON c.oid = a.attrelid 409 INNER JOIN pg_catalog.pg_namespace n 410 ON n.oid = c.relnamespace 411 INNER JOIN pg_catalog.pg_type t 412 ON t.oid = a.atttypid 413 WHERE a.attnum > 0 414 AND NOT a.attisdropped 415 AND c.relkind IN ('r', 'p', 'v', 'm', 'f') 416 AND c.relname = :table_name 417 AND t.typname IN ('geometry', 'geography') 418 AND ( 419 (:schema IS NOT NULL AND n.nspname = :schema) 420 OR (:schema IS NULL AND pg_catalog.pg_table_is_visible(c.oid)) 421 ) 422 ORDER BY a.attnum 423 """) 424 425 res = self.ensure_session().execute(sql_query, { 426 'table_name': nom_tab, 427 'schema': schema, 428 }) 429 430 geoms = {} 431 for row in res: 432 type_str, base_type, geometry_type, srid = self.__parse_spatial_type( 433 row.column_type, 434 base_type_hint=row.base_type, 435 ) 436 geoms[row.column_name] = { 437 'type': type_str, 438 'base_type': base_type, 439 'geometry_type': geometry_type, 440 'srid': srid, 441 } 442 443 return geoms 444 445 @staticmethod 446 def __parse_spatial_type(type_str, base_type_hint=None): 447 """Parsea tipos espaciales tipo geometry(Point,25831) sin depender de funciones PostGIS.""" 448 if not type_str and not base_type_hint: 449 return TYPE_UNKNOWN, TYPE_UNKNOWN, TYPE_UNKNOWN, None 450 451 clean_type = (type_str or '').replace('public.', '').strip() or TYPE_UNKNOWN 452 base_type = (base_type_hint or clean_type.split('(', 1)[0]).strip().lower() 453 geometry_type = TYPE_UNKNOWN 454 srid = None 455 456 # Ejemplos esperados: geometry(Point,25831), geography(MultiPolygon,4326), geometry 457 m = re.match(r'^(geometry|geography)\(([^,()]+)(?:\s*,\s*(\d+))?\)$', clean_type, flags=re.IGNORECASE) 458 if m: 459 base_type = m.group(1).lower() 460 geometry_type = m.group(2) 461 srid = int(m.group(3)) if m.group(3) else None 462 elif clean_type.lower() in ('geometry', 'geography'): 463 base_type = clean_type.lower() 464 465 if base_type not in ('geometry', 'geography'): 466 base_type = TYPE_UNKNOWN 467 468 return clean_type, base_type, geometry_type, srid 469 470 def rows_table_view(self, nom_tab, sql_query=None, **table_args): 471 """ 472 Itera sobre los registros de la tabla 473 474 Args: 475 nom_tab (str): 476 sql_query (str=None): 477 **table_args: argumentos de la funcion table() 478 479 Yields: 480 namedtuple 481 """ 482 tab = self.table_view(nom_tab, **table_args) 483 484 query = tab.select() 485 if sql_query: 486 query = query.where(text(sql_query)) 487 488 res = self.ensure_session().execute(query) 489 490 for row in self.iter_rows_result(res, tab.name): 491 yield row 492 493 def insert_rows_table(self, nom_tab, row_values, **table_args): 494 """ 495 Inserta los registros en la tabla 496 497 Args: 498 nom_tab (str): 499 row_values (list): lista de dicts con los valores de los campos a insertar por cada fila 500 **table_args: argumentos de la funcion table() 501 502 Returns: 503 list: lista de los rows_inserted (namedtuple) 504 """ 505 tab = self.table_view(nom_tab, **table_args) 506 query = tab.insert(values=row_values).returning(*tab.columns()) 507 508 res = self.ensure_session().execute(query) 509 510 rows_inserted = [*self.iter_rows_result(res, tab.name)] 511 512 return rows_inserted 513 514 def update_rows_table(self, tab, values, sql_query=None, **table_args): 515 """ 516 Actualiza los registros de la tabla que cumplan con where_query (todos por defecto) 517 518 Args: 519 tab (str): 520 values (dict): 521 sql_query (str): 522 **table_args: argumentos de la funcion table() 523 524 Returns: 525 generator: iterador con la lista de elementos a devolver 526 """ 527 tab = self.table_view(tab, **table_args) 528 query = tab.update() 529 if sql_query: 530 query = query.where(text(sql_query)) 531 query = query.values(values).returning(*tab.columns()) 532 533 res = self.ensure_session().execute(query) 534 535 return res 536 537 def remove_rows_table(self, nom_tab, sql_query=None, **table_args): 538 """ 539 Borra los registros de la tabla que cumplan con where_query (todos por defecto) 540 541 Args: 542 nom_tab (str): 543 sql_query (str): 544 **table_args: argumentos de la funcion table() 545 546 Returns: 547 res 548 """ 549 tab = self.table_view(nom_tab, **table_args) 550 query = tab.delete() 551 552 if sql_query: 553 query = query.where(text(sql_query)) 554 555 res = self.ensure_session().execute(query) 556 557 return res 558 559 560if __name__ == '__main__': 561 from fire import Fire 562 563 Fire()
30def url_pg_string_connection(user='', psw='', host='localhost', port=5432, db='postgres', url_conn_string=None, 31 schemas='') -> URL: 32 """ 33 Retorna una URL pg_connection parseada. 34 Se pueden informar los parámetros uno a uno o directamente con la cadena de conexión completa (conn_string). 35 En este ultimo caso se ignoran los parámetros individuales excepto el 'schemas' que se añade a la cadena de conexión si no estuviera 36 37 Args: 38 user (str): usuario 39 psw (str): password 40 host (str): host 41 port (int): port 42 db (str): database 43 url_conn_string (str | URL): connection string or sqlalchemy URL 44 schemas (str): schemas separados por comas 45 46 Returns: 47 URL 48 """ 49 if url_conn_string: 50 url = make_url(url_conn_string) 51 else: 52 url = URL.create( 53 drivername=PG_DRIVER, 54 username=user or '', password=psw or '', 55 host=host or '', port=port, database=db or '', 56 ) 57 58 if schemas: 59 normalized_schemas = ','.join([s.strip() for s in str(schemas).split(',') if s.strip()]) 60 if normalized_schemas: 61 url = url.update_query_dict(dict(options=f"-csearch_path={normalized_schemas}")) 62 63 return url
Retorna una URL pg_connection parseada. Se pueden informar los parámetros uno a uno o directamente con la cadena de conexión completa (conn_string). En este ultimo caso se ignoran los parámetros individuales excepto el 'schemas' que se añade a la cadena de conexión si no estuviera
Arguments:
- user (str): usuario
- psw (str): password
- host (str): host
- port (int): port
- db (str): database
- url_conn_string (str | URL): connection string or sqlalchemy URL
- schemas (str): schemas separados por comas
Returns:
URL
66class EngPsqlAlchemy(object): 67 """ 68 Clase que gestiona conexion sqlalchemy a Postgres 69 """ 70 __slots__ = 'url_con_db', 'eng_db', 'logger', 'session_db', 'schemas' 71 _CACHE = {} 72 _CACHE_LOCK = Lock() 73 74 @classmethod 75 def get_cached(cls, user=None, psw=None, 76 srvr_db='localhost', port_db=5432, 77 db='postgres', schemas=None, 78 a_logger=None, url_conn_string=None, force_new=False): 79 """ 80 Recupera una instancia cacheada por conexión o crea una nueva si no existe. 81 82 La clave de caché se basa en user/host/port/db/schemas (o conn_string). 83 Antes de devolver una instancia cacheada, valida y recupera su session si es necesario. 84 """ 85 cache_key = url_pg_string_connection(user=user, psw=psw, host=srvr_db, port=port_db, db=db, 86 url_conn_string=url_conn_string, schemas=schemas) 87 88 with cls._CACHE_LOCK: 89 if force_new: 90 cls._CACHE.pop(cache_key, None) 91 92 inst = cls._CACHE.get(cache_key) 93 if inst is None: 94 inst = cls(user=user, psw=psw, srvr_db=srvr_db, port_db=port_db, db=db, 95 schemas=schemas, a_logger=a_logger, url_conn_string=url_conn_string) 96 cls._CACHE[cache_key] = inst 97 else: 98 inst.ensure_session() 99 100 return inst 101 102 def __init__(self, user=None, psw=None, srvr_db='localhost', port_db=5432, db='postgres', schemas=None, 103 a_logger=None, url_conn_string=None): 104 """ 105 Retorna engine para database postgres. Si no se informa ningun argumento retorna el cacheado 106 107 Args: 108 user (str=None): 109 psw (str=None): 110 srvr_db (str=None): 111 port_db (int=None): 112 db (str=None): 113 schemas (str=None): schemas separated by comma 114 a_logger (logging.Logger=None): 115 url_conn_string (str|URL=None): connection string or sqlalchemy URL 116 117 Returns: 118 sqlalchemy.engine.base.Engine 119 """ 120 if (user is None or psw is None) and url_conn_string is None: 121 raise ValueError("Debe informar user y psw, o alternativamente conn_string") 122 123 self.url_con_db = url_pg_string_connection(user=user, psw=psw, host=srvr_db, port=port_db, db=db, 124 url_conn_string=url_conn_string, schemas=schemas) 125 126 if schemas: 127 self.schemas = [s.strip() for s in str(schemas).split(',') if s.strip()] 128 129 self.logger = a_logger 130 self.__set_logger() 131 132 self.logger.debug(f"Initializing EngPsqlAlchemy '{self.url_con_db}'") 133 134 extra_args = {} 135 if HAS_GEOALCHEMY2: 136 extra_args['plugins'] = ['geoalchemy2'] 137 138 eng_db: Engine = create_engine( 139 self.url_con_db, 140 **extra_args 141 ) 142 143 eng_db.connect() 144 145 self.eng_db = eng_db 146 # self.eng_db.logger = self.logger 147 self._set_session() 148 149 @classmethod 150 def from_url_conn_string(cls, url_conn_string, a_logger=None): 151 """ 152 Constructor alternativo a partir de cadena de conexion. 153 154 Args: 155 url_conn_string (str | URL): 156 a_logger (logging.Logger=None): 157 158 Returns: 159 EngPsqlAlchemy 160 """ 161 return cls(url_conn_string=url_conn_string, a_logger=a_logger) 162 163 def __set_logger(self): 164 """ 165 Asigna el LOGGER po defecto si este no se ha informado al inicializar el gestor 166 167 Returns: 168 """ 169 if self.logger is None: 170 self.logger = get_base_logger(f'{self.__class__.__name__}({self.url_con_db})') 171 172 def _set_session(self, eng_db=None): 173 """ 174 Configura session para controlar workflow de transacciones (commit, rollback, close...) 175 176 Args: 177 eng_db: 178 """ 179 session = sessionmaker() 180 session.configure(bind=eng_db if eng_db else self.eng_db) 181 self.session_db = session() 182 183 def is_session_alive(self): 184 """Valida si la session actual responde correctamente con un ping simple.""" 185 if not getattr(self, 'session_db', None): 186 return False 187 188 try: 189 self.session_db.execute(text('SELECT 1')) 190 return True 191 except Exception as exc: 192 if self.logger: 193 self.logger.warning(f"Session no disponible, se intentará recuperar: {exc}") 194 return False 195 196 def recover_session(self, dispose_engine=False): 197 """Recrea la sesión, opcionalmente reciclando el pool del engine.""" 198 try: 199 if getattr(self, 'session_db', None): 200 self.session_db.close() 201 except Exception: 202 pass 203 204 if dispose_engine and getattr(self, 'eng_db', None): 205 self.eng_db.dispose() 206 207 self._set_session() 208 return self.session_db 209 210 def ensure_session(self): 211 """Garantiza que la session está operativa; si no, la recupera.""" 212 if self.is_session_alive(): 213 return self.session_db 214 215 self.recover_session(dispose_engine=False) 216 if self.is_session_alive(): 217 return self.session_db 218 219 # Último intento reciclando conexiones del pool. 220 self.recover_session(dispose_engine=True) 221 return self.session_db 222 223 def __del__(self): 224 """ 225 Cierra la conexion al matar la instancia 226 """ 227 try: 228 if hasattr(self, 'session_db'): 229 self.session_db.close() 230 if hasattr(self, "con_db"): 231 self.eng_db = None 232 except: 233 pass 234 235 def commit(self): 236 """ 237 Hace commit sobre la sesion actual 238 """ 239 if self.session_db: 240 self.session_db.commit() 241 242 def rollback(self): 243 """ 244 Hace rollback sobre la sesion actual 245 """ 246 if self.session_db: 247 self.session_db.rollback() 248 249 def iter_rows_result(self, query_res, nom_row=None): 250 """ 251 Itera las filas del resultado como namedtuple con las claves de los campos seleccionados 252 253 Args: 254 query_res (sqlalchemy.engine.result.ResultProxy) 255 nom_row (str=None): nombre de la clase namedtuple si aplica 256 257 Yields: 258 namedtuple o tuple 259 """ 260 row_dd = None 261 if query_res.keys(): 262 row_dd = namedtuple(nom_row if nom_row else 'row_result', 263 [n_col.replace(" ", "_") for n_col in query_res.keys()]) 264 265 for row in query_res: 266 yield row_dd(*row) if row_dd else row 267 268 def table_view(self, nom_tab_view, schema=None): 269 """ 270 Retorna acceso a tabla sobre engine DB ( 271 272 Args: 273 nom_tab_view (str): 274 schema (str=None): 275 276 Returns: 277 278 """ 279 schema = self.check_exists_table_view(nom_tab_view, schema=schema) 280 281 meta = MetaData() 282 283 extra_args = {} 284 if schema: 285 extra_args['schema'] = schema 286 287 a_tab = Table(nom_tab_view, meta, autoload_with=self.eng_db, **extra_args) 288 289 return a_tab 290 291 def tables_views(self, schema=None, only_tables=False) -> dict[str, list[str]]: 292 """ 293 Lista las tablas/views disponibles para la conexion actual. 294 295 Args: 296 only_tables: 297 schema (str=None): esquema a consultar; si es None, usa el search_path/por defecto. 298 299 Returns: 300 dict[str, list[str]]: diccionario con las tablas/views disponibles por esquema 301 """ 302 inspector = inspect(self.eng_db) 303 if not schema: 304 schemas = inspector.get_schema_names() 305 else: 306 schemas = [schema] 307 308 tables_views = {} 309 for schema in schemas: 310 tabs_schema = sorted(inspector.get_table_names(schema=schema)) 311 if not only_tables: 312 tabs_schema.extend(sorted(inspector.get_view_names(schema=schema))) 313 if not tabs_schema: 314 continue 315 tables_views[schema] = tabs_schema 316 317 return tables_views 318 319 def columns_table_view(self, nom_tab, schema=None): 320 """ 321 Retorna columnas y tipos de una tabla o vista. 322 323 Args: 324 nom_tab (str): nombre de tabla o vista. 325 schema (str=None): esquema a consultar. 326 327 Returns: 328 dict[str, str]: diccionario indexado por nombre de columna con el tipo. 329 """ 330 schema = self.check_exists_table_view(nom_tab, schema=schema) 331 332 # noinspection SqlResolve,SqlNoDataSourceInspection 333 # language=PostgreSQL 334 sql_query = text(""" 335 SELECT a.attname AS column_name, 336 pg_catalog.format_type(a.atttypid, a.atttypmod) AS column_type 337 FROM pg_catalog.pg_attribute a 338 INNER JOIN pg_catalog.pg_class c 339 ON c.oid = a.attrelid 340 INNER JOIN pg_catalog.pg_namespace n 341 ON n.oid = c.relnamespace 342 WHERE a.attnum > 0 343 AND NOT a.attisdropped 344 AND c.relkind IN ('r', 'p', 'v', 'm', 'f') 345 AND c.relname = :table_name 346 AND ( 347 (:schema IS NOT NULL AND n.nspname = :schema) 348 OR (:schema IS NULL AND pg_catalog.pg_table_is_visible(c.oid)) 349 ) 350 ORDER BY a.attnum 351 """) 352 353 res = self.ensure_session().execute(sql_query, { 354 'table_name': nom_tab, 355 'schema': schema, 356 }) 357 358 cols = {} 359 for row in res: 360 cols[row.column_name] = row.column_type.replace('public.', '') if row.column_type else TYPE_UNKNOWN 361 362 return cols 363 364 def check_exists_table_view(self, nom_tab_or_view, schema=None) -> str: 365 """ 366 Valida que exista una tabla o vista para la conexión actual y devuelve su esquema 367 368 Args: 369 nom_tab_or_view (str): nombre de tabla o vista. 370 schema (str=None): esquema a consultar. 371 372 Returns: 373 str: schema de la tabla o vista. 374 """ 375 schema_tab_view = None 376 for sch, tabs_views in self.tables_views(schema=schema).items(): 377 if nom_tab_or_view in tabs_views: 378 schema_tab_view = sch 379 break 380 381 if not schema_tab_view: 382 raise ValueError(f"No existe la tabla/vista '{nom_tab_or_view}' " 383 f"{'para el esquema {} '.format(schema) if schema else ''}" 384 f"sobre la conexión '{self.url_con_db}'") 385 386 return schema_tab_view 387 388 def geoms_table_view(self, nom_tab, schema=None): 389 """ 390 Retorna columnas geométricas/geográficas de una tabla o vista con metadatos detallados. 391 392 Args: 393 nom_tab (str): nombre de tabla o vista. 394 schema (str=None): esquema a consultar 395 396 Returns: 397 dict[str, dict]: diccionario indexado por nombre de columna con metadatos geométricos. 398 """ 399 schema = self.check_exists_table_view(nom_tab, schema=schema) 400 401 # noinspection SqlResolve,SqlNoDataSourceInspection 402 # language=PostgreSQL 403 sql_query = text(""" 404 SELECT a.attname AS column_name, 405 REPLACE(pg_catalog.format_type(a.atttypid, a.atttypmod), 'public.', '') AS column_type, 406 t.typname AS base_type 407 FROM pg_catalog.pg_attribute a 408 INNER JOIN pg_catalog.pg_class c 409 ON c.oid = a.attrelid 410 INNER JOIN pg_catalog.pg_namespace n 411 ON n.oid = c.relnamespace 412 INNER JOIN pg_catalog.pg_type t 413 ON t.oid = a.atttypid 414 WHERE a.attnum > 0 415 AND NOT a.attisdropped 416 AND c.relkind IN ('r', 'p', 'v', 'm', 'f') 417 AND c.relname = :table_name 418 AND t.typname IN ('geometry', 'geography') 419 AND ( 420 (:schema IS NOT NULL AND n.nspname = :schema) 421 OR (:schema IS NULL AND pg_catalog.pg_table_is_visible(c.oid)) 422 ) 423 ORDER BY a.attnum 424 """) 425 426 res = self.ensure_session().execute(sql_query, { 427 'table_name': nom_tab, 428 'schema': schema, 429 }) 430 431 geoms = {} 432 for row in res: 433 type_str, base_type, geometry_type, srid = self.__parse_spatial_type( 434 row.column_type, 435 base_type_hint=row.base_type, 436 ) 437 geoms[row.column_name] = { 438 'type': type_str, 439 'base_type': base_type, 440 'geometry_type': geometry_type, 441 'srid': srid, 442 } 443 444 return geoms 445 446 @staticmethod 447 def __parse_spatial_type(type_str, base_type_hint=None): 448 """Parsea tipos espaciales tipo geometry(Point,25831) sin depender de funciones PostGIS.""" 449 if not type_str and not base_type_hint: 450 return TYPE_UNKNOWN, TYPE_UNKNOWN, TYPE_UNKNOWN, None 451 452 clean_type = (type_str or '').replace('public.', '').strip() or TYPE_UNKNOWN 453 base_type = (base_type_hint or clean_type.split('(', 1)[0]).strip().lower() 454 geometry_type = TYPE_UNKNOWN 455 srid = None 456 457 # Ejemplos esperados: geometry(Point,25831), geography(MultiPolygon,4326), geometry 458 m = re.match(r'^(geometry|geography)\(([^,()]+)(?:\s*,\s*(\d+))?\)$', clean_type, flags=re.IGNORECASE) 459 if m: 460 base_type = m.group(1).lower() 461 geometry_type = m.group(2) 462 srid = int(m.group(3)) if m.group(3) else None 463 elif clean_type.lower() in ('geometry', 'geography'): 464 base_type = clean_type.lower() 465 466 if base_type not in ('geometry', 'geography'): 467 base_type = TYPE_UNKNOWN 468 469 return clean_type, base_type, geometry_type, srid 470 471 def rows_table_view(self, nom_tab, sql_query=None, **table_args): 472 """ 473 Itera sobre los registros de la tabla 474 475 Args: 476 nom_tab (str): 477 sql_query (str=None): 478 **table_args: argumentos de la funcion table() 479 480 Yields: 481 namedtuple 482 """ 483 tab = self.table_view(nom_tab, **table_args) 484 485 query = tab.select() 486 if sql_query: 487 query = query.where(text(sql_query)) 488 489 res = self.ensure_session().execute(query) 490 491 for row in self.iter_rows_result(res, tab.name): 492 yield row 493 494 def insert_rows_table(self, nom_tab, row_values, **table_args): 495 """ 496 Inserta los registros en la tabla 497 498 Args: 499 nom_tab (str): 500 row_values (list): lista de dicts con los valores de los campos a insertar por cada fila 501 **table_args: argumentos de la funcion table() 502 503 Returns: 504 list: lista de los rows_inserted (namedtuple) 505 """ 506 tab = self.table_view(nom_tab, **table_args) 507 query = tab.insert(values=row_values).returning(*tab.columns()) 508 509 res = self.ensure_session().execute(query) 510 511 rows_inserted = [*self.iter_rows_result(res, tab.name)] 512 513 return rows_inserted 514 515 def update_rows_table(self, tab, values, sql_query=None, **table_args): 516 """ 517 Actualiza los registros de la tabla que cumplan con where_query (todos por defecto) 518 519 Args: 520 tab (str): 521 values (dict): 522 sql_query (str): 523 **table_args: argumentos de la funcion table() 524 525 Returns: 526 generator: iterador con la lista de elementos a devolver 527 """ 528 tab = self.table_view(tab, **table_args) 529 query = tab.update() 530 if sql_query: 531 query = query.where(text(sql_query)) 532 query = query.values(values).returning(*tab.columns()) 533 534 res = self.ensure_session().execute(query) 535 536 return res 537 538 def remove_rows_table(self, nom_tab, sql_query=None, **table_args): 539 """ 540 Borra los registros de la tabla que cumplan con where_query (todos por defecto) 541 542 Args: 543 nom_tab (str): 544 sql_query (str): 545 **table_args: argumentos de la funcion table() 546 547 Returns: 548 res 549 """ 550 tab = self.table_view(nom_tab, **table_args) 551 query = tab.delete() 552 553 if sql_query: 554 query = query.where(text(sql_query)) 555 556 res = self.ensure_session().execute(query) 557 558 return res
Clase que gestiona conexion sqlalchemy a Postgres
102 def __init__(self, user=None, psw=None, srvr_db='localhost', port_db=5432, db='postgres', schemas=None, 103 a_logger=None, url_conn_string=None): 104 """ 105 Retorna engine para database postgres. Si no se informa ningun argumento retorna el cacheado 106 107 Args: 108 user (str=None): 109 psw (str=None): 110 srvr_db (str=None): 111 port_db (int=None): 112 db (str=None): 113 schemas (str=None): schemas separated by comma 114 a_logger (logging.Logger=None): 115 url_conn_string (str|URL=None): connection string or sqlalchemy URL 116 117 Returns: 118 sqlalchemy.engine.base.Engine 119 """ 120 if (user is None or psw is None) and url_conn_string is None: 121 raise ValueError("Debe informar user y psw, o alternativamente conn_string") 122 123 self.url_con_db = url_pg_string_connection(user=user, psw=psw, host=srvr_db, port=port_db, db=db, 124 url_conn_string=url_conn_string, schemas=schemas) 125 126 if schemas: 127 self.schemas = [s.strip() for s in str(schemas).split(',') if s.strip()] 128 129 self.logger = a_logger 130 self.__set_logger() 131 132 self.logger.debug(f"Initializing EngPsqlAlchemy '{self.url_con_db}'") 133 134 extra_args = {} 135 if HAS_GEOALCHEMY2: 136 extra_args['plugins'] = ['geoalchemy2'] 137 138 eng_db: Engine = create_engine( 139 self.url_con_db, 140 **extra_args 141 ) 142 143 eng_db.connect() 144 145 self.eng_db = eng_db 146 # self.eng_db.logger = self.logger 147 self._set_session()
Retorna engine para database postgres. Si no se informa ningun argumento retorna el cacheado
Arguments:
- user (str=None):
- psw (str=None):
- srvr_db (str=None):
- port_db (int=None):
- db (str=None):
- schemas (str=None): schemas separated by comma
- a_logger (logging.Logger=None):
- url_conn_string (str|URL=None): connection string or sqlalchemy URL
Returns:
sqlalchemy.engine.base.Engine
74 @classmethod 75 def get_cached(cls, user=None, psw=None, 76 srvr_db='localhost', port_db=5432, 77 db='postgres', schemas=None, 78 a_logger=None, url_conn_string=None, force_new=False): 79 """ 80 Recupera una instancia cacheada por conexión o crea una nueva si no existe. 81 82 La clave de caché se basa en user/host/port/db/schemas (o conn_string). 83 Antes de devolver una instancia cacheada, valida y recupera su session si es necesario. 84 """ 85 cache_key = url_pg_string_connection(user=user, psw=psw, host=srvr_db, port=port_db, db=db, 86 url_conn_string=url_conn_string, schemas=schemas) 87 88 with cls._CACHE_LOCK: 89 if force_new: 90 cls._CACHE.pop(cache_key, None) 91 92 inst = cls._CACHE.get(cache_key) 93 if inst is None: 94 inst = cls(user=user, psw=psw, srvr_db=srvr_db, port_db=port_db, db=db, 95 schemas=schemas, a_logger=a_logger, url_conn_string=url_conn_string) 96 cls._CACHE[cache_key] = inst 97 else: 98 inst.ensure_session() 99 100 return inst
Recupera una instancia cacheada por conexión o crea una nueva si no existe.
La clave de caché se basa en user/host/port/db/schemas (o conn_string). Antes de devolver una instancia cacheada, valida y recupera su session si es necesario.
149 @classmethod 150 def from_url_conn_string(cls, url_conn_string, a_logger=None): 151 """ 152 Constructor alternativo a partir de cadena de conexion. 153 154 Args: 155 url_conn_string (str | URL): 156 a_logger (logging.Logger=None): 157 158 Returns: 159 EngPsqlAlchemy 160 """ 161 return cls(url_conn_string=url_conn_string, a_logger=a_logger)
Constructor alternativo a partir de cadena de conexion.
Arguments:
- url_conn_string (str | URL):
- a_logger (logging.Logger=None):
Returns:
EngPsqlAlchemy
183 def is_session_alive(self): 184 """Valida si la session actual responde correctamente con un ping simple.""" 185 if not getattr(self, 'session_db', None): 186 return False 187 188 try: 189 self.session_db.execute(text('SELECT 1')) 190 return True 191 except Exception as exc: 192 if self.logger: 193 self.logger.warning(f"Session no disponible, se intentará recuperar: {exc}") 194 return False
Valida si la session actual responde correctamente con un ping simple.
196 def recover_session(self, dispose_engine=False): 197 """Recrea la sesión, opcionalmente reciclando el pool del engine.""" 198 try: 199 if getattr(self, 'session_db', None): 200 self.session_db.close() 201 except Exception: 202 pass 203 204 if dispose_engine and getattr(self, 'eng_db', None): 205 self.eng_db.dispose() 206 207 self._set_session() 208 return self.session_db
Recrea la sesión, opcionalmente reciclando el pool del engine.
210 def ensure_session(self): 211 """Garantiza que la session está operativa; si no, la recupera.""" 212 if self.is_session_alive(): 213 return self.session_db 214 215 self.recover_session(dispose_engine=False) 216 if self.is_session_alive(): 217 return self.session_db 218 219 # Último intento reciclando conexiones del pool. 220 self.recover_session(dispose_engine=True) 221 return self.session_db
Garantiza que la session está operativa; si no, la recupera.
235 def commit(self): 236 """ 237 Hace commit sobre la sesion actual 238 """ 239 if self.session_db: 240 self.session_db.commit()
Hace commit sobre la sesion actual
242 def rollback(self): 243 """ 244 Hace rollback sobre la sesion actual 245 """ 246 if self.session_db: 247 self.session_db.rollback()
Hace rollback sobre la sesion actual
249 def iter_rows_result(self, query_res, nom_row=None): 250 """ 251 Itera las filas del resultado como namedtuple con las claves de los campos seleccionados 252 253 Args: 254 query_res (sqlalchemy.engine.result.ResultProxy) 255 nom_row (str=None): nombre de la clase namedtuple si aplica 256 257 Yields: 258 namedtuple o tuple 259 """ 260 row_dd = None 261 if query_res.keys(): 262 row_dd = namedtuple(nom_row if nom_row else 'row_result', 263 [n_col.replace(" ", "_") for n_col in query_res.keys()]) 264 265 for row in query_res: 266 yield row_dd(*row) if row_dd else row
Itera las filas del resultado como namedtuple con las claves de los campos seleccionados
Arguments:
- query_res (sqlalchemy.engine.result.ResultProxy)
- nom_row (str=None): nombre de la clase namedtuple si aplica
Yields:
namedtuple o tuple
268 def table_view(self, nom_tab_view, schema=None): 269 """ 270 Retorna acceso a tabla sobre engine DB ( 271 272 Args: 273 nom_tab_view (str): 274 schema (str=None): 275 276 Returns: 277 278 """ 279 schema = self.check_exists_table_view(nom_tab_view, schema=schema) 280 281 meta = MetaData() 282 283 extra_args = {} 284 if schema: 285 extra_args['schema'] = schema 286 287 a_tab = Table(nom_tab_view, meta, autoload_with=self.eng_db, **extra_args) 288 289 return a_tab
Retorna acceso a tabla sobre engine DB (
Arguments:
- nom_tab_view (str):
- schema (str=None):
Returns:
291 def tables_views(self, schema=None, only_tables=False) -> dict[str, list[str]]: 292 """ 293 Lista las tablas/views disponibles para la conexion actual. 294 295 Args: 296 only_tables: 297 schema (str=None): esquema a consultar; si es None, usa el search_path/por defecto. 298 299 Returns: 300 dict[str, list[str]]: diccionario con las tablas/views disponibles por esquema 301 """ 302 inspector = inspect(self.eng_db) 303 if not schema: 304 schemas = inspector.get_schema_names() 305 else: 306 schemas = [schema] 307 308 tables_views = {} 309 for schema in schemas: 310 tabs_schema = sorted(inspector.get_table_names(schema=schema)) 311 if not only_tables: 312 tabs_schema.extend(sorted(inspector.get_view_names(schema=schema))) 313 if not tabs_schema: 314 continue 315 tables_views[schema] = tabs_schema 316 317 return tables_views
Lista las tablas/views disponibles para la conexion actual.
Arguments:
- only_tables:
- schema (str=None): esquema a consultar; si es None, usa el search_path/por defecto.
Returns:
dict[str, list[str]]: diccionario con las tablas/views disponibles por esquema
319 def columns_table_view(self, nom_tab, schema=None): 320 """ 321 Retorna columnas y tipos de una tabla o vista. 322 323 Args: 324 nom_tab (str): nombre de tabla o vista. 325 schema (str=None): esquema a consultar. 326 327 Returns: 328 dict[str, str]: diccionario indexado por nombre de columna con el tipo. 329 """ 330 schema = self.check_exists_table_view(nom_tab, schema=schema) 331 332 # noinspection SqlResolve,SqlNoDataSourceInspection 333 # language=PostgreSQL 334 sql_query = text(""" 335 SELECT a.attname AS column_name, 336 pg_catalog.format_type(a.atttypid, a.atttypmod) AS column_type 337 FROM pg_catalog.pg_attribute a 338 INNER JOIN pg_catalog.pg_class c 339 ON c.oid = a.attrelid 340 INNER JOIN pg_catalog.pg_namespace n 341 ON n.oid = c.relnamespace 342 WHERE a.attnum > 0 343 AND NOT a.attisdropped 344 AND c.relkind IN ('r', 'p', 'v', 'm', 'f') 345 AND c.relname = :table_name 346 AND ( 347 (:schema IS NOT NULL AND n.nspname = :schema) 348 OR (:schema IS NULL AND pg_catalog.pg_table_is_visible(c.oid)) 349 ) 350 ORDER BY a.attnum 351 """) 352 353 res = self.ensure_session().execute(sql_query, { 354 'table_name': nom_tab, 355 'schema': schema, 356 }) 357 358 cols = {} 359 for row in res: 360 cols[row.column_name] = row.column_type.replace('public.', '') if row.column_type else TYPE_UNKNOWN 361 362 return cols
Retorna columnas y tipos de una tabla o vista.
Arguments:
- nom_tab (str): nombre de tabla o vista.
- schema (str=None): esquema a consultar.
Returns:
dict[str, str]: diccionario indexado por nombre de columna con el tipo.
364 def check_exists_table_view(self, nom_tab_or_view, schema=None) -> str: 365 """ 366 Valida que exista una tabla o vista para la conexión actual y devuelve su esquema 367 368 Args: 369 nom_tab_or_view (str): nombre de tabla o vista. 370 schema (str=None): esquema a consultar. 371 372 Returns: 373 str: schema de la tabla o vista. 374 """ 375 schema_tab_view = None 376 for sch, tabs_views in self.tables_views(schema=schema).items(): 377 if nom_tab_or_view in tabs_views: 378 schema_tab_view = sch 379 break 380 381 if not schema_tab_view: 382 raise ValueError(f"No existe la tabla/vista '{nom_tab_or_view}' " 383 f"{'para el esquema {} '.format(schema) if schema else ''}" 384 f"sobre la conexión '{self.url_con_db}'") 385 386 return schema_tab_view
Valida que exista una tabla o vista para la conexión actual y devuelve su esquema
Arguments:
- nom_tab_or_view (str): nombre de tabla o vista.
- schema (str=None): esquema a consultar.
Returns:
str: schema de la tabla o vista.
388 def geoms_table_view(self, nom_tab, schema=None): 389 """ 390 Retorna columnas geométricas/geográficas de una tabla o vista con metadatos detallados. 391 392 Args: 393 nom_tab (str): nombre de tabla o vista. 394 schema (str=None): esquema a consultar 395 396 Returns: 397 dict[str, dict]: diccionario indexado por nombre de columna con metadatos geométricos. 398 """ 399 schema = self.check_exists_table_view(nom_tab, schema=schema) 400 401 # noinspection SqlResolve,SqlNoDataSourceInspection 402 # language=PostgreSQL 403 sql_query = text(""" 404 SELECT a.attname AS column_name, 405 REPLACE(pg_catalog.format_type(a.atttypid, a.atttypmod), 'public.', '') AS column_type, 406 t.typname AS base_type 407 FROM pg_catalog.pg_attribute a 408 INNER JOIN pg_catalog.pg_class c 409 ON c.oid = a.attrelid 410 INNER JOIN pg_catalog.pg_namespace n 411 ON n.oid = c.relnamespace 412 INNER JOIN pg_catalog.pg_type t 413 ON t.oid = a.atttypid 414 WHERE a.attnum > 0 415 AND NOT a.attisdropped 416 AND c.relkind IN ('r', 'p', 'v', 'm', 'f') 417 AND c.relname = :table_name 418 AND t.typname IN ('geometry', 'geography') 419 AND ( 420 (:schema IS NOT NULL AND n.nspname = :schema) 421 OR (:schema IS NULL AND pg_catalog.pg_table_is_visible(c.oid)) 422 ) 423 ORDER BY a.attnum 424 """) 425 426 res = self.ensure_session().execute(sql_query, { 427 'table_name': nom_tab, 428 'schema': schema, 429 }) 430 431 geoms = {} 432 for row in res: 433 type_str, base_type, geometry_type, srid = self.__parse_spatial_type( 434 row.column_type, 435 base_type_hint=row.base_type, 436 ) 437 geoms[row.column_name] = { 438 'type': type_str, 439 'base_type': base_type, 440 'geometry_type': geometry_type, 441 'srid': srid, 442 } 443 444 return geoms
Retorna columnas geométricas/geográficas de una tabla o vista con metadatos detallados.
Arguments:
- nom_tab (str): nombre de tabla o vista.
- schema (str=None): esquema a consultar
Returns:
dict[str, dict]: diccionario indexado por nombre de columna con metadatos geométricos.
471 def rows_table_view(self, nom_tab, sql_query=None, **table_args): 472 """ 473 Itera sobre los registros de la tabla 474 475 Args: 476 nom_tab (str): 477 sql_query (str=None): 478 **table_args: argumentos de la funcion table() 479 480 Yields: 481 namedtuple 482 """ 483 tab = self.table_view(nom_tab, **table_args) 484 485 query = tab.select() 486 if sql_query: 487 query = query.where(text(sql_query)) 488 489 res = self.ensure_session().execute(query) 490 491 for row in self.iter_rows_result(res, tab.name): 492 yield row
Itera sobre los registros de la tabla
Arguments:
- nom_tab (str):
- sql_query (str=None):
- **table_args: argumentos de la funcion table()
Yields:
namedtuple
494 def insert_rows_table(self, nom_tab, row_values, **table_args): 495 """ 496 Inserta los registros en la tabla 497 498 Args: 499 nom_tab (str): 500 row_values (list): lista de dicts con los valores de los campos a insertar por cada fila 501 **table_args: argumentos de la funcion table() 502 503 Returns: 504 list: lista de los rows_inserted (namedtuple) 505 """ 506 tab = self.table_view(nom_tab, **table_args) 507 query = tab.insert(values=row_values).returning(*tab.columns()) 508 509 res = self.ensure_session().execute(query) 510 511 rows_inserted = [*self.iter_rows_result(res, tab.name)] 512 513 return rows_inserted
Inserta los registros en la tabla
Arguments:
- nom_tab (str):
- row_values (list): lista de dicts con los valores de los campos a insertar por cada fila
- **table_args: argumentos de la funcion table()
Returns:
list: lista de los rows_inserted (namedtuple)
515 def update_rows_table(self, tab, values, sql_query=None, **table_args): 516 """ 517 Actualiza los registros de la tabla que cumplan con where_query (todos por defecto) 518 519 Args: 520 tab (str): 521 values (dict): 522 sql_query (str): 523 **table_args: argumentos de la funcion table() 524 525 Returns: 526 generator: iterador con la lista de elementos a devolver 527 """ 528 tab = self.table_view(tab, **table_args) 529 query = tab.update() 530 if sql_query: 531 query = query.where(text(sql_query)) 532 query = query.values(values).returning(*tab.columns()) 533 534 res = self.ensure_session().execute(query) 535 536 return res
Actualiza los registros de la tabla que cumplan con where_query (todos por defecto)
Arguments:
- tab (str):
- values (dict):
- sql_query (str):
- **table_args: argumentos de la funcion table()
Returns:
generator: iterador con la lista de elementos a devolver
538 def remove_rows_table(self, nom_tab, sql_query=None, **table_args): 539 """ 540 Borra los registros de la tabla que cumplan con where_query (todos por defecto) 541 542 Args: 543 nom_tab (str): 544 sql_query (str): 545 **table_args: argumentos de la funcion table() 546 547 Returns: 548 res 549 """ 550 tab = self.table_view(nom_tab, **table_args) 551 query = tab.delete() 552 553 if sql_query: 554 query = query.where(text(sql_query)) 555 556 res = self.ensure_session().execute(query) 557 558 return res
Borra los registros de la tabla que cumplan con where_query (todos por defecto)
Arguments:
- nom_tab (str):
- sql_query (str):
- **table_args: argumentos de la funcion table()
Returns:
res