Source code for buildingmotif.building_motif.building_motif

import logging
import os
from contextlib import contextmanager
from typing import Optional

from rdflib import Graph
from rdflib.namespace import NamespaceManager
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker

from buildingmotif.building_motif.singleton import (
    Singleton,
    SingletonNotInstantiatedException,
)
from buildingmotif.database.graph_connection import GraphConnection
from buildingmotif.database.table_connection import TableConnection
from buildingmotif.database.tables import Base as BuildingMOTIFBase
from buildingmotif.database.utils import (
    _custom_json_deserializer,
    _custom_json_serializer,
)
from buildingmotif.namespaces import bind_prefixes


[docs]class BuildingMOTIF(metaclass=Singleton): """Manages BuildingMOTIF data classes.""" def __init__( self, db_uri: str, shacl_engine: Optional[str] = "pyshacl", log_level=logging.WARNING, ) -> None: """Class constructor. :param db_uri: database URI :type db_uri: str :param shacl_engine: the name of the engine to use for validation: "pyshacl" or "topquadrant". Using topquadrant requires Java to be installed on this machine, and the "topquadrant" feature on BuildingMOTIF, defaults to "pyshacl" :type shacl_engine: str, optional :param log_level: logging level of detail :type log_level: int :default log_level: INFO """ self.db_uri = db_uri self.shacl_engine = shacl_engine self.engine = create_engine( db_uri, echo=False, json_serializer=_custom_json_serializer, json_deserializer=_custom_json_deserializer, ) self.session_factory = sessionmaker(bind=self.engine, autoflush=True) self.Session = scoped_session(self.session_factory) self.setup_logging(log_level) # setup tables automatically if using a in-memory sqlite database if self._is_in_memory_sqlite(): self.setup_tables() self.table_connection = TableConnection(self.engine, self) self.graph_connection = GraphConnection( BuildingMotifEngine(self.engine, self.Session) ) g = Graph() bind_prefixes(g) self.template_ns_mgr: NamespaceManager = NamespaceManager(g) @property def session(self): return self.Session()
[docs] def setup_tables(self): """Creates all tables in the underlying database.""" BuildingMOTIFBase.metadata.create_all(self.engine)
def _is_in_memory_sqlite(self) -> bool: """Returns true if the BuildingMOTIF instance uses an in-memory SQLite database. """ if self.engine.dialect.name != "sqlite": return False # get the 'filename' of the database; if this is empty, the db is in-memory raw_conn = self.engine.raw_connection() filename = ( raw_conn.cursor() .execute("select file from pragma_database_list where name='main';", ()) .fetchone() ) # length is 0 if the db is in-memory return not len(filename[0])
[docs] def setup_logging(self, log_level): """Create log file with DEBUG level and stdout handler with specified logging level. :param log_level: logging level of detail :type log_level: int """ root_logger = logging.getLogger() root_logger.setLevel(logging.DEBUG) formatter = logging.Formatter( "%(asctime)s | %(name)s | %(levelname)s: %(message)s" ) log_file_handler = logging.FileHandler( os.path.join(os.getcwd(), "BuildingMOTIF.log"), mode="w" ) log_file_handler.setLevel(logging.DEBUG) log_file_handler.setFormatter(formatter) engine_logger = logging.getLogger("sqlalchemy.engine") pool_logger = logging.getLogger("sqlalchemy.pool") engine_logger.setLevel(logging.WARN) pool_logger.setLevel(logging.WARN) stream_handler = logging.StreamHandler() stream_handler.setLevel(log_level) stream_handler.setFormatter(formatter) root_logger.addHandler(log_file_handler) root_logger.addHandler(stream_handler)
[docs] def close(self) -> None: """Close session and engine.""" self.session.close() self.engine.dispose()
[docs]def get_building_motif() -> "BuildingMOTIF": """Returns singleton instance of BuildingMOTIF. Requires that BuildingMOTIF has been instantiated before, otherwise raises an exception. :raises SingletonNotInstantiatedException: if buildingmotif hasn't been instantiated :return: singleton instance of buildingmotif :rtype: BuildingMOTIF """ if hasattr(BuildingMOTIF, "instance"): return BuildingMOTIF.instance # type: ignore raise SingletonNotInstantiatedException
[docs]class BuildingMotifEngine: """BuildingMotifEngine is a class that wraps a SQLAlchemy Engine and Session. This enables the use of sessioned transactions in rdflib-sqlalchemy. If we are experiencing weird graph database issues this may be the cause. """ def __init__(self, engine, Session) -> None: self.engine = engine self.Session = Session # begin and connect attributes are queried from the wrapped session.
[docs] @contextmanager def begin(self): yield self.Session()
[docs] @contextmanager def connect(self): yield self.Session()
def __getattr__(self, attr): # When an attribute is requested, see if we have overriden it # If we have not return the attr of the wrapped engine if attr in self.__dict__: return getattr(self, attr) return getattr(self.engine, attr)