From 079d8f0c53584ad6d48c6b0908b78ed136d7d5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Brunner?= Date: Wed, 1 Feb 2023 17:43:11 +0100 Subject: [PATCH] Apply the update to SQLAlchemy 2.0 --- .prospector.yaml | 1 + c2cwsgiutils/db.py | 78 +++++++++++++---------- c2cwsgiutils/errors.py | 9 +-- c2cwsgiutils/health_check.py | 57 ++++++++++------- c2cwsgiutils/models_graph.py | 2 +- c2cwsgiutils/request_tracking/_sql.py | 4 +- c2cwsgiutils/scripts/stats_db.py | 68 ++++++++++++++------ c2cwsgiutils/sqlalchemylogger/_models.py | 5 +- c2cwsgiutils/sqlalchemylogger/handlers.py | 12 ++-- 9 files changed, 149 insertions(+), 87 deletions(-) diff --git a/.prospector.yaml b/.prospector.yaml index 2fdecb6e5..eba3f7ba7 100644 --- a/.prospector.yaml +++ b/.prospector.yaml @@ -40,6 +40,7 @@ pycodestyle: disable: - E203 # Whitespace before ':', duplicated with black, with error in array - E722 # do not use bare 'except', duplicated with pylint + - E261 # at least two spaces before inline comment, duplicated with black pydocstyle: disable: diff --git a/c2cwsgiutils/db.py b/c2cwsgiutils/db.py index 349edaf2e..da4f4aeaf 100644 --- a/c2cwsgiutils/db.py +++ b/c2cwsgiutils/db.py @@ -3,7 +3,6 @@ import re import warnings from typing import Any, Callable, Iterable, Optional, Pattern, Tuple, Union, cast - import pyramid.config import pyramid.config.settings import pyramid.request @@ -35,7 +34,7 @@ def setup_session( force_master: Optional[Iterable[str]] = None, force_slave: Optional[Iterable[str]] = None, ) -> Tuple[ - Union[sqlalchemy.orm.Session, sqlalchemy.orm.scoped_session], + Union[sqlalchemy.orm.Session, sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]], sqlalchemy.engine.Engine, sqlalchemy.engine.Engine, ]: @@ -67,7 +66,7 @@ def setup_session( slave_prefix = master_prefix settings = config.registry.settings rw_engine = sqlalchemy.engine_from_config(settings, master_prefix + ".") - rw_engine.c2c_name = master_prefix + rw_engine.c2c_name = master_prefix # type: ignore factory = sqlalchemy.orm.sessionmaker(bind=rw_engine) register(factory) db_session = sqlalchemy.orm.scoped_session(factory) @@ -76,14 +75,14 @@ def setup_session( if settings[master_prefix + ".url"] != settings.get(slave_prefix + ".url"): LOG.info("Using a slave DB for reading %s", master_prefix) ro_engine = sqlalchemy.engine_from_config(config.get_settings(), slave_prefix + ".") - ro_engine.c2c_name = slave_prefix + ro_engine.c2c_name = slave_prefix # type: ignore tween_name = master_prefix.replace(".", "_") _add_tween(config, tween_name, db_session, force_master, force_slave) else: ro_engine = rw_engine - db_session.c2c_rw_bind = rw_engine - db_session.c2c_ro_bind = ro_engine + db_session.c2c_rw_bind = rw_engine # type: ignore + db_session.c2c_ro_bind = ro_engine # type: ignore return db_session, rw_engine, ro_engine @@ -95,7 +94,7 @@ def create_session( force_master: Optional[Iterable[str]] = None, force_slave: Optional[Iterable[str]] = None, **engine_config: Any, -) -> Union[sqlalchemy.orm.Session, sqlalchemy.orm.scoped_session]: +) -> Union[sqlalchemy.orm.Session, sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]]: """ Create a SQLAlchemy session. @@ -133,21 +132,21 @@ def create_session( LOG.info("Using a slave DB for reading %s", name) ro_engine = sqlalchemy.create_engine(slave_url, **engine_config) _add_tween(config, name, db_session, force_master, force_slave) - rw_engine.c2c_name = name + "_master" - ro_engine.c2c_name = name + "_slave" + rw_engine.c2c_name = name + "_master" # type: ignore + ro_engine.c2c_name = name + "_slave" # type: ignore else: - rw_engine.c2c_name = name + rw_engine.c2c_name = name # type: ignore ro_engine = rw_engine - db_session.c2c_rw_bind = rw_engine - db_session.c2c_ro_bind = ro_engine + db_session.c2c_rw_bind = rw_engine # type: ignore + db_session.c2c_ro_bind = ro_engine # type: ignore return db_session def _add_tween( config: pyramid.config.Configurator, name: str, - db_session: Union[sqlalchemy.orm.Session, sqlalchemy.orm.scoped_session], + db_session: sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session], force_master: Optional[Iterable[str]], force_slave: Optional[Iterable[str]], ) -> None: @@ -176,11 +175,19 @@ def db_chooser_tween(request: pyramid.request.Request) -> Any: not has_force_master and (request.method in ("GET", "OPTIONS") or any(r.match(method_path) for r in slave_paths)) ): - LOG.debug("Using %s database for: %s", db_session.c2c_ro_bind.c2c_name, method_path) - session.bind = db_session.c2c_ro_bind + LOG.debug( + "Using %s database for: %s", + db_session.c2c_ro_bind.c2c_name, # type: ignore + method_path, + ) + session.bind = db_session.c2c_ro_bind # type: ignore else: - LOG.debug("Using %s database for: %s", db_session.c2c_rw_bind.c2c_name, method_path) - session.bind = db_session.c2c_rw_bind + LOG.debug( + "Using %s database for: %s", + db_session.c2c_rw_bind.c2c_name, # type: ignore + method_path, + ) + session.bind = db_session.c2c_rw_bind # type: ignore try: return handler(request) @@ -193,7 +200,7 @@ def db_chooser_tween(request: pyramid.request.Request) -> Any: config.add_tween("c2cwsgiutils.db.tweens." + name, over="pyramid_tm.tm_tween_factory") -class SessionFactory(sessionmaker): # type: ignore +class SessionFactory(sessionmaker[sqlalchemy.orm.Session]): # pylint: disable=unsubscriptable-object """The custom session factory that manage the read only and read write sessions.""" def __init__( @@ -213,18 +220,18 @@ def __init__( def engine_name(self, readwrite: bool) -> str: if readwrite: - return cast(str, self.rw_engine.c2c_name) - return cast(str, self.ro_engine.c2c_name) + return cast(str, self.rw_engine.c2c_name) # type: ignore + return cast(str, self.ro_engine.c2c_name) # type: ignore - def __call__( + def __call__( # type: ignore self, request: Optional[pyramid.request.Request], readwrite: Optional[bool] = None, **local_kw: Any - ) -> sqlalchemy.orm.Session: + ) -> sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]: if readwrite is not None: if readwrite and not force_readonly: - LOG.debug("Using %s database", self.rw_engine.c2c_name) + LOG.debug("Using %s database", self.rw_engine.c2c_name) # type: ignore self.configure(bind=self.rw_engine) else: - LOG.debug("Using %s database", self.ro_engine.c2c_name) + LOG.debug("Using %s database", self.ro_engine.c2c_name) # type: ignore self.configure(bind=self.ro_engine) else: assert request is not None @@ -237,12 +244,12 @@ def __call__( or any(r.match(method_path) for r in self.slave_paths) ) ): - LOG.debug("Using %s database for: %s", self.ro_engine.c2c_name, method_path) + LOG.debug("Using %s database for: %s", self.ro_engine.c2c_name, method_path) # type: ignore self.configure(bind=self.ro_engine) else: - LOG.debug("Using %s database for: %s", self.rw_engine.c2c_name, method_path) + LOG.debug("Using %s database for: %s", self.rw_engine.c2c_name, method_path) # type: ignore self.configure(bind=self.rw_engine) - return super().__call__(**local_kw) + return super().__call__(**local_kw) # type: ignore def get_engine( @@ -252,7 +259,9 @@ def get_engine( return engine_from_config(settings, prefix) -def get_session_factory(engine: sqlalchemy.engine.Engine) -> sessionmaker: +def get_session_factory( + engine: sqlalchemy.engine.Engine, +) -> sessionmaker[sqlalchemy.orm.Session]: # pylint: disable=unsubscriptable-object """Get the session factory from the engine.""" factory = sessionmaker() factory.configure(bind=engine) @@ -260,7 +269,7 @@ def get_session_factory(engine: sqlalchemy.engine.Engine) -> sessionmaker: def get_tm_session( - session_factory: sessionmaker, + session_factory: sessionmaker[sqlalchemy.orm.Session], # pylint: disable=unsubscriptable-object transaction_manager: transaction.TransactionManager, ) -> sqlalchemy.orm.Session: """ @@ -319,6 +328,7 @@ def get_tm_session( request = dbsession.info["request"] """ dbsession = session_factory() + assert isinstance(dbsession, sqlalchemy.orm.Session) zope.sqlalchemy.register(dbsession, transaction_manager=transaction_manager) return dbsession @@ -327,7 +337,7 @@ def get_tm_session_pyramid( session_factory: SessionFactory, transaction_manager: transaction.TransactionManager, request: pyramid.request.Request, -) -> sqlalchemy.orm.Session: +) -> sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]: """ Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. @@ -367,13 +377,13 @@ def init( dbengine = settings.get("dbengine") if not dbengine: rw_engine = get_engine(settings, master_prefix + ".") - rw_engine.c2c_name = master_prefix + rw_engine.c2c_name = master_prefix # type: ignore # Setup a slave DB connection and add a tween to use it. if slave_prefix and settings[master_prefix + ".url"] != settings.get(slave_prefix + ".url"): LOG.info("Using a slave DB for reading %s", master_prefix) ro_engine = get_engine(config.get_settings(), slave_prefix + ".") - ro_engine.c2c_name = slave_prefix + ro_engine.c2c_name = slave_prefix # type: ignore else: ro_engine = rw_engine else: @@ -383,12 +393,14 @@ def init( config.registry["dbsession_factory"] = session_factory # make request.dbsession available for use in Pyramid - def dbsession(request: pyramid.request.Request) -> sqlalchemy.orm.Session: + def dbsession(request: pyramid.request.Request) -> sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]: # hook to share the dbsession fixture in testing dbsession = request.environ.get("app.dbsession") if dbsession is None: # request.tm is the transaction manager used by pyramid_tm dbsession = get_tm_session_pyramid(session_factory, request.tm, request=request) + assert dbsession is not None + assert isinstance(dbsession, sqlalchemy.orm.scoped_session) return dbsession config.add_request_method(dbsession, reify=True) diff --git a/c2cwsgiutils/errors.py b/c2cwsgiutils/errors.py index 8ead774b3..f42144f50 100644 --- a/c2cwsgiutils/errors.py +++ b/c2cwsgiutils/errors.py @@ -107,10 +107,11 @@ def _include_dev_details(request: pyramid.request.Request) -> bool: def _integrity_error( exception: sqlalchemy.exc.StatementError, request: pyramid.request.Request ) -> pyramid.response.Response: - def reduce_info_sent(e: sqlalchemy.exc.StatementError) -> None: - # remove details (SQL statement and links to SQLAlchemy) from the error - e.statement = None - e.code = None + def reduce_info_sent(e: Exception) -> None: + if isinstance(e, sqlalchemy.exc.StatementError): + # remove details (SQL statement and links to SQLAlchemy) from the error + e.statement = None + e.code = None return _do_error(request, 400, exception, reduce_info_sent=reduce_info_sent) diff --git a/c2cwsgiutils/health_check.py b/c2cwsgiutils/health_check.py index 294725767..de5b50445 100644 --- a/c2cwsgiutils/health_check.py +++ b/c2cwsgiutils/health_check.py @@ -9,6 +9,7 @@ import logging import os import re +import sqlalchemy.sql import subprocess # nosec import time import traceback @@ -58,7 +59,7 @@ class _Binding: def name(self) -> str: raise NotImplementedError() - def __enter__(self) -> sqlalchemy.orm.Session: + def __enter__(self) -> sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]: raise NotImplementedError() def __exit__( @@ -78,21 +79,23 @@ def __init__(self, session: c2cwsgiutils.db.SessionFactory, readwrite: bool): def name(self) -> str: return self.session.engine_name(self.readwrite) - def __enter__(self) -> sqlalchemy.orm.Session: + def __enter__(self) -> sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]: return self.session(None, self.readwrite) class _OldBinding(_Binding): - def __init__(self, session: sqlalchemy.orm.scoping.scoped_session, engine: sqlalchemy.engine.Engine): + def __init__( + self, session: sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session], engine: sqlalchemy.engine.Engine + ): self.session = session self.engine = engine self.prev_bind = None def name(self) -> str: - return cast(str, self.engine.c2c_name) + return cast(str, self.engine.c2c_name) # type: ignore - def __enter__(self) -> sqlalchemy.orm.Session: - self.prev_bind = self.session.bind + def __enter__(self) -> sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]: + self.prev_bind = self.session.bind # type: ignore self.session.bind = self.engine return self.session @@ -107,7 +110,7 @@ def __exit__( def _get_binding_class( - session: Union[sqlalchemy.orm.scoping.scoped_session, c2cwsgiutils.db.SessionFactory], + session: Union[sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session], c2cwsgiutils.db.SessionFactory], ro_engin: sqlalchemy.engine.Engine, rw_engin: sqlalchemy.engine.Engine, readwrite: bool, @@ -119,15 +122,15 @@ def _get_binding_class( def _get_bindings( - session: Union[sqlalchemy.orm.scoping.scoped_session, c2cwsgiutils.db.SessionFactory], + session: Union[sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session], c2cwsgiutils.db.SessionFactory], engine_type: EngineType, -) -> List[sqlalchemy.engine.Engine]: +) -> List[_Binding]: if isinstance(session, c2cwsgiutils.db.SessionFactory): ro_engin = session.ro_engine rw_engin = session.rw_engine else: - ro_engin = session.c2c_ro_bind - rw_engin = session.c2c_rw_bind + ro_engin = session.c2c_ro_bind # type: ignore + rw_engin = session.c2c_rw_bind # type: ignore if rw_engin == ro_engin: engine_type = EngineType.WRITE_ONLY @@ -190,8 +193,8 @@ def __init__(self, config: pyramid.config.Configurator) -> None: def add_db_session_check( self, - session: Union[sqlalchemy.orm.scoping.scoped_session, c2cwsgiutils.db.SessionFactory], - query_cb: Optional[Callable[[sqlalchemy.orm.scoping.scoped_session], Any]] = None, + session: Union[sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session], c2cwsgiutils.db.SessionFactory], + query_cb: Optional[Callable[[sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]], Any]] = None, at_least_one_model: Optional[object] = None, level: int = 1, engine_type: EngineType = EngineType.READ_AND_WRITE, @@ -218,7 +221,7 @@ def add_db_session_check( def add_alembic_check( self, - session: Union[sqlalchemy.orm.scoping.scoped_session, c2cwsgiutils.db.SessionFactory], + session: sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session], alembic_ini_path: str, level: int = 2, name: str = "alembic", @@ -247,17 +250,21 @@ def add_alembic_check( if version_schema is None: version_schema = config.get(name, "version_table_schema", fallback="public") + assert version_schema if version_table is None: version_table = config.get(name, "version_table", fallback="alembic_version") + assert version_table class _Check: - def __init__(self, session: sqlalchemy.orm.scoping.scoped_session) -> None: + def __init__(self, session: sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]) -> None: self.session = session def __call__(self, request: pyramid.request.Request) -> str: + assert version_schema + assert version_table for binding in _get_bindings(self.session, EngineType.READ_AND_WRITE): - with binding as session: + with binding as binded_session: if stats.USE_TAGS: key = ["sql", "manual", "health_check", "alembic"] tags: Optional[Dict[str, str]] = dict(conf=alembic_ini_path, con=binding.name()) @@ -272,11 +279,15 @@ def __call__(self, request: pyramid.request.Request) -> str: ] tags = None with stats.timer_context(key, tags): - quote = session.bind.dialect.identifier_preparer.quote - (actual_version,) = session.execute( - "SELECT version_num FROM " # nosec - f"{quote(version_schema)}.{quote(version_table)}" + result = binded_session.execute( + sqlalchemy.text( + "SELECT version_num FROM " # nosec + f"{sqlalchemy.sql.quoted_name(version_schema, True)}." + f"{sqlalchemy.sql.quoted_name(version_table, True)}" + ) ).fetchone() + assert result is not None + (actual_version,) = result if stats.USE_TAGS: stats.increment_counter( ["alembic_version"], 1, tags=dict(version=actual_version, name=name) @@ -490,7 +501,7 @@ def _run_one( @staticmethod def _create_db_engine_check( binding: _Binding, - query_cb: Callable[[sqlalchemy.orm.scoping.scoped_session], None], + query_cb: Callable[[sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]], None], ) -> Tuple[str, Callable[[pyramid.request.Request], None]]: def check(request: pyramid.request.Request) -> None: with binding as session: @@ -506,8 +517,8 @@ def check(request: pyramid.request.Request) -> None: return "db_engine_" + binding.name(), check @staticmethod - def _at_least_one(model: Any) -> Callable[[sqlalchemy.orm.scoping.scoped_session], Any]: - def query(session: sqlalchemy.orm.scoping.scoped_session) -> None: + def _at_least_one(model: Any) -> Callable[[sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]], Any]: + def query(session: sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session]) -> None: result = session.query(model).first() if result is None: raise HTTPNotFound(model.__name__ + " record not found") diff --git a/c2cwsgiutils/models_graph.py b/c2cwsgiutils/models_graph.py index 12342b85f..b3a8b7aed 100644 --- a/c2cwsgiutils/models_graph.py +++ b/c2cwsgiutils/models_graph.py @@ -69,7 +69,7 @@ def _get_all_cols(symbol: Any) -> List[str]: # Those are not fields pass elif isinstance(member, sa.sql.schema.SchemaItem): - cols.append(member_name + ("[null]" if member.nullable else "")) + cols.append(member_name + ("[null]" if member.nullable else "")) # type: ignore elif isinstance(member, sa.orm.attributes.InstrumentedAttribute): nullable = ( member.property.columns[0].nullable diff --git a/c2cwsgiutils/request_tracking/_sql.py b/c2cwsgiutils/request_tracking/_sql.py index fd151b7ef..e06d78222 100644 --- a/c2cwsgiutils/request_tracking/_sql.py +++ b/c2cwsgiutils/request_tracking/_sql.py @@ -8,7 +8,9 @@ def _add_session_id(session: Session, _transaction: Any, _connection: Any) -> None: request = get_current_request() if request is not None: - session.execute("set application_name=:session_id", params={"session_id": request.c2c_request_id}) + session.execute( + sqlalchemy.text("set application_name=:session_id"), params={"session_id": request.c2c_request_id} + ) def init() -> None: diff --git a/c2cwsgiutils/scripts/stats_db.py b/c2cwsgiutils/scripts/stats_db.py index c55737aa8..8e764e462 100755 --- a/c2cwsgiutils/scripts/stats_db.py +++ b/c2cwsgiutils/scripts/stats_db.py @@ -99,16 +99,27 @@ def report_error(self) -> None: raise self._error -def do_table(session: sqlalchemy.orm.scoped_session, schema: str, table: str, reporter: Reporter) -> None: +def do_table( + session: sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session], + schema: str, + table: str, + reporter: Reporter, +) -> None: """Do the stats on a table.""" _do_table_count(reporter, schema, session, table) _do_table_size(reporter, schema, session, table) _do_indexes(reporter, schema, session, table) -def _do_indexes(reporter: Reporter, schema: str, session: sqlalchemy.orm.scoped_session, table: str) -> None: +def _do_indexes( + reporter: Reporter, + schema: str, + session: sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session], + table: str, +) -> None: for index_name, size_main, size_fsm, number_of_scans, tuples_read, tuples_fetched in session.execute( - """ + sqlalchemy.text( + """ SELECT foo.indexname, pg_relation_size(concat(quote_ident(foo.schemaname), '.', quote_ident(foo.indexrelname)), 'main'), @@ -127,7 +138,8 @@ def _do_indexes(reporter: Reporter, schema: str, session: sqlalchemy.orm.scoped_ ) AS foo ON t.tablename = foo.ctablename AND t.schemaname=foo.schemaname WHERE t.schemaname=:schema AND t.tablename=:table - """, + """ + ), params={"schema": schema, "table": table}, ): for fork, value in (("main", size_main), ("fsm", size_fsm)): @@ -147,37 +159,53 @@ def _do_indexes(reporter: Reporter, schema: str, session: sqlalchemy.orm.scoped_ def _do_table_size( - reporter: Reporter, schema: str, session: sqlalchemy.orm.scoped_session, table: str + reporter: Reporter, + schema: str, + session: sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session], + table: str, ) -> None: - size: int = 0 - (size,) = session.execute( - """ + result = session.execute( + sqlalchemy.text( + """ SELECT pg_table_size(c.oid) AS total_bytes FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace WHERE relkind = 'r' AND nspname=:schema AND relname=:table - """, + """ + ), params={"schema": schema, "table": table}, ).fetchone() + assert result is not None + size: int + (size,) = result reporter.do_report([schema, table], size, kind="size", tags=dict(schema=schema, table=table)) def _do_table_count( - reporter: Reporter, schema: str, session: sqlalchemy.orm.scoped_session, table: str + reporter: Reporter, + schema: str, + session: sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session], + table: str, ) -> None: - quote = session.bind.dialect.identifier_preparer.quote # We request and estimation of the count as a real count is very slow on big tables # and seems to cause replicatin lags. This estimate is updated on ANALYZE and VACUUM. - (count,) = session.execute( - "SELECT reltuples::bigint AS count FROM pg_class " # nosec - f"WHERE oid = '{quote(schema)}.{quote(table)}'::regclass;" + result = session.execute( + sqlalchemy.text( # nosec + "SELECT reltuples::bigint AS count FROM pg_class " + f"WHERE oid = '{sqlalchemy.sql.quoted_name(schema, True)}." + f"{sqlalchemy.sql.quoted_name(table, True)}'::regclass;" + ) ).fetchone() + assert result is not None + (count,) = result reporter.do_report([schema, table], count, kind="count", tags=dict(schema=schema, table=table)) -def do_extra(session: sqlalchemy.orm.scoped_session, extra: str, reporter: Reporter) -> None: +def do_extra( + session: sqlalchemy.orm.scoped_session[sqlalchemy.orm.Session], extra: str, reporter: Reporter +) -> None: """Do an extra report.""" - for metric, count in session.execute(extra): + for metric, count in session.execute(sqlalchemy.text(extra)): reporter.do_report(str(metric).split("."), count, kind="count", tags=dict(metric=metric)) @@ -193,12 +221,14 @@ def _do_dtats_db(args: argparse.Namespace) -> None: raise tables = session.execute( - """ + sqlalchemy.text( + """ SELECT table_schema, table_name FROM information_schema.tables WHERE table_type='BASE TABLE' AND table_schema IN :schemas - """, + """ + ), params={"schemas": tuple(args.schema)}, - ) + ).fetchall() for schema, table in tables: LOG.info("Process table %s.%s.", schema, table) try: diff --git a/c2cwsgiutils/sqlalchemylogger/_models.py b/c2cwsgiutils/sqlalchemylogger/_models.py index 4bac635c8..fd3692c4d 100644 --- a/c2cwsgiutils/sqlalchemylogger/_models.py +++ b/c2cwsgiutils/sqlalchemylogger/_models.py @@ -21,7 +21,10 @@ class Log(Base): # type: ignore level = Column(String) # info, debug, or error? trace = Column(String) # the full traceback printout msg = Column(String) # any custom log you may have included - created_at = Column(DateTime, default=func.now()) # the current timestamp + created_at = Column( # the current timestamp + DateTime, + default=func.now(), # pylint: disable=not-callable + ) def __init__(self, logger: Any = None, level: Any = None, trace: Any = None, msg: Any = None) -> None: self.logger = logger diff --git a/c2cwsgiutils/sqlalchemylogger/handlers.py b/c2cwsgiutils/sqlalchemylogger/handlers.py index f51c71b77..83bbd1575 100644 --- a/c2cwsgiutils/sqlalchemylogger/handlers.py +++ b/c2cwsgiutils/sqlalchemylogger/handlers.py @@ -98,11 +98,13 @@ def create_db(self) -> None: if not isinstance(self.Log.__table_args__, type(None)) and self.Log.__table_args__.get( "schema", None ): - if not self.engine.dialect.has_schema( - self.engine.raw_connection, self.Log.__table_args__["schema"] - ): - with self.engine.begin() as connection: - connection.execute(sqlalchemy.schema.CreateSchema(self.Log.__table_args__["schema"])) # type: ignore + with self.engine.begin() as connection: + if not self.engine.dialect.has_schema( + connection, self.Log.__table_args__["schema"] + ): + connection.execute( + sqlalchemy.schema.CreateSchema(self.Log.__table_args__["schema"]), # type: ignore + ) Base.metadata.create_all(self.engine) def emit(self, record: Any) -> None: