Skip to content

Commit d185650

Browse files
authored
Merge pull request #21 from senaite/mini-vidas
Add Biomérieux MINI VIDAS® import schema
2 parents 9fa2d6f + 444d2b8 commit d185650

File tree

14 files changed

+363
-8
lines changed

14 files changed

+363
-8
lines changed

docs/changelog.rst

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Changelog
55
1.0.0 (unreleased)
66
------------------
77

8+
- #21 Add Biomérieux MINI VIDAS® import schema
89
- #20 Add Abbott Afinion™ 2 Analyzer import schema
910
- #19 Add Spotchem™EL SE-1520 import schema
1011
- #18 Add Siemens' DCA Vantage® Analyzer import schema

src/senaite/astm/adapters/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# -*- coding: utf-8 -*-
22

3+
from senaite.astm.adapters import biomerieux # noqa: F401
34
from senaite.astm.adapters import spotchem # noqa: F401
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from . import mini_vidas # noqa: F401
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# -*- coding: utf-8 -*-
2+
3+
import re
4+
from datetime import datetime
5+
6+
from senaite.astm import adapter_registry
7+
from senaite.astm import utils
8+
from senaite.astm.constants import ENQ
9+
from senaite.astm.constants import EOT
10+
from senaite.astm.constants import NAK
11+
from senaite.astm.interfaces import IDataHandler
12+
from senaite.astm.utils import f
13+
from senaite.astm.utils import u
14+
from zope.interface import implementer
15+
16+
RX = (
17+
rb"\x02" # Start of the message
18+
rb"(?:\x1emt(?P<mt>[^|]*))?" # Message Type (mt)
19+
rb"(?:\|\x1epi(?P<pi>[^|]*))?" # Patient Identifier (pi)
20+
rb"(?:\|\x1epn(?P<pn>[^|]*))?" # Patient Name (pn)
21+
rb"(?:\|\x1epb(?P<pb>[^|]*))?" # Patient Birthdate (pb)
22+
rb"(?:\|\x1eps(?P<ps>[^|]*))?" # Patient Sex (ps)
23+
rb"(?:\|\x1eso(?P<so>[^|]*))?" # Sample Origin (so)
24+
rb"(?:\|\x1esi(?P<si>[^|]*))?" # Specimen separator (si)
25+
rb"(?:\|\x1eci(?P<ci>[^|]*))?" # Sample Identifier (ci)
26+
rb"(?:\|\x1ert(?P<rt>[^|]*))?" # Short assay name (rt)
27+
rb"(?:\|\x1ern(?P<rn>[^|]*))?" # Long assay name (rn)
28+
rb"(?:\|\x1ett(?P<tt>[^|]*))?" # Test completion time (tt)
29+
rb"(?:\|\x1etd(?P<td>[^|]*))?" # Test completion date (td)
30+
rb"(?:\|\x1eql(?P<ql>[^|]*))?" # Qualitative Result (ql)
31+
rb"(?:\|\x1eqn(?P<qn>[^|]*))?" # Quantitative Result (qn)
32+
rb"(?:\|\x1ey3(?P<y3>[^|]*))?" # Unit associated with qn (y3)
33+
rb"(?:\|\x1eqd(?P<qd>[^|]*))?" # Dilution (qd)
34+
rb"(?:\|\x1enc(?P<nc>[^|]*))?" # Vidas flags (nc)
35+
rb"(?:\|\x1eid(?P<id>[^|]*))?" # Instrument ID (id)
36+
rb"(?:\|\x1esn(?P<sn>[^|]*))?" # Serial Number (sn)
37+
rb"(?:\|\x1em4(?P<m4>[^|]*))?" # Technologist (m4)
38+
rb"(?:\|\x1d(?P<checksum>[a-fA-F0-9]{2}))$" # Checksum
39+
)
40+
41+
42+
@implementer(IDataHandler)
43+
class DataHandler:
44+
"""Custom data handler for Biomérieux miniVidas
45+
46+
We receive from this instrument a non valid ASTM message that need to be
47+
handled differntly
48+
"""
49+
def __init__(self, protocol, data):
50+
self.protocol = protocol
51+
self.data = data
52+
53+
def can_handle(self):
54+
return re.match(RX, self.data) is not None
55+
56+
def to_timestamp(self, date, time):
57+
"""Make a timestamp from the date and time
58+
"""
59+
dt = datetime.now()
60+
if date:
61+
dt = datetime.strptime(u(date), '%m/%d/%y')
62+
if time:
63+
t = datetime.strptime(u(time), '%H:%M').time()
64+
dt = datetime.combine(dt, t)
65+
return dt.strftime("%Y%m%d%H%M%S")
66+
67+
def handle_data(self):
68+
"""Create a valid ASTM message of the received data
69+
70+
1. Create a static header record
71+
2. Create a results record with the given data
72+
3. Create a termination record
73+
74+
"""
75+
parts = re.match(RX, self.data)
76+
if not parts:
77+
return NAK
78+
79+
# initialize the communication if we're not already in transfer state
80+
# Note: This is mainly a test fixture for the simulator
81+
if not self.protocol.in_transfer_state:
82+
self.protocol.on_enq(ENQ)
83+
84+
data = {}
85+
for k, v in parts.groupdict().items():
86+
data[k] = u(v) if v else ""
87+
88+
# convert date and time to a timestamp
89+
date = data.get("td")
90+
time = data.get("tt")
91+
data["ts"] = self.to_timestamp(date, time)
92+
93+
frames = [
94+
f("1H|\\^&|||miniVidas^biomerieux^1.0.0|||||||||{ts}{CR}{ETX}",
95+
**data),
96+
f("2P|1|||{pi}|{pn}||{pb}|{ps}||||||||||||||||||||||||||{CR}{ETX}",
97+
**data),
98+
f("3O|1|{ci}||{rn}||||||||||||||||||{ts}||||||||{CR}{ETX}",
99+
**data),
100+
f("4R|1|{rt}|{qn}|||{nc}||{ql}||{m4}||{ts}|{CR}{ETX}",
101+
**data),
102+
f("5L|1|N{CR}{ETX}"),
103+
]
104+
messages = []
105+
for frame in frames:
106+
cs = utils.make_checksum(frame)
107+
messages.append(
108+
f("{STX}{frame}{cs}{CRLF}", frame=u(frame), cs=u(cs)))
109+
110+
# fill in the full message
111+
self.protocol.messages = messages
112+
113+
# end the communicaiton
114+
self.protocol.on_eot(EOT)
115+
116+
117+
# register the adapter
118+
adapter_registry.registerAdapter(
119+
DataHandler,
120+
required=(object, object),
121+
provided=IDataHandler,
122+
name="mini_vidas",
123+
)

