Skip to content

Commit

Permalink
🐣 Issue #2 add supports for ´:below´ modifier and ´sa´ prefix. along …
Browse files Browse the repository at this point in the history
…side some bugs are fixes.

tests coverage added.
  • Loading branch information
nazrulworld committed Feb 20, 2020
1 parent bca2f5a commit 3988d7a
Show file tree
Hide file tree
Showing 12 changed files with 155 additions and 23 deletions.
11 changes: 9 additions & 2 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@
History
=======

0.4.2 (unreleased)
0.5.0 (unreleased)
------------------

- Nothing changed yet.
Improvements

- Add support for ``:bellow`` FHIR search modifier and ``sa`` prefix. See https://github.com/nazrulworld/fhirpath/issues/2

Bugfixes

- Upgrade to this version is recommended as it includes couples of major bug fixes.



0.4.1 (2019-11-05)
Expand Down
1 change: 0 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,6 @@ Note that the way search parameters operate is not the same as
the way the operations on two numbers work in a mathematical sense.
sa (starts-after) and
eb (ends-before) are not used with integer values but are used for decimals.
https://www.elastic.co/guide/en/elasticsearch/reference/current/index-prefixes.html
https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-wildcard-query.html
https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-prefix-query.html#prefix-query-ex-request

Expand Down
5 changes: 4 additions & 1 deletion src/fhirpath/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
def get_version():
""" """
import os
with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "version.py"), "r") as fp:

with open(
os.path.join(os.path.dirname(os.path.abspath(__file__)), "version.py"), "r"
) as fp:
for line in fp:
ln = line.strip()
if not ln:
Expand Down
30 changes: 28 additions & 2 deletions src/fhirpath/dialects/elasticsearch.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# _*_ coding: utf-8 _*_
"""ElasticSearch Dialect"""
import logging
from fhirpath.enums import OPERATOR
import re

import isodate
from zope.interface import alsoProvides

from fhirpath.enums import OPERATOR
from fhirpath.enums import GroupType
from fhirpath.enums import MatchType
from fhirpath.enums import SortOrderType
Expand Down Expand Up @@ -91,6 +91,25 @@ def _create_term(self, path, value, multiple=False):

return q

def _create_sa_term(self, path, value):
"""Create ES Prefix Query"""
if isinstance(value, (list, tuple)):
if len(value) == 1:
value = value[0]
else:
q = {
"bool": {"should": [], "minimum_should_match": 1}
}
for val in value:
q["bool"]["should"].append(
{"prefix": {path: {"value": val}}}
)
return q

q = {"prefix": {path: {"value": value}}}
# xxx: what about multiple
return q

def _create_dotted_path(self, term, root_replacer=None):
""" """
if INonFhirTerm.providedBy(term):
Expand Down Expand Up @@ -283,7 +302,12 @@ def resolve_term(self, term, mapping, root_replacer):
)

else:
q = self._create_term(dotted_path, value, multiple=multiple)
if term.comparison_operator == OPERATOR.sa:
q = self._create_sa_term(
dotted_path, value
)
else:
q = self._create_term(dotted_path, value, multiple=multiple)
resolved = q, term.unary_operator

elif term.path.context.type_name in (
Expand Down Expand Up @@ -433,6 +457,8 @@ def resolve_string_term(self, term, map_info, root_replacer=None):
# xxx: should handle exact match
if term.match_type == TermMatchType.EXACT:
qr = {"match_phrase": {path_: value}}
elif term.comparison_operator == OPERATOR.sa:
qr = {"match_phrase_prefix": {path_: value}}
else:
qr = {"match": {path_: value}}
elif ("/" in value or URI_SCHEME.match(value)) and ".reference" in path_:
Expand Down
8 changes: 6 additions & 2 deletions src/fhirpath/fhirspec/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -1258,7 +1258,7 @@ def write(self):
for resource_type in param_def.expression_map:
if not storage.exists(resource_type):
storage.insert(
resource_type, ResoureSearchParameterDefinition(resource_type)
resource_type, ResourceSearchParameterDefinition(resource_type)
)
obj = storage.get(resource_type)
# add search param code to obj
Expand Down Expand Up @@ -1390,6 +1390,10 @@ def get_expression(self, resource_type, definition):
# try cleanup Zero Width Space
if "\u200b" in exp:
exp = exp.replace("\u200b", "")
if "|" in exp:
# some case for example name: "Organization.name | Organization.alias"
# we take first one!
exp = exp.split("|")[0]

return exp.strip()

Expand All @@ -1414,7 +1418,7 @@ def __copy__(self):
return newone


class ResoureSearchParameterDefinition(object):
class ResourceSearchParameterDefinition(object):
""" """

__slots__ = ("__storage__", "_finalized", "resource_type")
Expand Down
4 changes: 4 additions & 0 deletions src/fhirpath/fql/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
from .expressions import T_
from .expressions import V_
from .expressions import and_
from .expressions import eb_
from .expressions import exists_
from .expressions import in_
from .expressions import not_
from .expressions import not_exists_
from .expressions import not_in_
from .expressions import or_
from .expressions import sa_
from .expressions import sort_
from .types import ElementPath

Expand All @@ -28,5 +30,7 @@
"not_in_",
"or_",
"sort_",
"sa_",
"eb_",
"ElementPath"
]
22 changes: 16 additions & 6 deletions src/fhirpath/fql/expressions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# _*_ coding: utf-8 _*_
from fhirpath.enums import OPERATOR

from fhirpath.interfaces.fql import IGroupTerm
from fhirpath.interfaces.fql import ITerm
from fhirpath.types import EMPTY_VALUE
Expand All @@ -15,6 +14,7 @@
from .types import Term
from .types import TermValue


__author__ = "Md Nazrul Islam <[email protected]>"

