"""This module provides a wrapper class :class:`ConnectionPlus` around:class:`sqlite3.Connection` together with functions around it which allowperforming nested atomic transactions on an SQLite database."""from__future__importannotationsimportloggingimportsqlite3fromcontextlibimportcontextmanagerfromtypingimportTYPE_CHECKING,Anyimportwrapt# type: ignore[import-untyped]fromtyping_extensionsimportdeprecatedfromqcodes.utilsimportDelayedKeyboardInterrupt,QCoDeSDeprecationWarningifTYPE_CHECKING:fromcollections.abcimportIteratorlog=logging.getLogger(__name__)
[docs]@deprecated("ConnectionPlus is deprecated. Please use connect to create an AtomicConnection.",category=QCoDeSDeprecationWarning,)classConnectionPlus(wrapt.ObjectProxy):# pyright: ignore[reportUntypedBaseClass]""" Note this is a legacy class. Please refer to :class:`AtomicConnection` A class to extend the sqlite3.Connection object. Since sqlite3.Connection has no __dict__, we can not directly add attributes to its instance directly. It is not allowed to instantiate a new `ConnectionPlus` object from a `ConnectionPlus` object. It is recommended to create a ConnectionPlus using the function :func:`connect` """atomic_in_progress:bool=False""" a bool describing whether the connection is currently in the middle of an atomic block of transactions, thus allowing to nest `atomic` context managers """path_to_dbfile:str=""""" Path to the database file of the connection. """def__init__(self,sqlite3_connection:sqlite3.Connection):super().__init__(sqlite3_connection)ifisinstance(sqlite3_connection,ConnectionPlus):# pyright: ignore[reportDeprecated]raiseValueError("Attempted to create `ConnectionPlus` from a ""`ConnectionPlus` object which is not allowed.")self.path_to_dbfile=path_to_dbfile(sqlite3_connection)
[docs]classAtomicConnection(sqlite3.Connection):""" A class to extend the sqlite3.Connection object. This extends Connection to allow addition operations to be performed atomically. It is recommended to create an AtomicConnection using the function :func:`connect` """atomic_in_progress:bool=False""" a bool describing whether the connection is currently in the middle of an atomic block of transactions, thus allowing to nest `atomic` context managers """path_to_dbfile:str=""""" Path to the database file of the connection. """def__init__(self,*args:Any,**kwargs:Any)->None:super().__init__(*args,**kwargs)self.path_to_dbfile=path_to_dbfile(self)
@deprecated("make_connection_plus_from is deprecated. Please use connect to create an AtomicConnection",category=QCoDeSDeprecationWarning,)defmake_connection_plus_from(conn:sqlite3.Connection|ConnectionPlus,# pyright: ignore[reportDeprecated])->ConnectionPlus:# pyright: ignore[reportDeprecated]""" Makes a ConnectionPlus connection object out of a given argument. If the given connection is already a ConnectionPlus, then it is returned without any changes. Args: conn: an sqlite database connection object Returns: the "same" connection but as ConnectionPlus object """ifnotisinstance(conn,ConnectionPlus):# pyright: ignore[reportDeprecated]conn_plus=ConnectionPlus(conn)# pyright: ignore[reportDeprecated]else:conn_plus=connreturnconn_plus@contextmanagerdefatomic(conn:AtomicConnection)->Iterator[AtomicConnection]:""" Guard a series of transactions as atomic. If one transaction fails, all the previous transactions are rolled back and no more transactions are performed. NB: 'BEGIN' is by default only inserted before INSERT/UPDATE/DELETE/REPLACE but we want to guard any transaction that modifies the database (e.g. also ALTER) Args: conn: connection to guard """withDelayedKeyboardInterrupt(context={"reason":"sqlite atomic operation"}):ifnotisinstance(conn,ConnectionPlus|AtomicConnection):# pyright: ignore[reportDeprecated]raiseValueError("atomic context manager only accepts ""AtomicConnection or ConnectionPlus database connection objects.")is_outmost=not(conn.atomic_in_progress)ifconn.in_transactionandis_outmost:raiseRuntimeError("SQLite connection has uncommitted ""transactions. ""Please commit those before starting an atomic ""transaction.")old_atomic_in_progress=conn.atomic_in_progressconn.atomic_in_progress=Trueold_level=conn.isolation_leveltry:ifis_outmost:conn.isolation_level=Noneconn.cursor().execute("BEGIN")yieldconnexceptExceptionase:conn.rollback()log.exception("Rolling back due to unhandled exception")raiseRuntimeError("Rolling back due to unhandled exception")fromeelse:ifis_outmost:conn.commit()finally:ifis_outmost:conn.isolation_level=old_levelconn.atomic_in_progress=old_atomic_in_progressdeftransaction(conn:AtomicConnection,sql:str,*args:Any)->sqlite3.Cursor:"""Perform a transaction. The transaction needs to be committed or rolled back. Args: conn: database connection sql: formatted string *args: arguments to use for parameter substitution Returns: sqlite cursor """c=conn.cursor()iflen(args)>0:c.execute(sql,args)else:c.execute(sql)returncdefatomic_transaction(conn:AtomicConnection,sql:str,*args:Any)->sqlite3.Cursor:"""Perform an **atomic** transaction. The transaction is committed if there are no exceptions else the transaction is rolled back. NB: 'BEGIN' is by default only inserted before INSERT/UPDATE/DELETE/REPLACE but we want to guard any transaction that modifies the database (e.g. also ALTER). 'BEGIN' marks a place to commit from/roll back to Args: conn: database connection sql: formatted string *args: arguments to use for parameter substitution Returns: sqlite cursor """withatomic(conn)asatomic_conn:c=transaction(atomic_conn,sql,*args)returncdefpath_to_dbfile(conn:AtomicConnection|sqlite3.Connection)->str:""" Return the path of the database file that the conn object is connected to """# according to https://www.sqlite.org/pragma.html#pragma_database_list# the 3th element (1 indexed) is the pathcursor=conn.cursor()cursor.execute("PRAGMA database_list")row=cursor.fetchall()[0]returnrow[2]