Source code for nexusLIMS.harvesters.nemo

#  NIST Public License - 2021
#
#  This software was developed by employees of the National Institute of
#  Standards and Technology (NIST), an agency of the Federal Government
#  and is being made available as a public service. Pursuant to title 17
#  United States Code Section 105, works of NIST employees are not subject
#  to copyright protection in the United States.  This software may be
#  subject to foreign copyright.  Permission in the United States and in
#  foreign countries, to the extent that NIST may hold copyright, to use,
#  copy, modify, create derivative works, and distribute this software and
#  its documentation without fee is hereby granted on a non-exclusive basis,
#  provided that this notice and disclaimer of warranty appears in all copies.
#
#  THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND,
#  EITHER EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED
#  TO, ANY WARRANTY THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY
#  IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
#  AND FREEDOM FROM INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION
#  WILL CONFORM TO THE SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE
#  ERROR FREE.  IN NO EVENT SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING,
#  BUT NOT LIMITED TO, DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES,
#  ARISING OUT OF, RESULTING FROM, OR IN ANY WAY CONNECTED WITH THIS SOFTWARE,
#  WHETHER OR NOT BASED UPON WARRANTY, CONTRACT, TORT, OR OTHERWISE, WHETHER
#  OR NOT INJURY WAS SUSTAINED BY PERSONS OR PROPERTY OR OTHERWISE, AND
#  WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT OF THE RESULTS OF,
#  OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
#
"""
NEMO harvester module.

This module contains the functionality to harvest instruments, reservations,
etc. from an instance of NEMO (https://github.com/usnistgov/NEMO/), a
calendering and laboratory logistics application.
"""
import logging
from datetime import timedelta

from nexusLIMS.db.session_handler import Session
from nexusLIMS.harvesters.reservation_event import ReservationEvent
from nexusLIMS.utils import get_timespan_overlap

from .exceptions import NoDataConsentError, NoMatchingReservationError
from .utils import (
    _get_res_question_value,
    get_connector_for_session,
    id_from_url,
    process_res_question_samples,
)

logger = logging.getLogger(__name__)