__all__ = [
Expand All @@ -37,11 +37,7 @@
# API functions
def T_(path, value=EMPTY_VALUE, match_type=None, non_fhir=False): # noqa: E302
""" """
params = {
"path": path,
"value": value,
"match_type": match_type
}
params = {"path": path, "value": value, "match_type": match_type}
if non_fhir is False:
term = Term(**params)
else:
Expand Down Expand Up @@ -145,6 +141,20 @@ def not_in_(path, values):
return not_(in_(path, values))


def sa_(path, value=EMPTY_VALUE):
""" """
term_or_group = _prepare_term_or_group(path, value)
term_or_group.comparison_operator = OPERATOR.sa
return term_or_group


def eb_(path, value=EMPTY_VALUE):
""" """
term_or_group = _prepare_term_or_group(path, value)
term_or_group.comparison_operator = OPERATOR.eb
return term_or_group


def sort_(path, order=EMPTY_VALUE):
""" """
sort_term = SortTerm(path)
Expand Down
2 changes: 1 addition & 1 deletion src/fhirpath/fql/types.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# _*_ coding: utf-8 _*_
import ast
import datetime
from fhirpath.enums import OPERATOR
import re
from collections import deque
from copy import copy
Expand All @@ -13,6 +12,7 @@
from fhirpath.constraints import required_finalized
from fhirpath.constraints import required_not_finalized
from fhirpath.constraints import required_value_not_assigned
from fhirpath.enums import OPERATOR
from fhirpath.enums import GroupType
from fhirpath.enums import MatchType
from fhirpath.enums import SortOrderType
Expand Down
25 changes: 21 additions & 4 deletions src/fhirpath/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
from fhirpath.fql import G_
from fhirpath.fql import T_
from fhirpath.fql import V_
from fhirpath.fql import eb_
from fhirpath.fql import exists_
from fhirpath.fql import not_
from fhirpath.fql import not_exists_
from fhirpath.fql import sa_
from fhirpath.fql import sort_
from fhirpath.fql.types import ElementPath
from fhirpath.interfaces import IFhirPrimitiveType
Expand Down Expand Up @@ -213,9 +215,11 @@ def build(self):
for nd in normalized_data:
self.add_term(nd, terms_container)

result = self.attach_limit_terms(
factory = self.attach_limit_terms(
self.attach_sort_terms(builder.where(*terms_container))
)(
)

result = factory(
unrestricted=self.context.unrestricted,
async_result=self.context.async_result,
)
Expand Down Expand Up @@ -853,16 +857,21 @@ def _single_valued_money_term_stu3(self, path_, value, modifier):
assert self.context.engine.fhir_release == FHIR_VERSION.STU3
return self.single_valued_quantity_term(path_, value, modifier)

def validate_pre_term(self, path_, value, modifier):
def validate_pre_term(self, operator_, path_, value, modifier):
""" """
pass
if modifier in ("above", "below") and operator_ in ("sa", "eb"):
raise ValidationError(
"You cannot use modifier (above,below) and prefix (sa,eb) at a time"
)

def create_term(self, path_, value, modifier):
""" """
assert IFhirPrimitiveType.implementedBy(path_.context.type_class)

if isinstance(value, tuple):
operator_, original_value = value
# do validate first
self.validate_pre_term(operator_, path_, value, modifier)
if isinstance(original_value, list):
# we force IN will have equal or not equal operator_
# xxx: should be validated already
Expand All @@ -877,6 +886,10 @@ def create_term(self, path_, value, modifier):
term = T_(path_)
if modifier == "not":
term = not_(term)
elif modifier == "below" and operator_ == "eq":
operator_ = "sa"
elif modifier == "above" and operator_ == "eq":
operator_ = "eb"

val = V_(original_value)

Expand All @@ -892,6 +905,10 @@ def create_term(self, path_, value, modifier):
term = term > val
elif operator_ == "ge":
term = term >= val
elif operator_ == "sa":
term = sa_(term, val)
elif operator_ == "eb":
term = eb_(term, val)
else:
raise NotImplementedError

Expand Down
4 changes: 2 additions & 2 deletions src/fhirpath/thirdparty/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# _*_ coding: utf-8 _*_
"""Basically here things are copied from various open source projects"""
from .peewee import Proxy # noqa: 401
from .peewee import attrdict # noqa: 401
from .plone_api_validation import InvalidParameterError # noqa: 401
from .plone_api_validation import MissingParameterError # noqa: 401
from .plone_api_validation import at_least_one_of # noqa: 401
from .plone_api_validation import mutually_exclusive_parameters # noqa: 401
from .plone_api_validation import required_parameters # noqa: 401
from .peewee import attrdict # noqa: 401
from .peewee import Proxy # noqa: 401
from .werkzeug import ImmutableDict # noqa: 401
13 changes: 12 additions & 1 deletion tests/test_fql.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#!/usr/bin/env python
# _*_ coding: utf-8 _*_
"""Tests for `fhirpath` package."""
from fhirpath.enums import OPERATOR
from datetime import datetime

import pytest
import pytz

from fhirpath.enums import OPERATOR
from fhirpath.enums import WhereConstraintType
from fhirpath.fql.expressions import G_
from fhirpath.fql.expressions import T_
Expand All @@ -17,6 +17,7 @@
from fhirpath.fql.expressions import not_exists_
from fhirpath.fql.expressions import not_in_
from fhirpath.fql.expressions import or_
from fhirpath.fql.expressions import sa_
from fhirpath.fql.types import ElementPath
from fhirpath.fql.types import Term
from fhirpath.fql.types import TermValue
Expand Down Expand Up @@ -179,6 +180,16 @@ def test_complex_expression(engine):
assert term.unary_operator == OPERATOR.neg


def test_sa_expression(engine):
""" """
term = T_("Organization.id")
value = V_("f0")
term = sa_(term, value)
term.finalize(engine)

assert term.comparison_operator == OPERATOR.sa


def test_query_builder(engine):
""" """
builder = (
Expand Down
Loading

0 comments on commit 3988d7a

Please sign in to comment.