Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add LIS2 request information functionality #3

Draft
wants to merge 9 commits into
base: 2.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
Changelog
=========

2.0.0 (unreleased)
2.0.1 (unreleased)
------------------

- Fixed broken tests
- Add LIS2-A2 Request Information capabilities


2.0.0
------------------

- Compatibility with SENAITE 2.x and senaite.astm
Expand Down
5 changes: 2 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
from setuptools import setup, find_packages

version = "2.0.0"
version = "2.0.1"

setup(
name="senaite.lis2a",
version=version,
description="CLIS LIS2-A results import for SENAITE",
long_description=open("README.rst").read() + "\n" +
open("CHANGES.rst").read() + "\n",
long_description=open("README.rst").read() + "\n" + open("CHANGES.rst").read() + "\n",
# Get more strings from
# http://pypi.python.org/pypi?:action=list_classifiers
classifiers=[
Expand Down
6 changes: 5 additions & 1 deletion src/senaite/lis2a/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from Products.CMFCore.interfaces import IContentish
from senaite.jsonapi.interfaces import IPushConsumer
from senaite.lis2a import api as _api
from senaite.lis2a import logger
from senaite.lis2a.api import message as msgapi
from zope.component import adapts
from zope.interface import implements
Expand All @@ -32,7 +33,7 @@
from senaite.queue.interfaces import IQueuedTaskAdapter
from senaite.queue import api as queueapi
from senaite.queue.queue import get_chunks_for
except:
except Exception:
IQueuedTaskAdapter = Interface
queueapi = None

Expand All @@ -43,6 +44,7 @@ class PushConsumer(object):
implements(IPushConsumer)

def __init__(self, data):
logger.info("PushConsumer: init with data {}".format(data))
self.data = data

def process(self):
Expand All @@ -57,6 +59,7 @@ def process(self):
if not messages:
raise ValueError("No messages found: {}".format(repr(self.data)))

logger.info("PushConsumer: process message {}".format(messages))
# Just in case we got a message instead of a list of messages
if isinstance(messages, six.string_types):
messages = (messages,)
Expand All @@ -80,6 +83,7 @@ def process(self):
# returns True if found a match in SENAITE and succeed on the result
# import. It might happen there is no object in SENAITE matching with
# the message we are trying to import.
logger.info("PushConsumer: process complete")
return True


Expand Down
34 changes: 26 additions & 8 deletions src/senaite/lis2a/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,15 @@

from os.path import splitext
from os.path import isfile
from os.path import join

import analysis as anapi
import json
import message as msgapi
import query as query_api
import six
from bika.lims import api
from pkg_resources import resource_filename
from pkg_resources import resource_listdir
from plone.resource.utils import iterDirectoriesOfType
from senaite.lis2a import PRODUCT_NAME
from senaite.lis2a import logger
from senaite.lis2a.interpreter import Interpreter
from senaite.lis2a.interpreter import lis2a2

Expand Down Expand Up @@ -100,12 +98,19 @@ def import_message(message):
if not interpreter:
raise ValueError("No interpreter found for {}".format(message))

# Determine if resuls or query
# Extract and import (R)esults
results = extract_results(message, interpreter)
imported = any(map(anapi.import_result, results))

# Extract and import other data
return imported
if results:
imported = any(map(anapi.import_result, results))
# Extract and import other data
logger.info('Imported results: {}'.format(imported))
return imported
results = extract_queries(message, interpreter)
if results:
processed = query_api.process_query(results)
logger.info('Processed query: {}'.format(processed))
return processed


def extract_results(message, interpreter):
Expand Down Expand Up @@ -200,3 +205,16 @@ def get_interpreter_from_json(str_or_file):
with open(str_or_file, "r") as f:
str_or_file = f.read()
return Interpreter(json.loads(str_or_file))


def extract_queries(message, interpreter):
"""Returns a list of query dicts. A given message can contain multiple
queries, so it returns a list of dicts, and each dict
represents a query
"""
logger.info('extract_queries message in: {}'.format(message))
interpreter.read(message)
query_data = interpreter.get_queries_data()
interpreter.close()
logger.info('extract_queries returns {}'.format(query_data))
return query_data
70 changes: 70 additions & 0 deletions src/senaite/lis2a/api/query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
#
# This file is part of SENAITE.LIS2A.
#
# SENAITE.LIS2A is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 51
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Copyright 2020 by it's authors.
# Some rights reserved, see README and LICENSE.


from senaite.lis2a import logger
from analysis import search_analysis_container

_marker = object()


def process_query(data):
"""Tries to process an query
:param data: dict representation of a query, suitable for excecution

data = [{
'specimen_id': 'AP-0001',
'id': '1',
'keyword': ['ALL'],
'patient_id': 'HH-23-P000001'
}]

"specimen_id" is used to identify to the sample/analysis_request
"""
for query in data:
specimen_id = query.get("specimen_id")
keyword = query.get("keyword")
if not all([keyword, specimen_id]):
logger.error("specifmen_id or keyword are missing or empty")
continue

logger.info("Start processing {} {}".format(specimen_id, keyword))

# Look for matches
sample = search_analysis_container([specimen_id])
if not sample:
logger.error("no match found for speciment {}"
.format(repr(specimen_id)))
return False

analyses = sample.getAnalyses()
if len(analyses) == 0:
logger.error("No analyses found in sample {}".format(sample.getId()))
continue

analyses = [an for an in analyses if an.review_state not in ['retracted', ]]
logger.info("Found {} active analysis".format(len(analyses)))

# Construct ASTM order to send to instrument

logger.info("Preceesed {} {}".format(specimen_id, keyword))

logger.info("process_query complete")
return True
104 changes: 101 additions & 3 deletions src/senaite/lis2a/interpreter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import itertools
from datetime import datetime

import lis2a2
import six
from senaite.lis2a.api import message as msgapi

Expand Down Expand Up @@ -120,6 +119,13 @@ def get_message_values(self, key):
def get_record_value(self, key, record):
"""Returns the value for the key passed-in from the record, if any
"""
# Get the key of the record to look at
record_type = self.get_record_type(key)

# Ensure the record type if the key matches the record type
if not record.startswith(record_type):
return

delimiters = {
"field_delimiter": self.field_delimiter,
"component_delimiter": self.component_delimiter,
Expand Down Expand Up @@ -207,7 +213,8 @@ def check_result_criteria(self, record):
for key, expected_value in self.result_criteria.items():

if self.get_record_type(key) != "R":
raise ValueError("Records other than Result are not supported")
# Records other than Result are not supported here
continue

# Get the real value from the record
value = self.get_record_value(key, record)
Expand Down Expand Up @@ -402,7 +409,98 @@ def to_date(self, ansi_str, default=_marker):

try:
return datetime.strptime(ansi_str, date_format)
except:
except Exception:
if default is _marker:
raise ValueError("No ANSI format date")
return default

def find_query_records(self):
"""Return the query records that match with the result_criteria
"""
query_records = msgapi.get_records(self.message, "Q")
return filter(self.check_query_criteria, query_records)

def get_queries_data(self):
"""Returns a list of dicts containing the query data information to
store from the message passed-in based on the configuration set for this
interpreter.
"""
# Extract the query records that match with the result_criteria
query_records = self.find_query_records()
if not query_records:
return {}

querys_data = []
for record in query_records:

# Generate the query data dict
data = self.to_query_data(record)
if data:
# Append to the list of querys data
querys_data.append(data)

return querys_data

def check_query_criteria(self, record):
"""Returns whether the record passed in matches with the query
criteria specified by this interpreter
"""
if not self.result_criteria:
raise ValueError("No query criteria set")

# The interpreter can handle the record if all criteria are met
for key, expected_value in self.result_criteria.items():

if self.get_record_type(key) != "Q":
# Records other than Result are not supported here
continue

# Get the real value from the record
value = self.get_record_value(key, record)

# Check if value matches with the expected value
if not self.match(value, expected_value):
return False

return True

def get_sequnce_number(self, record):
sequence_num = self.get_mapped_values("id", record)
if sequence_num:
sequence_num = sequence_num[0]
return sequence_num

def get_patient_id(self, record):
patient_id = self.get_mapped_values("patient_id", record)
if patient_id:
patient_id = patient_id[0]
return patient_id

def get_specimen_id(self, record):
specimen_id = self.get_mapped_values("specimen_id", record)
if specimen_id:
specimen_id = specimen_id[0]
return specimen_id

def to_query_data(self, record):
"""Returns a dict representing the query data information
"""

def resolve_query_mappings(record):
return {
"id": self.get_sequnce_number(record),
"patient_id": self.get_patient_id(record),
"specimen_id": self.get_specimen_id(record),
"keyword": self.get_analysis_keywords(record),
}

# Resolve mappings for result
data = resolve_query_mappings(record)

# id and keyword are required
if not all([data["id"]]) and not any([data["patient_id"], data["specimen_id"]]):
return {}

# Inject additional options (e.g. remove_interims)
data.update(self.get("options", {}))
return data
Loading