[docs]def res_event_from_session(session: Session) -> ReservationEvent: """ Create reservation event from session. Create an internal :py:class:`~nexusLIMS.harvesters.reservation_event.ReservationEvent` representation of a session by finding a matching reservation in the NEMO system and parsing the data contained within into a ``ReservationEvent``. This method assumes a certain format for the "reservation questions" associated with each reservation and parses that information into the resulting ``ReservationEvent``. The most critical of these is the ``data_consent`` field. If an affirmative response in this field is not found (because the user declined consent or the reservation questions are missing), a record will not be built. The following JSON object represents a minimal schema for a set of NEMO "Reservation Questions" that will satisfy the expectations of this method. Please see the NEMO documentation on this feature for more details. .. highlight:: json .. code-block:: json [ { "type": "textbox", "name": "project_id", "title": "Project ID", }, { "type": "textbox", "name": "experiment_title", "title": "Title of Experiment", }, { "type": "textarea", "name": "experiment_purpose", "title": "Experiment Purpose", }, { "type": "radio", "title": "Agree to NexusLIMS curation", "choices": ["Agree", "Disagree"], "name": "data_consent", "default_choice": "Agree" }, { "type": "group", "title": "Sample information", "name": "sample_group", "questions": [ { "type": "textbox", "name": "sample_name", "title": "Sample Name / PID", }, { "type": "radio", "title": "Sample or PID?", "choices": ["Sample Name", "PID"], "name": "sample_or_pid", }, { "type": "textarea", "name": "sample_details", "title": "Sample Details", } ] } ] Parameters ---------- session The session for which to get a reservation event Returns ------- res_event : ~nexusLIMS.harvesters.reservation_event.ReservationEvent The matching reservation event """ # a session has instrument, dt_from, dt_to, and user # we should fetch all reservations +/- two days, and then find the one # with the maximal overlap with the session time range # probably don't want to filter by user for now, since sometimes users # will enable/reserve on behalf of others, etc. # in order to get reservations, we need a NemoConnector nemo_connector = get_connector_for_session(session) # get reservation with maximum overlap (like sharepoint_calendar.fetch_xml) reservations = nemo_connector.get_reservations( # tool id can be extracted from instrument api_url query parameter tool_id=id_from_url(session.instrument.api_url), dt_from=session.dt_from - timedelta(days=2), dt_to=session.dt_to + timedelta(days=2), ) logger.info( "Found %i reservations between %s and %s with ids: %s", len(reservations), session.dt_from - timedelta(days=2), session.dt_to + timedelta(days=2), [i["id"] for i in reservations], ) for i, res in enumerate(reservations): logger.debug( "Reservation %i: %sreservations/?id=%s from %s to %s", i + 1, nemo_connector.config["base_url"], res["id"], res["start"], res["end"], ) starts = [nemo_connector.strptime(r["start"]) for r in reservations] ends = [nemo_connector.strptime(r["end"]) for r in reservations] overlaps = [ get_timespan_overlap((session.dt_from, session.dt_to), (s, e)) for s, e in zip(starts, ends) ] # handle if there are no matching sessions (i.e. reservations is an empty list # also need to handle if there is no overlap at all with any reservation if len(reservations) == 0 or max(overlaps) == timedelta(0): # there were no reservations that matched this usage event time range, # or none of the reservations overlapped with the usage event # so we'll use what limited information we have from the usage event # session logger.warning( "No reservations found with overlap for this usage " "event, so raising NoDataConsentError", ) msg = ( "No reservation found matching this session, so assuming NexusLIMS " "does not have user consent for data harvesting." ) raise NoMatchingReservationError(msg) # select the reservation with the most overlap res = reservations[overlaps.index(max(overlaps))] logger.info( "Using reservation %sreservations/?id=%s as match for " "usage event %s with overlap of %s", nemo_connector.config["base_url"], res["id"], session.session_identifier, max(overlaps), ) # DONE: check for presence of sample_group in the reservation metadata # and change the harvester to process the sample group metadata by # providing lists to the ReservationEvent constructor ( sample_details, sample_pid, sample_name, sample_elements, ) = process_res_question_samples(res) # DONE: respect user choice not to harvest data (data_consent) consent = "disagree" consent = _get_res_question_value("data_consent", res) # consent will be None here if it wasn't given (i.e. there was no # data_consent field in the reservation questions) if consent is None: msg = ( f"Reservation {res['id']} did not have data_consent defined, " "so we should not harvest its data" ) raise NoDataConsentError(msg) if consent.lower() in ["disagree", "no", "false", "negative"]: msg = f"Reservation {res['id']} requested not to have their data harvested" raise NoDataConsentError(msg) # Create ReservationEvent from NEMO reservation dict res_event = ReservationEvent( experiment_title=_get_res_question_value("experiment_title", res), instrument=session.instrument, last_updated=nemo_connector.strptime(res["creation_time"]), username=res["user"]["username"], user_full_name=( f"{res['user']['first_name']} " f"{res['user']['last_name']} " f"({res['user']['username']})" ), created_by=res["creator"]["username"], created_by_full_name=( f"{res['creator']['first_name']} " f"{res['creator']['last_name']} " f"({res['creator']['username']})" ), start_time=nemo_connector.strptime(res["start"]), end_time=nemo_connector.strptime(res["end"]), reservation_type=None, # reservation type is not collected in NEMO experiment_purpose=_get_res_question_value("experiment_purpose", res), sample_details=sample_details, sample_pid=sample_pid, sample_name=sample_name, sample_elements=sample_elements, project_name=[None], project_id=[_get_res_question_value("project_id", res)], project_ref=[None], internal_id=str(res["id"]), division=None, group=None, url=nemo_connector.config["base_url"].replace( "api/", f'event_details/reservation/{res["id"]}/', ), ) return res_event # noqa: RET504