src/senaite/astm/adapters/spotchem/se1520.py

+1
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,5 @@ def handle_data(self):
103103
DataHandler,
104104
required=(object, object),
105105
provided=IDataHandler,
106+
name="spotchem_se1520",
106107
)

src/senaite/astm/codec.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def decode_message(message, encoding=ENCODING):
9595
frame, cs = frame_cs[:-2], frame_cs[-2:]
9696
# validate the checksum
9797
ccs = make_checksum(frame)
98-
assert cs == ccs, "Checksum failure: expected %r, got %r" % (cs, ccs)
98+
assert cs.upper() == ccs, "Checksum wrong: expected %r, got %r" % (cs, ccs)
9999
seq, records = decode_frame(frame, encoding)
100100
return seq, records, cs.decode()
101101

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from senaite.astm import records
4+
from senaite.astm.fields import ComponentField, DateField
5+
from senaite.astm.fields import DateTimeField
6+
from senaite.astm.fields import TextField
7+
from senaite.astm.mapping import Component
8+
9+
VERSION = "1.0.0"
10+
# Supports Biomérieux miniVidas
11+
HEADER_RX = r".*miniVidas\^"
12+
13+
14+
def get_metadata(wrapper):
15+
"""Additional metadata
16+
17+
:param wrapper: The wrapper instance
18+
:returns: dictionary of additional metadata
19+
"""
20+
return {
21+
"version": VERSION,
22+
"header_rx": HEADER_RX,
23+
}
24+
25+
26+
def get_mapping():
27+
"""Returns the wrappers for this instrument
28+
"""
29+
return {
30+
"H": HeaderRecord,
31+
"P": PatientRecord,
32+
"O": OrderRecord,
33+
"R": ResultRecord,
34+
"L": TerminatorRecord,
35+
}
36+
37+
38+
class HeaderRecord(records.HeaderRecord):
39+
"""Message Header Record (H)
40+
"""
41+
sender = ComponentField(
42+
Component.build(
43+
TextField(name="name"),
44+
TextField(name="manufacturer", default="Biomerieux"),
45+
TextField(name="version"),
46+
))
47+
timestamp = DateTimeField()
48+
49+
50+
class PatientRecord(records.PatientRecord):
51+
"""Patient Information Record (P)
52+
53+
This record is used to transfer patient information to the analyzer (test
54+
order messages) or to the host (result messages).
55+
"""
56+
name = TextField()
57+
birthdate = DateField()
58+
sex = TextField()
59+
60+
61+
class OrderRecord(records.OrderRecord):
62+
63+
sample_id = TextField()
64+
test = TextField()
65+
reported_at = DateTimeField()
66+
67+
68+
class ResultRecord(records.ResultRecord):
69+
"""Record to transmit analytical data.
70+
"""
71+
test = TextField()
72+
value = TextField()
73+
status = TextField()
74+
completed_at = DateTimeField()
75+
76+
77+
class TerminatorRecord(records.TerminatorRecord):
78+
"""Message Termination Record (L)
79+
"""

src/senaite/astm/protocol.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,10 @@ def handle_data(self, data):
116116
"""Process incoming data
117117
"""
118118
# lookup custom multi-adapter to handle the data
119-
adapter = adapter_registry.queryMultiAdapter(
120-
(self, data), IDataHandler)
121-
if adapter and adapter.can_handle():
122-
return adapter.handle_data()
119+
adapters = adapter_registry.getAdapters((self, data), IDataHandler)
120+
for name, adapter in adapters:
121+
if adapter and adapter.can_handle():
122+
return adapter.handle_data()
123123

124124
response = None
125125
if data.startswith(ENQ):

src/senaite/astm/tests/base.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
from senaite.astm.constants import ACK
99
from senaite.astm.constants import ENQ
1010

11+
# Ignore invalid ASTM files that are handled by and adapter (see PR #19)
1112
IGNORE_INSTRUMENT_FILES = [
12-
"spotchem_el.txt" # Not valid ASTM (see PR #19)
13+
"spotchem_el.txt",
14+
"mini_vidas.txt",
1315
]
1416

1517

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mtrsl|pi|pn|si|ciZ1G021SCR|rtHBCT|rnAnti-HBc Total II|tt18:35|td10/25/24|qlPositif|qn0.05|b0

src/senaite/astm/tests/data/mini_vidas_mock.txt

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
1H|\\^&|||miniVidas^biomerieux^1.0.0|||||||||2024102518350058
2+
2P|1||||test_patient|||||||||||||||||||||||||||||4F
3+
3O|1|Z1G021SCR||Anti-HBc Total II||||||||||||||||||20241025183500||||||||D5
4+
4R|1|HBCT|0.05|||||Positif||||20241025183500|96
5+
5L|1|N08

0 commit comments

Comments
 (0)