malac-utils 1.3.3__tar.gz → 1.4.1a74__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: malac-utils
3
- Version: 1.3.3
3
+ Version: 1.4.1a74
4
4
  Summary: Mapping Language Compiler Utils
5
5
  Author-email: cdeHealth-Team <contact-project+cdehealth-malac-hd-52276676-issue-@incoming.gitlab.com>
6
6
  Project-URL: Homepage, https://gitlab.com/cdehealth/malac-hd
@@ -23,7 +23,8 @@ Classifier: Topic :: Software Development :: Libraries
23
23
  Requires-Python: >=3.10
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
- Requires-Dist: python-dateutil>=2.8.2
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest<10,>=9; extra == "test"
27
28
  Dynamic: license-file
28
29
 
29
30
  # MaLaC Utils
@@ -32,17 +33,13 @@ MaLaC-HD (MApping LAnguage Compiler for Health Data) is a tool that you can use
32
33
 
33
34
  This is the utils package to execute generated mappings. You can use it independently from the `malac-hd` package, together with the appropriate model packages.
34
35
 
35
- [TOC]
36
+ It contains the Python modules mappings compiled to Python use (`malac.utils`), and ships the runtimes of mappings compiled to JavaScript, Java and C#, which malac-hd runs them with:
36
37
 
