Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
manfred-kaiser committed Jun 15, 2021
2 parents 76ce4e1 + 8f70a7d commit 29551fe
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 17 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).


## [0.1.3] - 2021-06-15

### Fixed

- fixed optional function name for custom function
- added some syntax checks


## [0.1.2] - 2021-05-23

### Fixed
Expand Down Expand Up @@ -66,7 +74,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Initial release


[0.1.3]: https://github.com/manfred-kaiser/business-rule-engine/compare/0.1.1...0.1.3
[0.1.3]: https://github.com/manfred-kaiser/business-rule-engine/compare/0.1.2...0.1.3
[0.1.2]: https://github.com/manfred-kaiser/business-rule-engine/compare/0.1.1...0.1.2
[0.1.1]: https://github.com/manfred-kaiser/business-rule-engine/compare/0.1.0...0.1.1
[0.1.0]: https://github.com/manfred-kaiser/business-rule-engine/compare/0.0.5...0.1.0
Expand Down
36 changes: 22 additions & 14 deletions business_rule_engine/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
from collections import OrderedDict
from collections.abc import Iterator
import formulas # type: ignore

from typing import (
Expand All @@ -14,22 +15,23 @@
from business_rule_engine.exceptions import (
DuplicateRuleName,
MissingArgumentError,
ConditionReturnValueError
ConditionReturnValueError,
RuleParserSyntaxError,
)


class Rule():

def __init__(self, rulename, condition_requires_bool: bool = True) -> None:
def __init__(self, rulename: Text, condition_requires_bool: bool = True) -> None:
self.condition_requires_bool = condition_requires_bool
self.rulename: Text = rulename
self.conditions: List[Text] = []
self.actions: List[Text] = []
self.status = None
self.status: Optional[bool] = None

@staticmethod
def _compile_condition(condition_lines: List[Text]) -> Any:
condition = "".join(condition_lines)
condition = " ".join(condition_lines)
if not condition.startswith("="):
condition = "={}".format(condition)
return formulas.Parser().ast(condition)[1].compile() # type: ignore
Expand All @@ -52,7 +54,7 @@ def _get_params(params: Dict[Text, Any], condition_compiled: Any, set_default_ar
params_condition = {k: v for k, v in params_dict.items() if k in condition_args}
return params_condition

def check_condition(self, params, *, set_default_arg=False, default_arg=None):
def check_condition(self, params: Dict[Text, Any], *, set_default_arg: bool = False, default_arg: Any = None) -> Any:
condition_compiled = self._compile_condition(self.conditions)
params_condition = self._get_params(params, condition_compiled, set_default_arg, default_arg)
rvalue_condition = condition_compiled(**params_condition).tolist()
Expand All @@ -61,12 +63,12 @@ def check_condition(self, params, *, set_default_arg=False, default_arg=None):
self.status = bool(rvalue_condition)
return rvalue_condition

def run_action(self, params, *, set_default_arg=False, default_arg=None):
def run_action(self, params: Dict[Text, Any], *, set_default_arg: bool = False, default_arg: Any = None) -> Any:
action_compiled = self._compile_condition(self.actions)
params_actions = self._get_params(params, action_compiled, set_default_arg, default_arg)
return action_compiled(**params_actions)

def execute(self, params, *, set_default_arg=False, default_arg=None) -> Tuple[bool, Any]:
def execute(self, params: Dict[Text, Any], *, set_default_arg: bool = False, default_arg: Any = None) -> Tuple[Any, Any]:
rvalue_condition = self.check_condition(params, set_default_arg=set_default_arg, default_arg=default_arg)
if not self.status:
return rvalue_condition, None
Expand All @@ -83,10 +85,11 @@ def __init__(self, condition_requires_bool: bool = True) -> None:
self.condition_requires_bool = condition_requires_bool

def parsestr(self, text: Text) -> None:
rulename = None
is_condition = False
is_action = False
ignore_line = False
rulename: Optional[Text] = None
is_condition: bool = False
is_action: bool = False
is_then: bool = False
ignore_line: bool = False

for line in text.split('\n'):
ignore_line = False
Expand All @@ -103,24 +106,29 @@ def parsestr(self, text: Text) -> None:
is_condition = True
is_action = False
if line.lower().strip().startswith('then'):
if is_then:
raise RuleParserSyntaxError('using multiple "then" in one rule is not allowed')
is_then = True
ignore_line = True
is_condition = False
is_action = True
if line.lower().strip().startswith('end'):
ignore_line = True
is_condition = False
is_action = False
is_then = False
if rulename and is_condition and not ignore_line:
self.rules[rulename].conditions.append(line.strip())
if rulename and is_action and not ignore_line:
self.rules[rulename].actions.append(line.strip())

@classmethod
def register_function(cls, function: Any, function_name: Optional[Text] = None) -> None:
cls.CUSTOM_FUNCTIONS.append(function_name or function.__name__.upper())
formulas.get_functions()[function_name or function.__name__.upper()] = function # type: ignore
custom_function_name = function_name or function.__name__
cls.CUSTOM_FUNCTIONS.append(custom_function_name.upper())
formulas.get_functions()[custom_function_name.upper()] = function # type: ignore

def __iter__(self):
def __iter__(self) -> Iterator:
return self.rules.values().__iter__()

def execute(
Expand Down
4 changes: 4 additions & 0 deletions business_rule_engine/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ class RuleParserException(Exception):
pass


class RuleParserSyntaxError(RuleParserException):
pass


class DuplicateRuleName(RuleParserException):
pass

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

setup(
name='business-rule-engine',
version='0.1.2',
version='0.1.3',
author='Manfred Kaiser',
author_email='manfred.kaiser@logfile.at',
description='Python DSL for setting up business intelligence rules',
Expand Down
49 changes: 48 additions & 1 deletion tests/test_rules.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import pytest # type: ignore
from business_rule_engine import RuleParser
from business_rule_engine import RuleParser, Rule
from business_rule_engine.exceptions import RuleParserSyntaxError
from formulas.errors import FormulaError


def order_more(items_to_order):
Expand Down Expand Up @@ -47,3 +49,48 @@ def test_iterate_rules():
if rule.status:
assert rvalue_action == "you ordered 50 new items"
break


def test_duplicate_then():
rules_invalid = """
rule "order new items"
when
products_in_stock < 20
then
order_more(50)
then
order_more(10)
end
"""
parser = RuleParser()
with pytest.raises(RuleParserSyntaxError):
parser.parsestr(rules_invalid)


def test_multiple_lines_invalid():
rules_invalid = """
rule "order new items"
when
1 = 1
then
1 + 1
2 + 2
end
"""

parser = RuleParser()
parser.parsestr(rules_invalid)
with pytest.raises(FormulaError):
parser.execute({})


def test_rule():
params = {
'products_in_stock': 10
}
rule = Rule('testrule')
rule.conditions.append('products_in_stock < 20')
rule.actions.append('2 + 3')

assert rule.check_condition(params) == True
assert rule.run_action(params) == 5

0 comments on commit 29551fe

Please sign in to comment.