Skip to content

Commit

Permalink
Merge branch 'issues-24'
Browse files Browse the repository at this point in the history
  • Loading branch information
atuonufure committed Jan 18, 2024
2 parents 7a391e4 + d479d7a commit 13f4555
Show file tree
Hide file tree
Showing 87 changed files with 73,961 additions and 2,598 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ jobs:
strategy:
matrix:
include:
- python-version: 3.8
- python-version: 3.9
- python-version: "3.10"
- python-version: "3.11"
Expand All @@ -27,7 +26,7 @@ jobs:
- name: Install dependencies
run: pipenv install --dev
- name: Run tests
run: pipenv run pytest
run: pipenv run pytest --tb=no
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
Expand Down
6 changes: 5 additions & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ verify_ssl = true
name = "pypi"

[packages]
antlr4-python3-runtime = "==4.13"
python-dateutil = "==2.8.2"

[dev-packages]
antlr4-python3-runtime = "==4.8"
pytest = "==7.1.1"
pytest-cov = "==3.0.0"
freezegun = "==1.2.2"
pyyaml = "==6.0.1"
pre-commit = "~=2.21.0"
ipython = "*"
black = "*"
types-python-dateutil = "*"
759 changes: 659 additions & 100 deletions Pipfile.lock

Large diffs are not rendered by default.

21 changes: 18 additions & 3 deletions fhirpathpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from fhirpathpy.parser import parse
from fhirpathpy.engine import do_eval
from fhirpathpy.engine.util import arraify, get_data, set_paths
from fhirpathpy.engine.nodes import FP_Type
from fhirpathpy.engine.nodes import FP_Type, ResourceNode

__title__ = "fhirpathpy"
__version__ = "0.2.2"
Expand Down Expand Up @@ -37,7 +37,16 @@ def visit(node):
data = get_data(node)

if isinstance(node, list):
return [visit(item) for item in data]
res = []
for item in data:
# Filter out intenal representation of primitive extensions
i = visit(item)
if isinstance(i, dict):
keys = list(i.keys())
if keys == ["extension"]:
continue
res.append(i)
return res

