Skip to content

Commit 7093cc5

Browse files
authored
Merge pull request #22 from senaite/pentra_xlr
Add Horiba Pentra XLR import schema
2 parents d185650 + 21eec83 commit 7093cc5

File tree

7 files changed

+410
-2
lines changed

7 files changed

+410
-2
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
/astm_messages/
55
/test_messages/
66
/out/
7+
.python-version

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+
- #22 Add Horiba Pentra XLR import schema
89
- #21 Add Biomérieux MINI VIDAS® import schema
910
- #20 Add Abbott Afinion™ 2 Analyzer import schema
1011
- #19 Add Spotchem™EL SE-1520 import schema

src/senaite/astm/fields.py

+19
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import datetime
77
import decimal
88
import inspect
9+
import json
910
import time
1011
import warnings
1112
from itertools import islice
@@ -108,6 +109,24 @@ def _set_value(self, value):
108109
return super(TextField, self)._set_value(value)
109110

110111

112+
class JSONListField(Field):
113+
"""Converts the value into a JSON list
114+
"""
115+
def _set_value(self, value):
116+
if not isinstance(value, list):
117+
value = [value]
118+
value = json.dumps(value)
119+
return super(JSONListField, self)._set_value(value)
120+
121+
def _get_value(self, value, default=None):
122+
if default is None:
123+
default = []
124+
try:
125+
return json.loads(value)
126+
except (TypeError, ValueError):
127+
return default
128+
129+
111130
class DateField(Field):
112131
"""Mapping field for storing date/time values.
113132
"""
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
# -*- coding: utf-8 -*-
2+
3+
from senaite.astm import records
4+
from senaite.astm.fields import ComponentField
5+
from senaite.astm.fields import DateField
6+
from senaite.astm.fields import DateTimeField
7+
from senaite.astm.fields import JSONListField
8+
from senaite.astm.fields import NotUsedField
9+
from senaite.astm.fields import SetField
10+
from senaite.astm.fields import TextField
11+
from senaite.astm.mapping import Component
12+
13+
VERSION = "1.0.0"
14+
HEADER_RX = r".*(?<=\|)(LIS|ABX)(?=\|).*"
15+
16+
17+
def get_metadata(wrapper):
18+
"""Additional metadata
19+
20+
:param wrapper: The wrapper instance
21+
:returns: dictionary of additional metadata
22+
"""
23+
return {
24+
"version": VERSION,
25+
"header_rx": HEADER_RX,
26+
}
27+
28+
29+
def get_mapping():
30+
"""Returns the wrappers for this instrument
31+
"""
32+
return {
33+
"H": HeaderRecord,
34+
"P": PatientRecord,
35+
"O": OrderRecord,
36+
"R": ResultRecord,
37+
"C": CommentRecord,
38+
"Q": RequestInformationRecord,
39+
"M": ManufacturerInfoRecord,
40+
"L": TerminatorRecord,
41+
}
42+
43+
44+
class HeaderRecord(records.HeaderRecord):
45+
"""Message Header Record (H)
46+
"""
47+
48+
# Note: Although the field comes in as a single value text, we need to nest
49+
# it for senaite.core.astm.consumer.get_sender_name to work properly
50+
sender = ComponentField(
51+
Component.build(
52+
TextField(name="name"),
53+
))
54+
55+
processing_id = SetField(
56+
field=TextField(),
57+
# P: Patient message, Q: Quality control message, D: Technician
58+
values=("P", "Q", "D"))
59+
60+
version = TextField()
61+
62+
63+
class PatientRecord(records.PatientRecord):
64+
"""Patient Information Record (P)
65+
"""
66+
67+
# Patient Id (Advised on ABX Pentra XL80 for workflow management)
68+
laboratory_id = TextField(length=25)
69+
70+
# format: Name^First name
71+
name = ComponentField(
72+
Component.build(
73+
TextField(name="name"),
74+
TextField(name="first_name"),
75+
))
76+
77+
# format: YYYYMMDD
78+
birthdate = DateField()
79+
80+
# M, F or U
81+
sex = SetField(
82+
field=TextField(),
83+
length=1,
84+
values=("M", "F", "U"))
85+
86+
87+
class OrderRecord(records.OrderRecord):
88+
"""Order Record (O)
89+
"""
90+
91+
# Note: Field 9.4.3 "Sample ID" for ABX Pentra XL80 and Pentra XLR (Only
92+
# from Instrument to Host) is presented as follows:
93+
# SampleID^Rack(2 digits max.)^TubePosition(2 digits max.), e.g.
94+
# 45264012^02^08
95+
sample_id = ComponentField(
96+
Component.build(
97+
TextField(name="sample_id"),
98+
TextField(name="rack"),
99+
TextField(name="position"),
100+
)
101+
)
102+
103+
# Note: Field 9.4.5 "Universal test ID" must be filled by the parameters
104+
# panel requested (CBC or DIF or RET or DIR or CBR):
105+
# Refer to Special characteristics for HORIBA Medical data on page 16.).
106+
test = ComponentField(
107+
Component.build(
108+
NotUsedField(name="_"),
109+
NotUsedField(name="__"),
110+
NotUsedField(name="___"),
111+
SetField(
112+
name="testname",
113+
field=TextField(),
114+
values=("CBC", "DIF", "RET", "DIR", "CBR")),
115+
))
116+
117+
# format: YYYYMMDDHHMMSS
118+
sampled_at = DateTimeField()
119+
120+
# format: YYYYMMDDHHMMSS
121+
collected_at = DateTimeField()
122+
123+
biomaterial = TextField()
124+
report_type = SetField(
125+
field=TextField(),
126+
length=1,
127+
values=("F", "C")) # F: final; C: correction
128+
129+
130+
class CommentRecord(records.CommentRecord):
131+
"""Comment Record (C)
132+
"""
133+
# I: clinical instrument system
134+
source = SetField(
135+
field=TextField(),
136+
length=1,
137+
values=("I"))
138+
139+
# Text (Refer to Table 23 - Analytical alarms, page 18; Table 24 - Analyzer
140+
# alarms, page 18; Table 25 - Suspected pathologies, page 18.
141+
#
142+
# Note: values come in either as a string or a list with variable length.
143+
# Therefore, we simply handle it as a JSON list
144+
data = JSONListField()
145+
146+
# G:Free text
147+
# I: Instrument flag comment
148+
# L: Comment from host (Patient order) P80 V1.1 and above
149+
ctype = SetField(
150+
field=TextField(),
151+
length=1,
152+
values=("G", "I", "L"))
153+
154+
155+
class ResultRecord(records.ResultRecord):
156+
"""Record to transmit analytical data.
157+
"""
158+
159+
# Note: Field 10.1.3 "Universal TestID" for ABX Pentra XL80 and Pentra XLR
160+
# includes the dilution ratio as follows: ^^^Result name in english^LOINC
161+
# code^CDR (CDR=1 or 2 or 3 or 5). Results are returned in between ().
162+
test = ComponentField(
163+
Component.build(
164+
NotUsedField(name='_'),
165+
NotUsedField(name='__'),
166+
NotUsedField(name='___'),
167+
TextField(name='result_name'),
168+
TextField(name='loinc_code'),
169+
SetField(
170+
name="testname",
171+
field=TextField(),
172+
values=("1", "2", "3", "5")),
173+
))
174+
175+
value = TextField()
176+
177+
units = SetField(
178+
field=TextField(),
179+
length=1,
180+
values=("1", "2", "3", "4"))
181+
182+
abnormal_flag = SetField(
183+
field=TextField(),
184+
length=2,
185+
values=("L", "H", "LL", "HH", ">"))
186+
187+
# W: suspicion
188+
# N: rejected result
189+
# F: final result
190+
# X: Parameter exceeding the capacity
191+
# (ABX Pentra 80 / ABX Pentra XL80 / Pentra XLR)
192+
# M: Value input manually (ABX Pentra XL80 / Pentra XLR)
193+
# D: Value obtained by dilution (ABX Pentra XL80 / Pentra XLR)
194+
status = SetField(
195+
field=TextField(),
196+
length=1,
197+
values=("W", "N", "F", "X", "M", "D"))
198+
199+
# Operator Code + Name
200+
operator = TextField()
201+
202+
completed_at = DateTimeField()
203+
204+
205+
class RequestInformationRecord(records.RequestInformationRecord):
206+
"""Request information Record (Q)
207+
"""
208+
209+
210+
class ManufacturerInfoRecord(records.ManufacturerInfoRecord):
211+
"""Manufacturer Specific Records (M)
212+
"""
213+
214+
215+
class TerminatorRecord(records.TerminatorRecord):
216+
"""Message Termination Record (L)
217+
"""

src/senaite/astm/mapping.py

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def build(cls, *a):
6969
for field in a:
7070
if field.name is None:
7171
raise ValueError('Name is required for ordered fields.')
72+
# set the definied fields as class attributes
7273
setattr(newcls, field.name, field)
7374
fields.append((field.name, field))
7475
newcls._fields = fields

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
1H|\^&|||ABX|||||||P|E1394-97|2022072712155158
2-
2P|1||||^|||U56
3-
3O|1|AUTO_SID1^00^00||^^^DIF|||||||||||Standard||||||||||F68
2+
2P|1||||Mohale^Rita||19771201|FC9
3+
3O|1|S1234^00^00||^^^DIF|||202205270000|202205260000|||||||Standard||||||||||F83
44
4R|1|^^^WBC^804-5^1|8.5|1||||W||NNE NNEMT||20220727121550E2
55
5C|1|I|Alarm_WBC^LMNE-^BASO+^LL^NL^LN^NO^SL1|ID7
66
6C|2|I|LARGE IMMATURE CELL^NRBCs|I62

0 commit comments

Comments
 (0)