""" @package pmsco.database.access wrapper classes for access to a pmsco database the most import class to be used is DatabaseAccess. usage: ~~~~~~{.py} db = DatabaseAccess() db.connect("file.db") with db.session(): # database access here # ... # commit transaction session.commit() # continue in new transaction # at the end of the context # the session is closed and orm objects are detached from the database. ~~~~~~ @author Matthias Muntwiler, matthias.muntwiler@psi.ch @copyright (c) 2016-21 by Paul Scherrer Institut @n Licensed under the Apache License, Version 2.0 (the "License"); @n you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 """ import fasteners import logging from pathlib import Path import pmsco.database.orm as orm logger = logging.getLogger(__name__) class _DummyLock(object): """ dummy lock used for in memory database. """ def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): pass class LockedSession(object): """ database session context manager this context manager (to be used in a with statement) acquires a lock on the database lock file and provides a database session (orm.Session()). the session is closed (and pending transactions committed) on exit. if an exception occurs, pending transactions are rolled back before the session is closed. @note the term _session_ refers to a session in sqlalchemy. """ def __init__(self, lock_file=None): self.lock_file = lock_file self._session = None self._lock = None def __enter__(self): self._lock = self.lock() self._lock.__enter__() self._session = orm.Session() return self._session def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: self._session.close() else: self._session.rollback() self._session.close() self._lock.__exit__(exc_type, exc_val, exc_tb) self._lock = None def lock(self): """ create a file-lock context manager for the database. this is either a fasteners.InterProcessLock object on self._lock_filename or a _DummyLock object if the database is in memory. InterprocessLock allows to serialize access to the database by means of a lock file. this is necessary if multiple pmsco instances require access to the same database. _DummyLock is used with an in-memory database which does not require locking. the lock object can be used as context-manager in a with statement. """ if self.lock_file: return fasteners.InterProcessLock(self.lock_file) else: return _DummyLock() class DatabaseAccess(object): """ basic database connection this class maintains a database connection and builds session objects. a _session_ corresponds to an sqlalchemy session, which defines the lifecycle of mapped objects. a session can open one or multiple (subsequent) transactions. usage: ~~~~~~{.py} db = DatabaseAccess() db.connect("file.db") with db.session(): # database access session.commit() ~~~~~~ the session object is a context handler. it commits the transaction and closes the session at the end of the context. if an exception occurs, it rolls back the transaction and closes the session before passing the exception. """ def __init__(self): self.db_file = "" self.lock_file = "" def connect(self, db_file, lock_file=""): """ connect to a new or existing database file. if the file does not exist, or if it is empty, a new database schema is created. @param db_file: name of a file or ":memory:" for an in-memory database. @param lock_file: name of a file that is used to lock the database. by default, the db_filename with a suffix of ".lock" is used. for most uses, the default should be fine. the argument is provided mainly for testing the locking functionality. this must be a file that is not used for anything else. the file does not need to exist. it's best if the file is in the same directory as the database file. all clients of a database must use the same lock file. @return: None """ self.db_file = db_file if lock_file: self.lock_file = lock_file elif db_file == ":memory:": self.lock_file = "" else: self.lock_file = Path(str(db_file) + ".lock") orm.connect(orm.sqlite_link(self.db_file)) def session(self): """ open a database session. this function returns a pmsco.database.util.LockedSession object which is a context handler that provides an sqlalchemy session that is locked against concurrent access from other DatabaseAccess instances. see the class description for an example usage pattern. @return: pmsco.database.util.LockedSession() object. """ return LockedSession(self.lock_file)