Source code for nexusLIMS.db.session_handler

"""Classes and methods to interact with sessions from the NexusLIMS database."""

import logging
from datetime import datetime as dt
from typing import Tuple

from sqlalchemy.orm import selectinload
from sqlmodel import Session as DBSession
from sqlmodel import select

from nexusLIMS.db.engine import get_engine
from nexusLIMS.db.enums import EventType, RecordStatus
from nexusLIMS.db.models import Instrument, SessionLog
from nexusLIMS.utils.time import current_system_tz

_logger = logging.getLogger(__name__)


[docs] class Session: """ A representation of a session in the NexusLIMS database. A record of an individual session as read from the Nexus Microscopy facility session database. Created by combining two :py:class:`~nexusLIMS.db.models.SessionLog` objects with status ``"TO_BE_BUILT"``. Parameters ---------- session_identifier The unique identifier for an individual session on an instrument instrument An object representing the instrument associated with this session dt_range A tuple of two :py:class:`~datetime.datetime` objects representing the start and end of this session )in that order user : str The username associated with this session (may not be trustworthy) """ def __init__( self, session_identifier: str, instrument: Instrument, dt_range: Tuple[dt, dt], user: str, ): self.session_identifier = session_identifier self.instrument = instrument self.dt_from, self.dt_to = dt_range self.user = user def __repr__(self): """Return custom representation of a Session.""" return ( f"{self.dt_from.isoformat()} to {self.dt_to.isoformat()} on " f"{self.instrument.name}" )
[docs] def update_session_status(self, status: RecordStatus): """ Update the status of this Session in the NexusLIMS database. Specifically, update the ``record_status`` in any session logs for this :py:class:`~nexusLIMS.db.session_handler.Session`. Parameters ---------- status : RecordStatus The new status for this session (type-safe enum value) Returns ------- success : bool Whether the update operation was successful """ with DBSession(get_engine()) as session: statement = select(SessionLog).where( SessionLog.session_identifier == self.session_identifier ) logs = session.exec(statement).all() for log in logs: log.record_status = status session.commit() return True
[docs] def insert_record_generation_event(self) -> dict: """ Insert record generation event to session log. Insert a log for this session into the session database with ``event_type`` `"RECORD_GENERATION"` and the current time (with local system timezone) as the timestamp. Returns ------- res : dict A dictionary containing the inserted log information """ _logger.debug("Logging RECORD_GENERATION for %s", self.session_identifier) log = SessionLog( session_identifier=self.session_identifier, instrument=self.instrument.instrument_pid, timestamp=dt.now(tz=current_system_tz()), event_type=EventType.RECORD_GENERATION, user="nexuslims", record_status=RecordStatus.WAITING_FOR_END, ) with DBSession(get_engine()) as session: session.add(log) session.commit() session.refresh(log) _logger.debug( "Confirmed RECORD_GENERATION insertion for %s", self.session_identifier, ) return { "id_session_log": log.id_session_log, "event_type": log.event_type.value, "session_identifier": log.session_identifier, "timestamp": log.timestamp, }
[docs] def get_sessions_to_build() -> list[Session]: """ Get list of sessions that need to be built from the NexusLIMS database. Query the NexusLIMS database for pairs of logs with status ``TO_BE_BUILT`` and return the information needed to build a record for that session. Returns ------- sessions : list[Session] A list of :py:class:`~nexusLIMS.db.session_handler.Session` objects containing the sessions that need their record built. Will be an empty list if there's nothing to do. """ sessions = [] with DBSession(get_engine()) as db_session: # Query for all TO_BE_BUILT logs with eager loading of instrument relationship statement = ( select(SessionLog) .where(SessionLog.record_status == RecordStatus.TO_BE_BUILT) .options(selectinload(SessionLog.instrument_obj)) ) session_logs = db_session.exec(statement).all() # Separate START and END logs start_logs = [sl for sl in session_logs if sl.event_type == EventType.START] end_logs = [sl for sl in session_logs if sl.event_type == EventType.END] for start_l in start_logs: # for every log that has a 'START', there should be one corresponding # log with 'END' that has the same session identifier. If not, # the database is in an inconsistent state and we should know about it el_list = [ el for el in end_logs if el.session_identifier == start_l.session_identifier ] if len(el_list) != 1: msg = ( "There was not exactly one 'END' log for this 'START' log; " f"len(el_list) was {len(el_list)}; sl was {start_l}; el_list " f"was {el_list}" ) raise ValueError(msg) end_l = el_list[0] # Use relationship to get Instrument object (eagerly loaded above) session = Session( session_identifier=start_l.session_identifier, instrument=start_l.instrument_obj, # Relationship navigation! dt_range=(start_l.timestamp, end_l.timestamp), # No fromisoformat()! user=start_l.user, ) sessions.append(session) _logger.info("Found %i new sessions to build", len(sessions)) return sessions
[docs] def get_all_session_logs() -> list[SessionLog]: """ Fetch all session logs from the database and return SessionLogs. Returns ------- session_logs : list[SessionLog] A list of all SessionLog objects from the database, ordered by timestamp. Will be an empty list if there are no session logs. """ with DBSession(get_engine()) as db_session: # Query for all session logs, ordered by timestamp statement = select(SessionLog).order_by(SessionLog.timestamp) session_logs = list(db_session.exec(statement).all()) _logger.info("Found %i session logs in database", len(session_logs)) return session_logs