if isinstance(data, dict) and not isinstance(data, FP_Type):
for key, value in data.items():
Expand All @@ -63,7 +72,13 @@ def evaluate(resource, path, context={}, model=None):
int: Description of return value
"""
node = parse(path)
if isinstance(path, dict):
node = parse(path["expression"])
if "base" in path:
resource = ResourceNode.create_node(resource, path['base'])
else:
node = parse(path)

return apply_parsed_path(resource, node, context, model)


Expand Down
32 changes: 25 additions & 7 deletions fhirpathpy/engine/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import numbers
import fhirpathpy.engine.util as util
from fhirpathpy.engine.nodes import TypeInfo
from fhirpathpy.engine.evaluators import evaluators
from fhirpathpy.engine.invocations import invocations

Expand Down Expand Up @@ -62,6 +63,9 @@ def doInvoke(ctx, fn_name, data, raw_params):

raise Exception(fn_name + " expects no params")

if invocation["fn"].__name__ == "trace_fn" and raw_params is not None:
raw_params = raw_params[:1]

paramsNumber = 0
if isinstance(raw_params, list):
paramsNumber = len(raw_params)
Expand Down Expand Up @@ -89,6 +93,21 @@ def doInvoke(ctx, fn_name, data, raw_params):
return util.arraify(res)


def type_specifier(ctx, parent_data, node):
identifiers = node["text"].replace("`", "").split(".")
namespace = None
name = None

if len(identifiers) == 2:
namespace, name = identifiers
elif len(identifiers) == 1:
(name,) = identifiers
else:
raise Exception(f"Expected TypeSpecifier node, got {node}")

return TypeInfo(name=name, namespace=namespace)


param_check_table = {
"Integer": check_integer_param,
"Number": check_number_param,
Expand All @@ -98,7 +117,6 @@ def doInvoke(ctx, fn_name, data, raw_params):


def make_param(ctx, parentData, node_type, param):

if node_type == "Expr":

def func(data):
Expand All @@ -108,15 +126,18 @@ def func(data):
return func

if node_type == "AnyAtRoot":
ctx["$this"] = ctx["$this"] if "$this" in ctx else ctx['dataRoot']
return do_eval(ctx, ctx["dataRoot"], param)
ctx["$this"] = ctx["$this"] if "$this" in ctx else ctx["dataRoot"]
return do_eval(ctx, ctx["$this"], param)

if node_type == "Identifier":
if param["type"] == "TermExpression":
return param["text"]

raise Exception("Expected identifier node, got " + json.dumps(param))

if node_type == "TypeSpecifier":
return type_specifier(ctx, parentData, param)

ctx["$this"] = parentData
res = do_eval(ctx, parentData, param)

Expand All @@ -131,10 +152,7 @@ def func(data):

if len(res) > 1:
raise Exception(
"Unexpected collection"
+ json.dumps(res)
+ "; expected singleton of type "
+ node_type
"Unexpected collection" + json.dumps(res) + "; expected singleton of type " + node_type
)

if len(res) == 0:
Expand Down
113 changes: 62 additions & 51 deletions fhirpathpy/engine/evaluators/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from decimal import Decimal
from functools import reduce

import re
Expand All @@ -14,10 +15,10 @@ def boolean_literal(ctx, parentData, node):


def number_literal(ctx, parentData, node):
float_number = float(node["text"])
int_number = int(float_number)
decimal_number = Decimal(node["text"])
int_number = int(decimal_number)

return [int_number] if float_number == int_number else [float_number]
return [int_number] if decimal_number == int_number else [decimal_number]


def identifier(ctx, parentData, node):
Expand Down Expand Up @@ -48,10 +49,18 @@ def union_expression(ctx, parentData, node):
return engine.infix_invoke(ctx, "|", parentData, node["children"])


def index_invocation(ctx, parentData, node):
return util.arraify(ctx["$index"])


def this_invocation(ctx, parentData, node):
return util.arraify(ctx["$this"])


def total_invocation(ctx, parentData, node):
return util.arraify(ctx["$total"])


def op_expression(ctx, parentData, node):
op = node["terminalNodeText"][0]
return engine.infix_invoke(ctx, op, parentData, node["children"])
Expand All @@ -62,9 +71,7 @@ def func(ctx, parentData, node):
op = node["terminalNodeText"][0]

if not op in mapFn:
raise Exception(
"Do not know how to alias " + op + " by " + json.dumps(mapFn)
)
raise Exception("Do not know how to alias " + op + " by " + json.dumps(mapFn))

alias = mapFn[op]
return engine.infix_invoke(ctx, alias, parentData, node["children"])
Expand Down Expand Up @@ -93,11 +100,10 @@ def literal_term(ctx, parentData, node):
return [node["text"]]


# TODO
def external_constant_term(ctx, parent_data, node):
ext_constant = node["children"][0]
ext_identifier = ext_constant["children"][0]
varName = identifier(ctx, parent_data, ext_identifier)[0]
varName = identifier(ctx, parent_data, ext_identifier)[0].replace("`", "")

if not varName in ctx["vars"]:
return []
Expand All @@ -107,42 +113,48 @@ def external_constant_term(ctx, parent_data, node):
# For convenience, we all variable values to be passed in without their array
# wrapper. However, when evaluating, we need to put the array back in.

if value is None:
return []

if not isinstance(value, list):
return [value]

return value


def match(m):
code = m.group(1)
return chr(int(code[1:], 16))


def string_literal(ctx, parentData, node):
# Remove the beginning and ending quotes.
rtn = re.sub(r"^['\"]|['\"]$", "", node["text"])

rtn = rtn.replace("\\'", "'")
rtn = rtn.replace("\\`", "`")
rtn = rtn.replace('\\"', '"')
rtn = rtn.replace("\\r", "\r")
rtn = rtn.replace("\\n", "\n")
rtn = rtn.replace("\\t", "\t")
rtn = rtn.replace("\\f", "\f")
rtn = rtn.replace("\\\\", "\\")

# TODO
# rtn = rtn.replace(/\\(u\d{4}|.)/g, function(match, submatch) {
# if (submatch.length > 1)
# return String.fromCharCode('0x'+submatch.slice(1));
# else
# return submatch;
rtn = re.sub(r"\\(u\d{4})", match, rtn)

return [rtn]


def quantity_literal(ctx, parentData, node):
valueNode = node["children"][0]
value = float(valueNode["terminalNodeText"][0])
value = Decimal(valueNode["terminalNodeText"][0])
unitNode = valueNode["children"][0]
unit = unitNode.terminalNodeText[0]
if len(unitNode["terminalNodeText"]) > 0:
unit = unitNode["terminalNodeText"][0]
# Sometimes the unit is in a child node of the child
if unit is not None and len(unitNode["children"]) > 0:
elif "children" in unitNode and len(unitNode["children"]) > 0:
unit = unitNode["children"][0]["terminalNodeText"][0]
else:
unit = None

return [nodes.FP_Quantity(value, unit)]

Expand All @@ -160,46 +172,42 @@ def time_literal(ctx, parentData, node):
def create_reduce_member_invocation(model, key):
def func(acc, res):
res = nodes.ResourceNode.create_node(res)

childPath = ""
if res.path is not None:
childPath = res.path + "." + key

if (
model is not None
and "pathsDefinedElsewhere" in model
and childPath in model["pathsDefinedElsewhere"]
):
childPath = model["pathsDefinedElsewhere"][childPath]
childPath = f"{res.path}.{key}" if res.path else f"_.{key}"

actualTypes = None

if (
model is not None
and "choiceTypePaths" in model
and childPath in model["choiceTypePaths"]
):
actualTypes = model["choiceTypePaths"][childPath]

toAdd = None
toAdd_ = None

if isinstance(actualTypes, list):
if isinstance(model, dict):
childPath = model["pathsDefinedElsewhere"].get(childPath, childPath)
actualTypes = model["choiceTypePaths"].get(childPath)

if isinstance(res.data, nodes.FP_Quantity):
toAdd = res.data.value

if actualTypes and isinstance(res.data, dict):
# Use actualTypes to find the field's value
for actualType in actualTypes:
field = key + actualType
if isinstance(res.data, (dict, list)):
toAdd = res.data.get(field)
toAdd_ = res.data.get(f"_{field}")
if toAdd is not None or toAdd_ is not None:
childPath = actualType
break
field = f"{key}{actualType}"
toAdd = res.data.get(field)
toAdd_ = res.data.get(f"_{field}")
if toAdd is not None or toAdd_ is not None:
childPath += actualType
break
elif isinstance(res.data, dict):
toAdd = res.data.get(key)
toAdd_ = res.data.get(f"_{key}")
if key == "extension":
childPath = "Extension"
else:
if isinstance(res.data, (dict, list)):
toAdd = res.data.get(key)
toAdd_ = res.data.get(f"_{key}")
if key == 'extension':
childPath = 'Extension'
if key == "length":
toAdd = len(res.data)

childPath = (
model["path2Type"].get(childPath, childPath)
if isinstance(model, dict) and "path2Type" in model
else childPath
)

if util.is_some(toAdd):
if isinstance(toAdd, list):
Expand All @@ -219,7 +227,7 @@ def func(acc, res):


def member_invocation(ctx, parentData, node):
key = engine.do_eval(ctx, parentData, node["children"][0])[0]
key = engine.do_eval(ctx, parentData, node["children"][0])[0].replace("`", "")
model = ctx["model"]

if isinstance(parentData, list):
Expand Down Expand Up @@ -307,6 +315,8 @@ def polarity_expression(ctx, parentData, node):
"ThisInvocation": this_invocation,
"MemberInvocation": member_invocation,
"FunctionInvocation": function_invocation,
"IndexInvocation": index_invocation,
"TotalInvocation": total_invocation,
# expressions
"PolarityExpression": polarity_expression,
"IndexerExpression": indexer_expression,
Expand All @@ -317,6 +327,7 @@ def polarity_expression(ctx, parentData, node):
"InequalityExpression": op_expression,
"AdditiveExpression": op_expression,
"MultiplicativeExpression": op_expression,
"TypeExpression": alias_op_expression({"is": "isOp", "as": "asOp"}),
"EqualityExpression": op_expression,
"OrExpression": op_expression,
"ImpliesExpression": op_expression,
Expand Down
Loading

0 comments on commit 13f4555

Please sign in to comment.