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()
TYPE_UNKNOWN = 'UNKNOWN'
PG_DRIVER = 'postgresql+psycopg2'
def url_pg_string_connection( user='', psw='', host='localhost', port=5432, db='postgres', url_conn_string=None, schemas='') -> sqlalchemy.engine.url.URL:
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

class EngPsqlAlchemy:
 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

EngPsqlAlchemy( user=None, psw=None, srvr_db='localhost', port_db=5432, db='postgres', schemas=None, a_logger=None, url_conn_string=None)
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

@classmethod
def get_cached( cls, user=None, psw=None, srvr_db='localhost', port_db=5432, db='postgres', schemas=None, a_logger=None, url_conn_string=None, force_new=False):
 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.

url_con_db
logger
eng_db
@classmethod
def from_url_conn_string(cls, url_conn_string, a_logger=None):
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

def is_session_alive(self):
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.

def recover_session(self, dispose_engine=False):
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.

def ensure_session(self):
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.

def commit(self):
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

def rollback(self):
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

def iter_rows_result(self, query_res, nom_row=None):
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

def table_view(self, nom_tab_view, schema=None):
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:

def tables_views(self, schema=None, only_tables=False) -> dict[str, list[str]]:
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

def columns_table_view(self, nom_tab, schema=None):
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.

def check_exists_table_view(self, nom_tab_or_view, schema=None) -> str:
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.

def geoms_table_view(self, nom_tab, schema=None):
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.

def rows_table_view(self, nom_tab, sql_query=None, **table_args):
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

def insert_rows_table(self, nom_tab, row_values, **table_args):
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)

def update_rows_table(self, tab, values, sql_query=None, **table_args):
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

def remove_rows_table(self, nom_tab, sql_query=None, **table_args):
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

schemas
session_db