37
- ## Contributing and Support
38
+ - `malac/utils/js/malac-utils.js`, the JavaScript runtime as one self-contained file for Node.js and web browsers (also published to npm as [`malac-utils`](https://www.npmjs.com/package/malac-utils)),
39
+ - `malac/utils/java/malac-utils.jar`, the Java runtime as one self-contained jar (also published to Maven Central as `io.gitlab.cdehealth:malac-utils`),
40
+ - `malac/utils/dotnet/malac-utils.dll`, the .NET runtime (also published to NuGet as [`Malac.Utils`](https://www.nuget.org/packages/Malac.Utils)).
38
41
 
39
- Please read [CONTRIBUTING.md](CONTRIBUTING.md) for contributing to cdeHealth projects which are hosted in the [cdeHealth group](https://gitlab.com/cdehealth) on GitLab.com.
40
- Please read [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) to make participation in our community a harassment free experience for everyone.
41
-
42
- ## Authors and acknowledgment
43
- We want to thank
44
- - [ELGA GmbH](https://www.elga.gv.at/) with their [CDA2FHIR](https://collab.hl7.at/display/BAL/AG+ELGA+CDA+Laborbefund+zu+FHIR) projects and
45
- - [AIT Austrian Institute of Technology GmbH](https://www.ait.ac.at/) with their [SmartFOX](https://www.smart-fox.at/) project.
42
+ See the [repository](https://gitlab.com/cdehealth/malac/utils) for their sources and how to build them.
46
43
 
47
44
  ## License
48
- This is a LGPL licensed project.
45
+ This is a LGPL licensed project (LGPL-3.0-only).
@@ -0,0 +1,16 @@
1
+ # MaLaC Utils
2
+
3
+ MaLaC-HD (MApping LAnguage Compiler for Health Data) is a tool that you can use to convert mappings between different health data formats to executable code. It can also be used as a library to dynamically execute mappings.
4
+
5
+ This is the utils package to execute generated mappings. You can use it independently from the `malac-hd` package, together with the appropriate model packages.
6
+
7
+ It contains the Python modules mappings compiled to Python use (`malac.utils`), and ships the runtimes of mappings compiled to JavaScript, Java and C#, which malac-hd runs them with:
8
+
9
+ - `malac/utils/js/malac-utils.js`, the JavaScript runtime as one self-contained file for Node.js and web browsers (also published to npm as [`malac-utils`](https://www.npmjs.com/package/malac-utils)),
10
+ - `malac/utils/java/malac-utils.jar`, the Java runtime as one self-contained jar (also published to Maven Central as `io.gitlab.cdehealth:malac-utils`),
11
+ - `malac/utils/dotnet/malac-utils.dll`, the .NET runtime (also published to NuGet as [`Malac.Utils`](https://www.nuget.org/packages/Malac.Utils)).
12
+
13
+ See the [repository](https://gitlab.com/cdehealth/malac/utils) for their sources and how to build them.
14
+
15
+ ## License
16
+ This is a LGPL licensed project (LGPL-3.0-only).
@@ -0,0 +1,2 @@
1
+
2
+ __version__ = "1.4.1-alpha.74"
@@ -0,0 +1,61 @@
1
+ import re
2
+ import datetime
3
+
4
+ # FHIR date, dateTime and instant as well as FHIRPath date and dateTime literals: YYYY, YYYY-MM
5
+ # or YYYY-MM-DD, followed by T and hh, hh:mm or hh:mm:ss with optional fractional seconds and an
6
+ # offset Z or +/-hh:mm up to 14:00, see https://hl7.org/fhir/datatypes.html#primitive and
7
+ # https://hl7.org/fhirpath/#datetime (also a space as separator, as str(datetime) gives)
8
+ FHIR_PATTERN = re.compile(r"^([0-9]{4})(?:-([0-9]{2})(?:-([0-9]{2}))?)?(?:[T ]([0-9]{2})(?::([0-9]{2})(?::([0-9]{2})(?:\.([0-9]+))?)?)?(Z|[+-][0-9]{2}:?[0-9]{2})?|T)?$")
9
+
10
+ # CDA TS: any portion from YYYY to YYYYMMDDhhmmss, fractional seconds only if precise
11
+ # to the second, and an optional offset +/-HHMM, see
12
+ # https://build.fhir.org/ig/HL7/CDA-core-2.0/StructureDefinition-ts-simple.html
13
+ TS_PATTERN = re.compile(r"^([0-9]{4})(?:([0-9]{2})(?:([0-9]{2})(?:([0-9]{2})(?:([0-9]{2})(?:([0-9]{2})(?:\.([0-9]+))?)?)?)?)?)?([+-][0-9]{1,4})?$")
14
+
15
+ # FHIR permits offsets from -14:00 to +14:00, which also bounds CDA offsets mapped to FHIR
16
+ MAX_OFFSET = datetime.timedelta(hours=14)
17
+
18
+ class PartialDate:
19
+
20
+ def __init__(self, value):
21
+ self.value = value
22
+
23
+ def isoformat(self):
24
+ return self.value
25
+
26
+ def __repr__(self):
27
+ return self.value
28
+
29
+ def _tzinfo(offset):
30
+ if not offset:
31
+ return None
32
+ if offset == "Z":
33
+ return datetime.timezone.utc
34
+ digits = offset[1:].replace(":", "")
35
+ hours, minutes = (digits, 0) if len(digits) <= 2 else (digits[:-2], digits[-2:])
36
+ delta = datetime.timedelta(hours=int(hours), minutes=int(minutes))
37
+ if int(minutes) > 59 or delta > MAX_OFFSET:
38
+ raise ValueError("Invalid offset: %s" % offset)
39
+ return datetime.timezone(-delta if offset[0] == "-" else delta)
40
+
41
+ def _datetime(year, month, day, hour, minute, second, fraction, offset):
42
+ tzinfo = _tzinfo(offset)
43
+ microsecond = int(fraction[:6].ljust(6, "0")) if fraction else 0 # datetime supports microseconds only
44
+ return datetime.datetime(int(year), int(month), int(day), int(hour or 0), int(minute or 0), int(second or 0), microsecond, tzinfo)
45
+
46
+ def parse(datestr):
47
+ datestr = getattr(datestr, "value", datestr).strip()
48
+ if (match := FHIR_PATTERN.match(datestr)) and (match.group(3) or not match.group(4)): # time only with a full date
49
+ year, month, day = match.groups()[:3]
50
+ if not day:
51
+ return PartialDate("-".join(part for part in (year, month) if part))
52
+ return _datetime(*match.groups())
53
+ elif match := TS_PATTERN.match(datestr):
54
+ year, month, day, hour = match.groups()[:4]
55
+ if not hour:
56
+ # date precision, an offset is not representable here but must still be valid
57
+ _tzinfo(match.group(8))
58
+ return PartialDate("-".join(part for part in (year, month, day) if part))
59
+ return _datetime(*match.groups())
60
+ else:
61
+ raise ValueError("Unknown date format: %s" % datestr)
@@ -0,0 +1,10 @@
1
+ """
2
+ Exception hierarchy for MaLaC-HD-generated code
3
+ """
4
+
5
+ class MappingExecutionError(Exception):
6
+ """Base class for errors raised while executing a mapping."""
7
+
8
+
9
+ class CheckFailedError(MappingExecutionError):
10
+ """Error in case a check clause failed."""
@@ -7,6 +7,7 @@ import math
7
7
  from urllib.parse import urlparse
8
8
 
9
9
  from . import date as dateutil
10
+ from . import html
10
11
 
11
12
  time_units = ["year", "years", "month", "months", "week", "weeks", "day", "days", "hour", "hours", "minute", "minutes", "second", "seconds", "millisecond", "milliseconds"]
12
13
 
@@ -100,6 +101,8 @@ class FHIRPathUtils(object):
100
101
  }
101
102
 
102
103
  def get(self, val, *attrs, **options):
104
+ if len(attrs) == 1 and options.get("strip", False):
105
+ return [html.strip_tags(val)]
103
106
  attr = attrs[0]
104
107
  try:
105
108
  result = getattr(val, attr) or []
@@ -119,10 +122,6 @@ class FHIRPathUtils(object):
119
122
  prefix = getattr(val, key)
120
123
  if prefix and attr.lower() == (prefix + key[:-10]).lower():
121
124
  result = getattr(val, key[:-10]) or []
122
- if result != [] and len(attrs) == 1 and options.get("strip", False):
123
- result = str(result).strip()
124
- if result == "":
125
- result = []
126
125
  result = result if isinstance(result, list) else [result]
127
126
  if len(attrs) == 1:
128
127
  return result
@@ -369,7 +368,12 @@ class FHIRPathUtils(object):
369
368
  unit = val_a if isinstance(val_a, self.model.Quantity) else val_b # TODO divide units (g / m -> g/m)
370
369
  return [self.model.Quantity(value=self.model.decimal(value=v_a / v_b), unit=unit.unit, code=unit.code, system=unit.system)]
371
370
  else:
372
- return [decimal.Decimal(round(val_a / val_b, 8))]
371
+ # decimal division, also for integers (not float division)
372
+ if isinstance(val_a, int):
373
+ val_a = decimal.Decimal(val_a)
374
+ if isinstance(val_b, int):
375
+ val_b = decimal.Decimal(val_b)
376
+ return [round(val_a / val_b, 8)]
373
377
 
374
378
  def div(self, val_a, val_b):
375
379
  val_a = self.single_value(val_a)
@@ -563,6 +567,14 @@ class FHIRPathUtils(object):
563
567
  else:
564
568
  return [-1]
565
569
 
570
+ def lastIndexOf(self, val, other):
571
+ val = self.string_value(val)
572
+ other = self.string_value(self.single_value(other, str, self.model.string))
573
+ if val == [] or other == []:
574
+ return []
575
+ else:
576
+ return [val.rfind(other)]
577
+
566
578
  def concat(self, val, other):
567
579
  val = self.string_value(self.single_value(val))
568
580
  other = self.string_value(self.single_value(other))
@@ -1106,13 +1118,13 @@ class FHIRPathUtils(object):
1106
1118
  return [val]
1107
1119
  elif isinstance(val, self.model.decimal):
1108
1120
  return [decimal.Decimal(val.value)]
1121
+ elif isinstance(val, self.model.boolean) or isinstance(val, bool): # before int, which bool is a subclass of
1122
+ return [decimal.Decimal("1.0") if self.boolean_value(val) else decimal.Decimal("0.0")]
1109
1123
  elif isinstance(val, self.model.integer) or isinstance(val, int):
1110
1124
  if isinstance(val, self.model.integer):
1111
1125
  return [decimal.Decimal(val.value)]
1112
1126
  else:
1113
1127
  return [decimal.Decimal(val)]
1114
- elif isinstance(val, self.model.boolean) or isinstance(val, bool):
1115
- return [decimal.Decimal("1.0") if self.boolean_value(val) else decimal.Decimal("0.0")]
1116
1128
  elif isinstance(val, self.model.string) or isinstance(val, str):
1117
1129
  try:
1118
1130
  return [decimal.Decimal(self.string_value(val))]
@@ -0,0 +1,176 @@
1
+ import io
2
+ import logging
3
+ from html import escape as html_escape
4
+ from html.parser import HTMLParser
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ def _attr(name, value):
9
+ return f' {name}="{html_escape(str(value))}"'
10
+
11
+ def _handle_tr(tr):
12
+ html = ""
13
+ attrs = ""
14
+ if tr.ID:
15
+ attrs += _attr("id", tr.ID)
16
+ if tr.styleCode:
17
+ attrs += _attr("class", tr.styleCode)
18
+ html += "<tr %s>" % attrs
19
+ for th in tr.th:
20
+ html += _handle_tdh(th, "th")
21
+ for td in tr.td:
22
+ html += _handle_tdh(td, "td")
23
+ html += "</tr>"
24
+ return html
25
+
26
+ def _handle_tdh(tdh, tag):
27
+ html = ""
28
+ attrs = ""
29
+ if tdh.ID:
30
+ attrs += _attr("id", tdh.ID)
31
+ if tdh.styleCode:
32
+ attrs += _attr("class", tdh.styleCode)
33
+ html += f"<{tag}%s>" % attrs
34
+ html += _handle_content(tdh)
35
+ html += f"</{tag}>"
36
+ return html
37
+
38
+ def _handle_content(elem):
39
+ html = ""
40
+ for content in elem.content_:
41
+ if content.category == 1:
42
+ html += html_escape(content.value)
43
+ elif content.category == 2:
44
+ if content.name == "br":
45
+ html += f'<br />'
46
+ else:
47
+ logger.warning("Skipping unhandled simple narrative content '%s'", content.name)
48
+ elif content.category == 3:
49
+ attrs = ""
50
+ if getattr(content.value, "ID", None):
51
+ attrs += _attr("id", content.value.ID)
52
+ if content.name == "table":
53
+ html += "<table%s>" % attrs
54
+ if content.value.thead:
55
+ html += "<thead>"
56
+ for tr in content.value.thead.tr:
57
+ html += _handle_tr(tr)
58
+ html += "</thead>"
59
+ if content.value.tfoot:
60
+ html += "<tfoot>"
61
+ for tr in content.value.tfoot.tr:
62
+ html += _handle_tr(tr)
63
+ html += "</tfoot>"
64
+ for tbody in content.value.tbody or [None]:
65
+ html += "<tbody>"
66
+ for tr in (tbody.tr if tbody else []):
67
+ html += _handle_tr(tr)
68
+ html += "</tbody>"
69
+ html += "</table>"
70
+ elif content.name == "content":
71
+ if content.value.styleCode == "Italics":
72
+ attrs += ' class="italics"'
73
+ elif content.value.styleCode:
74
+ attrs += _attr("class", content.value.styleCode)
75
+ html += "<span%s>" % attrs
76
+ html += _handle_content(content.value)
77
+ html += "</span>"
78
+ elif content.name == "paragraph":
79
+ if content.value.styleCode:
80
+ attrs += _attr("class", content.value.styleCode)
81
+ html += "<p%s>" % attrs
82
+ html += _handle_content(content.value)
83
+ html += "</p>"
84
+ elif content.name == "sup":
85
+ html += "<sup%s>" % attrs
86
+ html += _handle_content(content.value)
87
+ html += "</sup>"
88
+ elif content.name == "footnote":
89
+ html += '<span class="footnote"%s>' % attrs
90
+ html += _handle_content(content.value)
91
+ html += "</span>"
92
+ elif content.name == "renderMultiMedia":
93
+ html += '<img%s%s />' % (_attr("src", content.value.referencedObject), attrs)
94
+ elif content.name == "br":
95
+ html += f'<br />'
96
+ elif content.name == "caption":
97
+ html += "<caption%s>" % attrs
98
+ html += _handle_content(content.value)
99
+ html += "</caption>"
100
+ elif content.name == "list":
101
+ if content.value.listType in [None, "unordered"]:
102
+ if content.value.styleCode:
103
+ attrs += _attr("style", f"list-style-type:{content.value.styleCode.lower()}")
104
+ html += "<ul%s>" % attrs
105
+ for item in content.value.item:
106
+ html += "<li>"
107
+ html += _handle_content(item)
108
+ html += "</li>"
109
+ html += "</ul>"
110
+ elif content.value.listType == "ordered":
111
+ styleCodeTrans = {"LittleRoman": "i", "BigRoman": "I", "LittleAlpha": "a", "BigAlpha": "A", "Arabic": "1"}
112
+ listType = styleCodeTrans.get(content.value.styleCode)
113
+ if listType:
114
+ attrs += f' type="{listType}"'
115
+ html += "<ol%s>" % attrs
116
+ for item in content.value.item:
117
+ html += "<li>"
118
+ html += _handle_content(item)
119
+ html += "</li>"
120
+ html += "</ol>"
121
+ else:
122
+ html += _handle_content(content.value)
123
+ else:
124
+ logger.warning("Skipping unhandled narrative content category %s", content.category)
125
+ return html
126
+
127
+ def strucdoctext2html(module, strucdoctext):
128
+ html = _handle_content(strucdoctext)
129
+ div = module.Flow(content_=[module.MixedContainer(1, 0, "", html)])
130
+ div.original_tagname_ = "div"
131
+ div.valueOf_ = ""
132
+ return div
133
+
134
+ def ed2html(module, ed):
135
+ # TODO: improve
136
+ div = module.Flow(content_=[module.MixedContainer(1, 0, "", f"<v3:reference>{ed.reference.value}</v3:reference>")])
137
+ div.original_tagname_ = "div"
138
+ div.valueOf_ = ""
139
+ return div
140
+
141
+ def ed2markdown(module, ed):
142
+ # TODO: improve
143
+ return module.markdown(value=f"<v3:reference>{ed.reference.value}</v3:reference>")
144
+
145
+ def strucdoctext2markdown(module, strucdoctext):
146
+ # TODO: implement properly
147
+ return module.markdown(value=strip_tags(strucdoctext))
148
+
149
+
150
+ def strip_tags(val):
151
+ class MLStripper(HTMLParser):
152
+ def __init__(self):
153
+ super().__init__()
154
+ self.reset()
155
+ self.strict = False
156
+ self.convert_charrefs= True
157
+ self.text = io.StringIO()
158
+ def handle_data(self, d):
159
+ self.text.write(d)
160
+ self.text.write(" ")
161
+ def get_data(self):
162
+ return self.text.getvalue()
163
+
164
+ html = _handle_content(val)
165
+ s = MLStripper()
166
+ s.feed(html)
167
+ text = s.get_data()
168
+ text = " ".join(text.split())
169
+ return text
170
+
171
+
172
+ def builddiv(module, content):
173
+ div = module.Flow(content_=[module.MixedContainer(1, 0, "", content)])
174
+ div.original_tagname_ = "div"
175
+ div.valueOf_ = ""
176
+ return div