"""Various utility functions used by the NEMO harvester."""
import logging
import os
import re
from datetime import datetime
from typing import Dict, List, Optional, Tuple, Union
from urllib.parse import parse_qs, urljoin, urlparse
from nexusLIMS.db.session_handler import Session
from .connector import NemoConnector
logger = logging.getLogger(__name__)
[docs]def get_harvesters_enabled() -> List[NemoConnector]:
"""
Return a list of enabled connectors based off the environment.
Returns
-------
harvesters_enabled : List[NemoConnector]
A list of NemoConnector objects representing the NEMO APIs enabled
via environment settings
"""
harvesters_enabled_str: List[str] = list(
filter(lambda x: re.search("NEMO_address", x), os.environ.keys()),
)
harvesters_enabled = [
NemoConnector(
base_url=os.getenv(addr),
token=os.getenv(addr.replace("address", "token")),
strftime_fmt=os.getenv(addr.replace("address", "strftime_fmt")),
strptime_fmt=os.getenv(addr.replace("address", "strptime_fmt")),
timezone=os.getenv(addr.replace("address", "tz")),
)
for addr in harvesters_enabled_str
]
return harvesters_enabled # noqa: RET504
[docs]def add_all_usage_events_to_db(
user: Optional[Union[str, int]] = None,
dt_from: datetime = None,
dt_to: datetime = None,
tool_id: Optional[Union[int, List[int]]] = None,
):
"""
Add all usage events to database for enabled NEMO connectors.
Loop through enabled NEMO connectors and add each one's usage events to
the NexusLIMS ``session_log`` database table (if required).
Parameters
----------
user
The user(s) for which to add usage events. If ``None``, events will
not be filtered by user at all
dt_from
The point in time after which usage events will be added. If ``None``,
no date filtering will be performed
dt_to
The point in time before which usage events will be added. If
``None``, no date filtering will be performed
tool_id
The tools(s) for which to add usage events. If ``'None'`` (default),
the tool IDs for each instrument in the NexusLIMS DB will be extracted
and used to limit the API response
"""
for nemo_connector in get_harvesters_enabled():
events = nemo_connector.get_usage_events(
user=user,
dt_range=(dt_from, dt_to),
tool_id=tool_id,
)
for event in events:
nemo_connector.write_usage_event_to_session_log(event["id"])
[docs]def get_usage_events_as_sessions(
user: Union[str, int] = None,
dt_from: datetime = None,
dt_to: datetime = None,
tool_id: Optional[Union[int, List[int]]] = None,
) -> List[Session]:
"""
Get all usage events for enabled NEMO connectors as Sessions.
Loop through enabled NEMO connectors and return each one's usage events to
as :py:class:`~nexusLIMS.db.session_handler.Session` objects without
writing logs to the ``session_log`` table. Mostly used for doing dry runs
of the record builder.
Parameters
----------
user
The user(s) for which to fetch usage events. If ``None``, events will
not be filtered by user at all
dt_from
The point in time after which usage events will be fetched. If ``None``,
no date filtering will be performed
dt_to
The point in time before which usage events will be fetched. If
``None``, no date filtering will be performed
tool_id
The tools(s) for which to fetch usage events. If ``None``, events will
only be filtered by tools known in the NexusLIMS DB for each connector
"""
sessions = []
for nemo_connector in get_harvesters_enabled():
events = nemo_connector.get_usage_events(
user=user,
dt_range=(dt_from, dt_to),
tool_id=tool_id,
)
for event in events:
this_session = nemo_connector.get_session_from_usage_event(event["id"])
# this_session could be None, and if the instrument from the
# usage event is not in our DB, this_session.instrument could
# also be None. In each case, we should ignore that one
if this_session is not None and this_session.instrument is not None:
sessions.append(this_session)
return sessions
[docs]def get_connector_for_session(session: Session) -> NemoConnector:
"""
Get the appropriate NEMO connector for a given Session.
Given a :py:class:`~nexusLIMS.db.session_handler.Session`, find the matching
:py:class:`~nexusLIMS.harvesters.nemo.connector.NemoConnector` from the enabled
list of NEMO harvesters.
Parameters
----------
session
The session for which a NemoConnector is needed
Returns
-------
n : ~nexusLIMS.harvesters.nemo.connector.NemoConnector
The connector object that allows for querying the NEMO API for the
instrument contained in ``session``
Raises
------
LookupError
Raised if a matching connector is not found
"""
instr_base_url = urljoin(session.instrument.api_url, ".")
for nemo_connector in get_harvesters_enabled():
if nemo_connector.config["base_url"] in instr_base_url:
return nemo_connector
msg = (
f"Did not find enabled NEMO harvester for "
f'"{session.instrument.name}". Perhaps check environment '
f"variables? The following harvesters are enabled: "
f"{get_harvesters_enabled()}"
)
raise LookupError(msg)
[docs]def get_connector_by_base_url(base_url: str) -> NemoConnector:
"""
Get an enabled NemoConnector by inspecting the ``base_url``.
Parameters
----------
base_url
A portion of the API url to search for
Returns
-------
n : ~nexusLIMS.harvesters.nemo.connector.NemoConnector
The enabled NemoConnector instance
Raises
------
LookupError
Raised if a matching connector is not found
"""
for nemo_connector in get_harvesters_enabled():
if base_url in nemo_connector.config["base_url"]:
return nemo_connector
msg = (
f"Did not find enabled NEMO harvester with url "
f'containing "{base_url}". Perhaps check environment '
f"variables? The following harvesters are enabled: "
f"{get_harvesters_enabled()}"
)
raise LookupError(msg)
[docs]def process_res_question_samples(
res_dict: Dict,
) -> Tuple[
Optional[List[Optional[str]]],
Optional[List[Optional[str]]],
Optional[List[Optional[str]]],
Optional[List[Optional[str]]],
]:
"""
Process sample information from reservation questions.
Parameters
----------
res_dict
The reservation dictionary (i.e. the response from the ``reservations`` api
endpoint)
"""
sample_details, sample_pid, sample_name, periodic_tables = [], [], [], []
sample_group = _get_res_question_value("sample_group", res_dict)
if sample_group is not None:
# multiple samples form will have
# res_dict['question_data']['sample_group']['user_input'] of form:
#
# _{
# _ "0": {
# _ "sample_name": "sample_pid_1",
# _ "sample_or_pid": "PID",
# _ "sample_details": "A sample with a PID and some more details"
# _ },
# _ "1": {
# _ "sample_name": "sample name 1",
# _ "sample_or_pid": "Sample Name",
# _ "sample_details": "A sample with name and some additional detail",
# _ "periodic_table": ["H", "Ti", "Cu", "Sb", "Re"]
# _ },
# _ ...
# _
# _}
# each key "0", "1", "2", etc. represents a single sample the user
# added via the "Add" button. There should always be at least one,
# since sample information is required
# the "periodic_table" key is optional, and won't be present if the
# user did not select anything in that section of the questions
for _, v in sample_group.items():
if v["sample_or_pid"].lower() == "pid":
sample_pid.append(v["sample_name"])
sample_name.append(None)
elif v["sample_or_pid"].lower() == "sample name":
sample_name.append(v["sample_name"])
sample_pid.append(None)
else:
sample_name.append(None)
sample_pid.append(None)
# as of NEMO 4.3.2, an empty textarea returns None rather than "",
# so check for None first, then test string length
if v["sample_details"] is not None and len(v["sample_details"]) > 0:
sample_details.append(v["sample_details"])
else:
sample_details.append(None)
if "periodic_table" in v:
periodic_tables.append(v["periodic_table"])
else:
periodic_tables.append(None)
else: # pragma: no cover
# non-multiple samples (old-style form) (this is deprecated,
# so doesn't need coverage since we don't have reservations in this
# style any longer)
sample_details = [_get_res_question_value("sample_details", res_dict)]
sample_pid = [None]
sample_name = [_get_res_question_value("sample_name", res_dict)]
return sample_details, sample_pid, sample_name, periodic_tables
def _get_res_question_value(value: str, res_dict: Dict) -> Optional[Union[str, Dict]]:
if "question_data" in res_dict and res_dict["question_data"] is not None:
if value in res_dict["question_data"]:
return res_dict["question_data"][value].get("user_input", None)
return None
return None
[docs]def id_from_url(url: str) -> Optional[int]:
"""
Get the value of the id query parameter stored in URL string.
This is used to extract the value as needed from API strings.
Parameters
----------
url
The URL to parse, such as
``https://nemo.url.com/api/usage_events/?id=9``
Returns
-------
this_id : None or int
The id value if one is present, otherwise ``None``
"""
query = parse_qs(urlparse(url).query)
if "id" in query:
return int(query["id"][0])
return None