diff --git a/.circleci/config.yml b/.circleci/config.yml
index 4b5db2b4..2dc85f37 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -2,19 +2,21 @@ version: 2
jobs:
build:
+ resource_class: small
docker:
- - image: circleci/python:2.7
+ - image: cimg/python:3.11
steps:
- checkout
- run:
name: Install Dependencies
command: |
- sudo pip install virtualenv
- virtualenv venv
- . venv/bin/activate
- make deps
+ make clean
+ poetry install --no-ansi --no-interaction
- run:
name: Run Tests
command: |
- . venv/bin/activate
make coverage
+ - store_artifacts:
+ path: htmlcov
+ - store_test_results:
+ path: ./test-results
diff --git a/.flake8 b/.flake8
new file mode 100644
index 00000000..81570acb
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,12 @@
+[flake8]
+# Taken from https://black.readthedocs.io/en/stable/compatible_configs.html#flake8
+
+extend-ignore = E203,E501,W503,B904,B905,B907
+select = B,C,E,F,W,T4,B9
+
+# Note on line length:
+# We use flake8-bugbear's B950 instead of flake8's E501. It considers "max-line-length"
+# but only triggers when the value has been exceeded by more than 10%.
+max-line-length = 88
+
+max-complexity = 18
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
new file mode 100644
index 00000000..e1a70dfc
--- /dev/null
+++ b/.github/workflows/tests.yaml
@@ -0,0 +1,32 @@
+name: Tests
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+jobs:
+ tests:
+ # https://help.github.com/articles/virtual-environments-for-github-actions
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ # Python 3.12 is not working yet at time of writing this.
+ python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install dependencies
+ run: |
+ curl -sSL https://install.python-poetry.org | python3 -
+ poetry install -v --no-root --with dev
+
+ - name: Test with tox
+ run: poetry run tox -vv
diff --git a/.gitignore b/.gitignore
index d5f52038..0858199d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
*.py[cod]
+.DS_Store
# C extensions
*.so
@@ -24,7 +25,8 @@ pip-log.txt
# Unit test / coverage reports
.coverage
.tox
-nosetests.xml
+.pytest_cache
+test-results/
# Translations
*.mo
@@ -37,6 +39,8 @@ nosetests.xml
# Vim
*.sw[po]
.idea/
+
+# Virtual environments
+venv*/
tests/__pycache__/
.cache/
-business-rules.iml
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..7f3a0dcb
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,46 @@
+repos:
+ - repo: https://github.com/PyCQA/isort
+ rev: 5.12.0
+ hooks:
+ - id: isort
+
+ - repo: https://github.com/psf/black
+ rev: 22.3.0
+ hooks:
+ - id: black
+
+ - repo: https://github.com/PyCQA/flake8
+ rev: 6.0.0
+ hooks:
+ - id: flake8
+ args: [--config, .flake8]
+ additional_dependencies: [flake8-bugbear]
+
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.4.0
+ hooks:
+ - id: check-added-large-files
+ - id: check-ast
+ - id: check-byte-order-marker
+ - id: check-case-conflict
+ - id: check-executables-have-shebangs
+ - id: check-json
+ - id: check-symlinks
+ - id: check-toml
+ - id: check-vcs-permalinks
+ - id: check-xml
+ - id: check-yaml
+ - id: debug-statements
+ - id: detect-private-key
+ - id: end-of-file-fixer
+ - id: fix-encoding-pragma
+ args: [--remove]
+ - id: forbid-new-submodules
+ - id: mixed-line-ending
+ args: [--fix=lf]
+ # Cannot use for now due to our factories.py files
+ # - id: name-tests-test
+ # args: [--django]
+ - id: no-commit-to-branch
+ args: [--branch=master]
+ - id: trailing-whitespace
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 2d254379..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-language: python
-python:
- - "2.6"
- - "2.7"
- - "pypy"
- - "3.2"
- - "3.3"
-install:
- - if [[ $TRAVIS_PYTHON_VERSION == 2* ]]; then pip install unittest2; fi
- - "pip install -e ."
-script:
- - "nosetests tests"
diff --git a/HISTORY.md b/HISTORY.md
new file mode 100644
index 00000000..13d32dfb
--- /dev/null
+++ b/HISTORY.md
@@ -0,0 +1,26 @@
+History
+=========
+
+## 1.1.1
+
+Release date: 2022-3-18
+
+- Fix package description and long description
+
+## 1.1.0
+
+Release date: 2022-3-18
+
+- Add support for Python 3.5-3.7
+
+## 1.0.1
+
+Release date: 2016-3-16
+
+- Fixes a packaging bug preventing 1.0.0 from being installed on some platforms.
+
+## 1.0.0
+
+Release date: 2016-3-16
+
+- Removes caching layer on rule decorator
diff --git a/HISTORY.rst b/HISTORY.rst
deleted file mode 100644
index 9a682cb6..00000000
--- a/HISTORY.rst
+++ /dev/null
@@ -1,14 +0,0 @@
-History
--------
-
-1.0.1
-+++++
-released 2016-3-16
-
-- Fixes a packaging bug preventing 1.0.0 from being installed on some platforms.
-
-1.0.0
-+++++
-released 2016-3-16
-
-- Removes caching layer on rule decorator
diff --git a/README.md b/README.md
index 40369281..61b23b3d 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,6 @@
business-rules
==============
-[![Build Status](https://travis-ci.org/venmo/business-rules.svg?branch=master)](https://travis-ci.org/venmo/business-rules)
-
As a software system grows in complexity and usage, it can become burdensome if
every change to the logic/behavior of the system also requires you to write and
deploy new code. The goal of this business rules engine is to provide a simple
@@ -14,10 +12,6 @@ marketing logic around when certain customers or items are eligible for a
discount or to automate emails after users enter a certain state or go through
a particular sequence of events.
-
-
-
-
## Usage
### 1. Define Your set of variables
@@ -443,7 +437,15 @@ A variable can return the following types of values:
Open up a pull request, making sure to add tests for any new functionality. To set up the dev environment (assuming you're using [virtualenvwrapper](http://docs.python-guide.org/en/latest/dev/virtualenvs/#virtualenvwrapper)):
```bash
-$ mkvirtualenv business-rules
-$ pip install -r dev-requirements.txt
-$ nosetests
+$ python -m virtualenv venv
+$ source ./venv/bin/activate
+$ pip install -r dev-requirements.txt -e .
+$ pytest
+```
+
+Alternatively, you can also use Tox:
+
+```bash
+$ pip install "tox<4"
+$ tox -p auto --skip-missing-interpreters
```
diff --git a/business_rules/__init__.py b/business_rules/__init__.py
index e64d144d..059a129a 100644
--- a/business_rules/__init__.py
+++ b/business_rules/__init__.py
@@ -1,6 +1,6 @@
-__version__ = '1.5.0'
+__version__ = "1.1.1"
-from .engine import run_all, check_conditions_recursively
+from .engine import check_conditions_recursively, run_all
from .utils import export_rule_data, validate_rule_data
# Appease pyflakes by "using" these exports
diff --git a/business_rules/actions.py b/business_rules/actions.py
index 647269b0..e717656e 100644
--- a/business_rules/actions.py
+++ b/business_rules/actions.py
@@ -1,22 +1,23 @@
-from __future__ import absolute_import
import inspect
+from dataclasses import dataclass
+from typing import Optional
from .utils import fn_name_to_pretty_label, get_valid_fields
class BaseActions(object):
- """ Classes that hold a collection of actions to use with the rules
+ """Classes that hold a collection of actions to use with the rules
engine should inherit from this.
"""
@classmethod
def get_all_actions(cls):
methods = inspect.getmembers(cls)
- return [{
- 'name': m[0],
- 'label': m[1].label,
- 'params': m[1].params
- } for m in methods if getattr(m[1], 'is_rule_action', False)]
+ return [
+ {'name': m[0], 'label': m[1].label, 'params': m[1].params}
+ for m in methods
+ if getattr(m[1], 'is_rule_action', False)
+ ]
def _validate_action_parameters(func, params):
@@ -40,12 +41,18 @@ def _validate_action_parameters(func, params):
for param in params:
param_name, field_type = param['name'], param['fieldType']
if param_name not in func.__code__.co_varnames:
- raise AssertionError("Unknown parameter name {0} specified for action {1}".format(
- param_name, func.__name__))
+ raise AssertionError(
+ "Unknown parameter name {0} specified for action {1}".format(
+ param_name, func.__name__
+ )
+ )
if field_type not in valid_fields:
- raise AssertionError("Unknown field type {0} specified for action {1} param {2}".format(
- field_type, func.__name__, param_name))
+ raise AssertionError(
+ "Unknown field type {0} specified for action {1} param {2}".format(
+ field_type, func.__name__, param_name
+ )
+ )
def rule_action(label=None, params=None):
@@ -76,8 +83,9 @@ def wrapper(func):
label=fn_name_to_pretty_label(key),
name=key,
fieldType=getattr(value, "field_type", value),
- defaultValue=getattr(value, "default_value", None)
- ) for key, value in params.items()
+ defaultValue=getattr(value, "default_value", None),
+ )
+ for key, value in params.items()
]
_validate_action_parameters(func, params_)
@@ -91,7 +99,7 @@ def wrapper(func):
return wrapper
+@dataclass
class ActionParam:
- def __init__(self, field_type, default_value=None):
- self.field_type = field_type
- self.default_value = default_value
+ field_type: type
+ default_value: Optional[int]
diff --git a/business_rules/engine.py b/business_rules/engine.py
index dc82c60a..90118ce8 100644
--- a/business_rules/engine.py
+++ b/business_rules/engine.py
@@ -1,18 +1,18 @@
-from __future__ import absolute_import
-import inspect
import logging
+from inspect import getfullargspec
+from typing import List
from . import utils
from .fields import FIELD_NO_INPUT
from .models import ConditionResult
from .util import method_type
-from .util.compat import getfullargspec
logger = logging.getLogger(__name__)
-def run_all(rule_list, defined_variables, defined_actions, stop_on_first_trigger=False):
- # type: (...) -> List[bool]
+def run_all(
+ rule_list, defined_variables, defined_actions, stop_on_first_trigger=False
+) -> List[bool]:
results = [False] * len(rule_list)
for i, rule in enumerate(rule_list):
result = run(rule, defined_variables, defined_actions)
@@ -24,10 +24,12 @@ def run_all(rule_list, defined_variables, defined_actions, stop_on_first_trigger
def run(rule, defined_variables, defined_actions):
- conditions, actions = rule.get('conditions'), rule['actions']
+ conditions, actions = rule.get("conditions"), rule["actions"]
if conditions is not None:
- rule_triggered, checked_conditions_results = check_conditions_recursively(conditions, defined_variables, rule)
+ rule_triggered, checked_conditions_results = check_conditions_recursively(
+ conditions, defined_variables, rule
+ )
else:
# If there are no conditions then trigger actions
rule_triggered = True
@@ -46,29 +48,36 @@ def check_conditions_recursively(conditions, defined_variables, rule):
This method checks all conditions including embedded ones.
:param conditions: Conditions to be checked
- :param defined_variables: BaseVariables instance to get variables values to check Conditions
+ :param defined_variables: BaseVariables instance to get variables values to check
+ Conditions
:param rule: Original rule where Conditions and Actions are defined
- :return: tuple with result of condition check and list of checked conditions with each individual result.
+ :return: tuple with result of condition check and list of checked conditions with
+ each individual result.
(condition_result, [(condition1_result), (condition2_result)]
- condition1_result = (condition_result, variable name, condition operator, condition value, condition params)
+ condition1_result = (condition_result, variable name, condition operator,
+ condition value, condition params)
"""
keys = list(conditions.keys())
- if keys == ['all']:
- assert len(conditions['all']) >= 1
+ if keys == ["all"]:
+ assert len(conditions["all"]) >= 1
matches = []
- for condition in conditions['all']:
- check_condition_result, matches_results = check_conditions_recursively(condition, defined_variables, rule)
+ for condition in conditions["all"]:
+ check_condition_result, matches_results = check_conditions_recursively(
+ condition, defined_variables, rule
+ )
matches.extend(matches_results)
if not check_condition_result:
return False, []
return True, matches
- elif keys == ['any']:
- assert len(conditions['any']) >= 1
- for condition in conditions['any']:
- check_condition_result, matches_results = check_conditions_recursively(condition, defined_variables, rule)
+ elif keys == ["any"]:
+ assert len(conditions["any"]) >= 1
+ for condition in conditions["any"]:
+ check_condition_result, matches_results = check_conditions_recursively(
+ condition, defined_variables, rule
+ )
if check_condition_result:
return True, matches_results
return False, []
@@ -76,7 +85,7 @@ def check_conditions_recursively(conditions, defined_variables, rule):
else:
# help prevent errors - any and all can only be in the condition dict
# if they're the only item
- assert not ('any' in keys or 'all' in keys)
+ assert not ("any" in keys or "all" in keys)
result = check_condition(conditions, defined_variables, rule)
return result[0], [result]
@@ -101,11 +110,16 @@ def check_condition(condition, defined_variables, rule):
condition params: {}
)
"""
- name, op, value = condition['name'], condition['operator'], condition['value']
- params = condition.get('params', {})
+ name, op, value = condition["name"], condition["operator"], condition["value"]
+ params = condition.get("params", {})
operator_type = _get_variable_value(defined_variables, name, params, rule)
- return ConditionResult(result=_do_operator_comparison(operator_type, op, value), name=name, operator=op,
- value=value, parameters=params)
+ return ConditionResult(
+ result=_do_operator_comparison(operator_type, op, value),
+ name=name,
+ operator=op,
+ value=value,
+ parameters=params,
+ )
def _get_variable_value(defined_variables, name, params, rule):
@@ -124,10 +138,15 @@ def _get_variable_value(defined_variables, name, params, rule):
method = getattr(defined_variables, name, None)
if method is None:
- raise AssertionError("Variable {0} is not defined in class {1}".format(
- name, defined_variables.__class__.__name__))
+ raise AssertionError(
+ "Variable {0} is not defined in class {1}".format(
+ name, defined_variables.__class__.__name__
+ )
+ )
- utils.check_params_valid_for_method(method, params, method_type.METHOD_TYPE_VARIABLE)
+ utils.check_params_valid_for_method(
+ method, params, method_type.METHOD_TYPE_VARIABLE
+ )
method_params = _build_variable_parameters(method, params, rule)
variable_value = method(**method_params)
@@ -149,11 +168,14 @@ def _do_operator_comparison(operator_type, operator_name, comparison_value):
"""
def fallback(*args, **kwargs):
- raise AssertionError("Operator {0} does not exist for type {1}".format(
- operator_name, operator_type.__class__.__name__))
+ raise AssertionError(
+ "Operator {0} does not exist for type {1}".format(
+ operator_name, operator_type.__class__.__name__
+ )
+ )
method = getattr(operator_type, operator_name, fallback)
- if getattr(method, 'input_type', '') == FIELD_NO_INPUT:
+ if getattr(method, "input_type", "") == FIELD_NO_INPUT:
return method()
return method(comparison_value)
@@ -172,8 +194,8 @@ def do_actions(actions, defined_actions, checked_conditions_results, rule):
"param1": value
}
}
- :param defined_actions: Class with function that implement the logic for each possible action defined in
- 'actions' parameter
+ :param defined_actions: Class with function that implement the logic for each
+ possible action defined in 'actions' parameter
:param checked_conditions_results:
:param rule: Rule that is being executed
:return: None
@@ -183,41 +205,50 @@ def do_actions(actions, defined_actions, checked_conditions_results, rule):
successful_conditions = [x for x in checked_conditions_results if x[0]]
for action in actions:
- method_name = action['name']
- action_params = action.get('params', {})
+ method_name = action["name"]
+ action_params = action.get("params", {})
method = getattr(defined_actions, method_name, None)
if not method:
raise AssertionError(
- "Action {0} is not defined in class {1}".format(method_name, defined_actions.__class__.__name__))
+ "Action {0} is not defined in class {1}".format(
+ method_name, defined_actions.__class__.__name__
+ )
+ )
- missing_params_with_default_value = utils.check_params_valid_for_method(method, action_params,
- method_type.METHOD_TYPE_ACTION)
+ missing_params_with_default_value = utils.check_params_valid_for_method(
+ method, action_params, method_type.METHOD_TYPE_ACTION
+ )
if missing_params_with_default_value:
- action_params = _set_default_values_for_missing_action_params(method,
- missing_params_with_default_value,
- action_params)
+ action_params = _set_default_values_for_missing_action_params(
+ method, missing_params_with_default_value, action_params
+ )
- method_params = _build_action_parameters(method, action_params, rule, successful_conditions)
+ method_params = _build_action_parameters(
+ method, action_params, rule, successful_conditions
+ )
method(**method_params)
-def _set_default_values_for_missing_action_params(method, missing_parameters_with_default_value, action_params):
+def _set_default_values_for_missing_action_params(
+ method, missing_parameters_with_default_value, action_params
+):
"""
Adds default parameter from method params to Action parameters.
:param method: Action object.
- :param parameters_with_default_value: set of parameters which have a default value for Action parameters.
+ :param parameters_with_default_value: set of parameters which have a default value
+ for Action parameters.
:param action_params: Action parameters dict.
:return: Modified action_params.
"""
modified_action_params = {}
- if getattr(method, 'params', None):
+ if getattr(method, "params", None):
for param in method.params:
- param_name = param['name']
+ param_name = param["name"]
if param_name in missing_parameters_with_default_value:
- default_value = param.get('defaultValue', None)
+ default_value = param.get("defaultValue", None)
if default_value is not None:
modified_action_params[param_name] = default_value
continue
@@ -234,10 +265,7 @@ def _build_action_parameters(method, parameters, rule, conditions):
:param conditions:
:return:
"""
- extra_parameters = {
- 'rule': rule,
- 'conditions': conditions
- }
+ extra_parameters = {"rule": rule, "conditions": conditions}
return _build_parameters(method, parameters, extra_parameters)
@@ -251,7 +279,7 @@ def _build_variable_parameters(method, parameters, rule):
:return:
"""
extra_parameters = {
- 'rule': rule,
+ "rule": rule,
}
return _build_parameters(method, parameters, extra_parameters)
diff --git a/business_rules/models.py b/business_rules/models.py
index c576fea5..a5677a30 100644
--- a/business_rules/models.py
+++ b/business_rules/models.py
@@ -1,4 +1,7 @@
from __future__ import absolute_import
+
from collections import namedtuple
-ConditionResult = namedtuple('ConditionResult', ['result', 'name', 'operator', 'value', 'parameters'])
+ConditionResult = namedtuple(
+ 'ConditionResult', ['result', 'name', 'operator', 'value', 'parameters']
+)
diff --git a/business_rules/operators.py b/business_rules/operators.py
index 17de63b2..e344d6d1 100644
--- a/business_rules/operators.py
+++ b/business_rules/operators.py
@@ -1,16 +1,18 @@
-from __future__ import absolute_import
-import calendar
import inspect
import re
from datetime import date, datetime, time
from decimal import Decimal
from functools import wraps
-from six import integer_types, string_types
-
-from .fields import (FIELD_DATETIME, FIELD_NO_INPUT, FIELD_NUMERIC,
- FIELD_SELECT, FIELD_SELECT_MULTIPLE, FIELD_TEXT,
- FIELD_TIME)
+from .fields import (
+ FIELD_DATETIME,
+ FIELD_NO_INPUT,
+ FIELD_NUMERIC,
+ FIELD_SELECT,
+ FIELD_SELECT_MULTIPLE,
+ FIELD_TEXT,
+ FIELD_TIME,
+)
from .utils import float_to_decimal, fn_name_to_pretty_label
@@ -24,21 +26,21 @@ def _assert_valid_value_and_cast(self, value):
@classmethod
def get_all_operators(cls):
methods = inspect.getmembers(cls)
- return [{'name': m[0],
- 'label': m[1].label,
- 'input_type': m[1].input_type}
- for m in methods if getattr(m[1], 'is_operator', False)]
+ return [
+ {"name": m[0], "label": m[1].label, "input_type": m[1].input_type}
+ for m in methods
+ if getattr(m[1], "is_operator", False)
+ ]
def export_type(cls):
- """ Decorator to expose the given class to business_rules.export_rule_data. """
+ """Decorator to expose the given class to business_rules.export_rule_data."""
cls.export_in_rule_data = True
return cls
-def type_operator(input_type, label=None,
- assert_type_for_arguments=True):
- """ Decorator to make a function into a type operator.
+def type_operator(input_type, label=None, assert_type_for_arguments=True):
+ """Decorator to make a function into a type operator.
- assert_type_for_arguments - if True this patches the operator function
so that arguments passed to it will have _assert_valid_value_and_cast
@@ -66,13 +68,13 @@ def inner(self, *args, **kwargs):
@export_type
class StringType(BaseType):
+
name = "string"
def _assert_valid_value_and_cast(self, value):
value = value or ""
- if not isinstance(value, string_types):
- raise AssertionError("{0} is not a valid string type.".
- format(value))
+ if not isinstance(value, str):
+ raise AssertionError("{0} is not a valid string type.".format(value))
return value
@type_operator(FIELD_TEXT)
@@ -106,7 +108,7 @@ def non_empty(self):
@export_type
class NumericType(BaseType):
- EPSILON = Decimal('0.000001')
+ EPSILON = Decimal("0.000001")
name = "numeric"
@@ -115,13 +117,12 @@ def _assert_valid_value_and_cast(value):
if isinstance(value, float):
# In python 2.6, casting float to Decimal doesn't work
return float_to_decimal(value)
- if isinstance(value, integer_types):
+ if isinstance(value, int):
return Decimal(value)
if isinstance(value, Decimal):
return value
else:
- raise AssertionError("{0} is not a valid numeric type.".
- format(value))
+ raise AssertionError("{0} is not a valid numeric type.".format(value))
@type_operator(FIELD_NUMERIC)
def equal_to(self, other_numeric):
@@ -146,12 +147,12 @@ def less_than_or_equal_to(self, other_numeric):
@export_type
class BooleanType(BaseType):
+
name = "boolean"
def _assert_valid_value_and_cast(self, value):
if type(value) != bool:
- raise AssertionError("{0} is not a valid boolean type".
- format(value))
+ raise AssertionError("{0} is not a valid boolean type".format(value))
return value
@type_operator(FIELD_NO_INPUT)
@@ -165,18 +166,17 @@ def is_false(self):
@export_type
class SelectType(BaseType):
+
name = "select"
def _assert_valid_value_and_cast(self, value):
- if not hasattr(value, '__iter__'):
- raise AssertionError("{0} is not a valid select type".
- format(value))
+ if not hasattr(value, "__iter__"):
+ raise AssertionError("{0} is not a valid select type".format(value))
return value
@staticmethod
def _case_insensitive_equal_to(value_from_list, other_value):
- if isinstance(value_from_list, string_types) and \
- isinstance(other_value, string_types):
+ if isinstance(value_from_list, str) and isinstance(other_value, str):
return value_from_list.lower() == other_value.lower()
else:
return value_from_list == other_value
@@ -198,12 +198,14 @@ def does_not_contain(self, other_value):
@export_type
class SelectMultipleType(BaseType):
+
name = "select_multiple"
def _assert_valid_value_and_cast(self, value):
- if not hasattr(value, '__iter__'):
- raise AssertionError("{0} is not a valid select multiple type".
- format(value))
+ if not hasattr(value, "__iter__"):
+ raise AssertionError(
+ "{0} is not a valid select multiple type".format(value)
+ )
return value
@type_operator(FIELD_SELECT_MULTIPLE)
@@ -246,12 +248,13 @@ def shares_no_elements_with(self, other_value):
@export_type
class DateTimeType(BaseType):
name = "datetime"
- DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
- DATE_FORMAT = '%Y-%m-%d'
+ DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
+ DATE_FORMAT = "%Y-%m-%d"
def _assert_valid_value_and_cast(self, value):
"""
- Parse string with formats '%Y-%m-%dT%H:%M:%S' or '%Y-%m-%d' into datetime.datetime instance.
+ Parse string with formats '%Y-%m-%dT%H:%M:%S' or '%Y-%m-%d' into
+ datetime.datetime instance.
:param value:
:return:
@@ -315,8 +318,8 @@ def before_than_or_equal_to(self, other_datetime):
@export_type
class TimeType(BaseType):
name = "time"
- TIME_FORMAT = '%H:%M:%S'
- TIME_FORMAT_NO_SECONDS = '%H:%M'
+ TIME_FORMAT = "%H:%M:%S"
+ TIME_FORMAT_NO_SECONDS = "%H:%M"
def _assert_valid_value_and_cast(self, value):
"""
diff --git a/business_rules/util/compat.py b/business_rules/util/compat.py
deleted file mode 100644
index 0caba58f..00000000
--- a/business_rules/util/compat.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from __future__ import absolute_import, unicode_literals
-
-import sys
-from collections import namedtuple
-
-PY2 = sys.version_info[0] == 2
-
-# Taken from https://github.com/HypothesisWorks/hypothesis/pull/625/files#diff-e84a85b835af44101e1986c47ba39630R264
-if PY2:
- FullArgSpec = namedtuple('FullArgSpec', 'args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations')
-
- def getfullargspec(func):
- import inspect
- args, varargs, varkw, defaults = inspect.getargspec(func)
- return FullArgSpec(args, varargs, varkw, defaults, [], None, {})
-else:
- from inspect import getfullargspec
-
- if sys.version_info[:2] == (3, 5):
- # silence deprecation warnings on Python 3.5
- # (un-deprecated in 3.6 to allow single-source 2/3 code like this)
- def silence_warnings(func):
- import warnings
- import functools
-
- @functools.wraps(func)
- def inner(*args, **kwargs):
- with warnings.catch_warnings():
- warnings.simplefilter('ignore', DeprecationWarning)
- return func(*args, **kwargs)
- return inner
-
- getfullargspec = silence_warnings(getfullargspec)
diff --git a/business_rules/utils.py b/business_rules/utils.py
index e18eb385..7c6f02cb 100644
--- a/business_rules/utils.py
+++ b/business_rules/utils.py
@@ -1,4 +1,5 @@
from __future__ import absolute_import
+
import inspect
from decimal import Context, Decimal, Inexact
@@ -14,9 +15,11 @@ def export_rule_data(variables, actions):
Export_rule_data is used to export all information about the
variables, actions, and operators to the client. This will return a
dictionary with three keys:
- - variables: a list of all available variables along with their label, type, options and params
+ - variables: a list of all available variables along with their label, type,
+ options and params
- actions: a list of all actions along with their label and params
- - variable_type_operators: a dictionary of all field_types -> list of available operators
+ - variable_type_operators: a dictionary of all field_types -> list of available
+ operators
:param variables:
:param actions:
:return:
@@ -27,14 +30,16 @@ def export_rule_data(variables, actions):
variables_data = variables.get_all_variables()
variable_type_operators = {}
- for variable_class in inspect.getmembers(operators, lambda x: getattr(x, 'export_in_rule_data', False)):
+ for variable_class in inspect.getmembers(
+ operators, lambda x: getattr(x, 'export_in_rule_data', False)
+ ):
variable_type = variable_class[1] # getmembers returns (name, value)
variable_type_operators[variable_type.name] = variable_type.get_all_operators()
return {
"variables": variables_data,
"actions": actions_data,
- "variable_type_operators": variable_type_operators
+ "variable_type_operators": variable_type_operators,
}
@@ -89,21 +94,23 @@ def params_dict_to_list(params):
{
'label': fn_name_to_pretty_label(name),
'name': name,
- 'field_type': param_field_type
- } for name, param_field_type in params.items()
+ 'field_type': param_field_type,
+ }
+ for name, param_field_type in params.items()
]
def check_params_valid_for_method(method, given_params, method_type_name):
"""
- Verifies that the given parameters (defined in the Rule) match the names of those defined in
- the variable or action decorator. Raise an error if one of the sets contains a parameter that
- the other does not.
+ Verifies that the given parameters (defined in the Rule) match the names of those
+ defined in the variable or action decorator. Raise an error if one of the sets
+ contains a parameter that the other does not.
:param method:
:param given_params: Parameters defined within the Rule (Action or Condition)
:param method_type_name: A method type defined in util.method_type module
- :return: Set of default values for params which are missing but have a default value. Raise exception if parameters
+ :return: Set of default values for params which are missing but have a default
+ value. Raise exception if parameters
don't
match (defined in method and
Rule)
@@ -112,21 +119,30 @@ def check_params_valid_for_method(method, given_params, method_type_name):
defined_params = [param.get('name') for param in method_params]
missing_params = set(defined_params).difference(given_params)
- # check for default value in action parameters, if it is present, exclude param from missing params
+ # check for default value in action parameters, if it is present, exclude param
+ # from missing params
params_with_default_value = set()
if method_type_name == method_type.METHOD_TYPE_ACTION and missing_params:
- params_with_default_value = check_for_default_value_for_missing_params(missing_params, method_params)
+ params_with_default_value = check_for_default_value_for_missing_params(
+ missing_params, method_params
+ )
missing_params -= params_with_default_value
if missing_params:
- raise AssertionError("Missing parameters {0} for {1} {2}".format(
- ', '.join(missing_params), method_type_name, method.__name__))
+ raise AssertionError(
+ "Missing parameters {0} for {1} {2}".format(
+ ', '.join(missing_params), method_type_name, method.__name__
+ )
+ )
invalid_params = set(given_params).difference(defined_params)
if invalid_params:
- raise AssertionError("Invalid parameters {0} for {1} {2}".format(
- ', '.join(invalid_params), method_type_name, method.__name__))
+ raise AssertionError(
+ "Invalid parameters {0} for {1} {2}".format(
+ ', '.join(invalid_params), method_type_name, method.__name__
+ )
+ )
return params_with_default_value
@@ -134,7 +150,8 @@ def check_params_valid_for_method(method, given_params, method_type_name):
def check_for_default_value_for_missing_params(missing_params, method_params):
"""
:param missing_params: Params missing from Rule
- :param method_params: Params defined on method, which could have default value for missing param
+ :param method_params: Params defined on method, which could have default value for
+ missing param
[{
'label': 'action_label',
'name': 'action_parameter',
@@ -143,17 +160,103 @@ def check_for_default_value_for_missing_params(missing_params, method_params):
},
...
]
- :return Params that are missing from rule but have default params: {'action_parameter'}
+ :return Params that are missing from rule but have
+ default params: {'action_parameter'}
"""
missing_params_with_default_value = set()
if method_params:
for param in method_params:
- if param['name'] in missing_params and param.get('defaultValue', None) is not None:
+ if (
+ param['name'] in missing_params
+ and param.get('defaultValue', None) is not None
+ ):
missing_params_with_default_value.add(param['name'])
return missing_params_with_default_value
+def validate_root_keys(rule):
+ """
+ Check the root object contains both 'actions' & 'conditions'
+ """
+ root_keys = list(rule.keys())
+ if 'actions' not in root_keys:
+ raise AssertionError('Missing "{}" key'.format('actions'))
+
+
+def validate_condition_operator(condition, rule_schema):
+ """
+ Check provided condition contains a valid operator
+ """
+ if "operator" not in condition:
+ raise AssertionError(
+ 'Missing "operator" key for condition {}'.format(condition.get('name'))
+ )
+ for item in rule_schema.get('variables'):
+ if item.get('name') == condition.get('name'):
+ condition_field_type = item.get('field_type')
+ variable_operators = rule_schema.get('variable_type_operators', {}).get(
+ condition_field_type, []
+ )
+ for operators in variable_operators:
+ if operators['name'] == condition['operator']:
+ return True
+ raise AssertionError('Unknown operator "{}"'.format(condition['operator']))
+ raise AssertionError('Name "{}" not supported'.format(condition.get('name')))
+
+
+def validate_condition_name(condition, variables):
+ """
+ Check provided condition contains a 'name' key and the value is valid
+ """
+ condition_name = condition.get('name')
+ if not condition_name:
+ raise AssertionError('Missing condition "name" key in {}'.format(condition))
+ if not hasattr(variables, condition_name):
+ raise AssertionError('Unknown condition "{}"'.format(condition_name))
+
+
+def validate_condition(condition, variables, rule_schema):
+ validate_condition_name(condition, variables)
+ validate_condition_operator(condition, rule_schema)
+ method = getattr(variables, condition.get('name'))
+ params = condition.get('params', {})
+ check_params_valid_for_method(method, params, method_type.METHOD_TYPE_VARIABLE)
+
+
+def validate_conditions(input_conditions, rule_schema, variables):
+ """
+ Recursively check all levels of input conditions
+ """
+ if isinstance(input_conditions, list):
+ for condition in input_conditions:
+ validate_conditions(condition, rule_schema, variables)
+ if isinstance(input_conditions, dict):
+ keys = list(input_conditions.keys())
+ if 'any' in keys or 'all' in keys:
+ if len(keys) > 1:
+ raise AssertionError(
+ 'Expected ONE of "any" or "all" but found {}'.format(keys)
+ )
+ else:
+ for _, v in input_conditions.items():
+ validate_conditions(v, rule_schema, variables)
+ else:
+ validate_condition(input_conditions, variables, rule_schema)
+
+
+def validate_actions(input_actions, actions):
+ """
+ Check all input actions contain valid names and parameters for defined actions
+ """
+ if type(input_actions) is not list:
+ raise AssertionError('"actions" key must be a list')
+ for action in input_actions:
+ method = getattr(actions, action.get('name'), None)
+ params = action.get('params', {})
+ check_params_valid_for_method(method, params, method_type.METHOD_TYPE_ACTION)
+
+
def validate_rule_data(variables, actions, rule):
"""
validate_rule_data is used to check a generated rule against a set of variables and actions
@@ -163,83 +266,11 @@ def validate_rule_data(variables, actions, rule):
:return: bool
:raises AssertionError:
"""
- def validate_root_keys(rule):
- """
- Check the root object contains both 'actions' & 'conditions'
- """
- root_keys = list(rule.keys())
- if 'actions' not in root_keys:
- raise AssertionError('Missing "{}" key'.format('actions'))
-
- def validate_condition_operator(condition, rule_schema):
- """
- Check provided condition contains a valid operator
- """
- if "operator" not in condition:
- raise AssertionError('Missing "operator" key for condition {}'.format(condition.get('name')))
- for item in rule_schema.get('variables'):
- if item.get('name') == condition.get('name'):
- condition_field_type = item.get('field_type')
- variable_operators = rule_schema.get('variable_type_operators', {}).get(condition_field_type, [])
- for operators in variable_operators:
- if operators['name'] == condition['operator']:
- return True
- raise AssertionError('Unknown operator "{}"'.format(condition['operator']))
- raise AssertionError('Name "{}" not supported'.format(condition.get('name')))
-
- def validate_condition_name(condition, variables):
- """
- Check provided condition contains a 'name' key and the value is valid
- """
- condition_name = condition.get('name')
- if not condition_name:
- raise AssertionError('Missing condition "name" key in {}'.format(condition))
- if not hasattr(variables, condition_name):
- raise AssertionError('Unknown condition "{}"'.format(condition_name))
-
- def validate_condition(condition, variables, rule_schema):
- validate_condition_name(condition, variables)
- validate_condition_operator(condition, rule_schema)
- method = getattr(variables, condition.get('name'))
- params = condition.get('params', {})
- check_params_valid_for_method(method, params, method_type.METHOD_TYPE_VARIABLE)
-
- def validate_conditions(input_conditions, rule_schema):
- """
- Recursively check all levels of input conditions
- """
- import six
-
- if isinstance(input_conditions, list):
- for condition in input_conditions:
- validate_conditions(condition, rule_schema)
- if isinstance(input_conditions, dict):
- keys = list(input_conditions.keys())
- if 'any' in keys or 'all' in keys:
- if len(keys) > 1:
- raise AssertionError('Expected ONE of "any" or "all" but found {}'.format(keys))
- else:
- for _, v in six.iteritems(input_conditions):
- validate_conditions(v, rule_schema)
- else:
- validate_condition(input_conditions, variables, rule_schema)
-
- def validate_actions(input_actions):
- """
- Check all input actions contain valid names and parameters for defined actions
- """
- if type(input_actions) is not list:
- raise AssertionError('"actions" key must be a list')
- for action in input_actions:
- method = getattr(actions, action.get('name'), None)
- params = action.get('params', {})
- check_params_valid_for_method(method, params, method_type.METHOD_TYPE_ACTION)
-
rule_schema = export_rule_data(variables, actions)
validate_root_keys(rule)
conditions = rule.get('conditions', None)
if conditions is not None and type(conditions) is not dict:
raise AssertionError('"conditions" must be a dictionary')
- validate_conditions(conditions, rule_schema)
- validate_actions(rule.get('actions'))
+ validate_conditions(conditions, rule_schema, variables)
+ validate_actions(rule.get('actions'), actions)
return True
diff --git a/business_rules/variables.py b/business_rules/variables.py
index 6e44a2c9..9f567324 100644
--- a/business_rules/variables.py
+++ b/business_rules/variables.py
@@ -1,11 +1,19 @@
from __future__ import absolute_import
-import inspect
+import inspect
from typing import Callable, List, Type # noqa: F401
from . import utils
-from .operators import (BaseType, BooleanType, DateTimeType, NumericType,
- SelectMultipleType, SelectType, StringType, TimeType)
+from .operators import (
+ BaseType,
+ BooleanType,
+ DateTimeType,
+ NumericType,
+ SelectMultipleType,
+ SelectType,
+ StringType,
+ TimeType,
+)
from .utils import fn_name_to_pretty_label
@@ -26,7 +34,9 @@ def get_all_variables(cls):
'options': m[1].options,
'params': m[1].params,
'public': m[1].public,
- } for m in methods if getattr(m[1], 'is_rule_variable', False)
+ }
+ for m in methods
+ if getattr(m[1], 'is_rule_variable', False)
]
@@ -47,8 +57,10 @@ def rule_variable(field_type, label=None, options=None, params=None, public=True
def wrapper(func):
if not (type(field_type) == type and issubclass(field_type, BaseType)):
- raise AssertionError("{0} is not instance of BaseType in" \
- " rule_variable field_type".format(field_type))
+ raise AssertionError(
+ "{0} is not instance of BaseType in"
+ " rule_variable field_type".format(field_type)
+ )
params_wrapper = utils.params_dict_to_list(params)
@@ -71,7 +83,9 @@ def _rule_variable_wrapper(field_type, label, params=None, options=None, public=
# Decorator is being called with no args, label is actually the decorated func
return rule_variable(field_type, params=params, public=public)(label)
- return rule_variable(field_type, label=label, params=params, options=options, public=public)
+ return rule_variable(
+ field_type, label=label, params=params, options=options, public=public
+ )
def numeric_rule_variable(label=None, params=None, public=True):
@@ -101,7 +115,9 @@ def string_rule_variable(label=None, params=None, options=None, public=True):
:param public: Flag to identify if a variable is public or not
:return: Decorator function wrapper
"""
- return _rule_variable_wrapper(StringType, label, params=params, options=options, public=public)
+ return _rule_variable_wrapper(
+ StringType, label, params=params, options=options, public=public
+ )
def boolean_rule_variable(label=None, params=None, public=True):
@@ -130,7 +146,9 @@ def select_rule_variable(label=None, options=None, params=None, public=True):
:param public: Flag to identify if a variable is public or not
:return: Decorator function wrapper
"""
- return rule_variable(SelectType, label=label, options=options, params=params, public=public)
+ return rule_variable(
+ SelectType, label=label, options=options, params=params, public=public
+ )
def select_multiple_rule_variable(label=None, options=None, params=None, public=True):
@@ -145,7 +163,9 @@ def select_multiple_rule_variable(label=None, options=None, params=None, public=
:param public: Flag to identify if a variable is public or not
:return: Decorator function wrapper
"""
- return rule_variable(SelectMultipleType, label=label, options=options, params=params, public=public)
+ return rule_variable(
+ SelectMultipleType, label=label, options=options, params=params, public=public
+ )
def datetime_rule_variable(label=None, params=None, public=True):
@@ -160,7 +180,9 @@ def datetime_rule_variable(label=None, params=None, public=True):
:return: Decorator function wrapper for DateTime values
"""
- return _rule_variable_wrapper(field_type=DateTimeType, label=label, params=params, public=public)
+ return _rule_variable_wrapper(
+ field_type=DateTimeType, label=label, params=params, public=public
+ )
def time_rule_variable(label=None, params=None, public=True):
@@ -174,7 +196,9 @@ def time_rule_variable(label=None, params=None, public=True):
:return: Decorator function wrapper for Time values
"""
- return _rule_variable_wrapper(field_type=TimeType, label=label, params=params, public=public)
+ return _rule_variable_wrapper(
+ field_type=TimeType, label=label, params=params, public=public
+ )
def _validate_variable_parameters(func, params):
@@ -192,9 +216,15 @@ def _validate_variable_parameters(func, params):
param_name, field_type = param['name'], param['field_type']
if param_name not in func.__code__.co_varnames:
- raise AssertionError("Unknown parameter name {0} specified for variable {1}".format(
- param_name, func.__name__))
+ raise AssertionError(
+ "Unknown parameter name {0} specified for variable {1}".format(
+ param_name, func.__name__
+ )
+ )
if field_type not in valid_fields:
- raise AssertionError("Unknown field type {0} specified for variable {1} param {2}".format(
- field_type, func.__name__, param_name))
+ raise AssertionError(
+ "Unknown field type {0} specified for variable {1} param {2}".format(
+ field_type, func.__name__, param_name
+ )
+ )
diff --git a/makefile b/makefile
index 0a3847ce..664bb8ff 100644
--- a/makefile
+++ b/makefile
@@ -2,12 +2,23 @@
clean:
-find . -type f -name "*.pyc" -delete
+ poetry env remove 3.11 || true
+ poetry env use 3.11
deps:
- pip install -r requirements-dev.txt
+ poetry install
test:
- py.test $(pytest_args)
+ poetry run py.test $(pytest_args)
coverage:
- py.test --cov-report term-missing --cov=./business_rules $(pytest_args)
+ mkdir -p test-results
+ poetry run py.test --junitxml=test-results/junit.xml --cov-report term-missing --cov=./business_rules $(pytest_args)
+ poetry run coverage html # open htmlcov/index.html in a browser
+
+merge-upstream:
+ # Merge the venmo/business-rules upstream master branch to our fork
+ # Once this command completes there will likely be conflicts so you will need to fix them and commit the changes.
+ git remote add upstream git@github.com:venmo/business-rules.git
+ git fetch upstream
+ git merge upstream/master
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 00000000..9c290cf0
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,443 @@
+# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
+
+[[package]]
+name = "cachetools"
+version = "5.3.0"
+description = "Extensible memoizing collections and decorators"
+category = "dev"
+optional = false
+python-versions = "~=3.7"
+files = [
+ {file = "cachetools-5.3.0-py3-none-any.whl", hash = "sha256:429e1a1e845c008ea6c85aa35d4b98b65d6a9763eeef3e37e92728a12d1de9d4"},
+ {file = "cachetools-5.3.0.tar.gz", hash = "sha256:13dfddc7b8df938c21a940dfa6557ce6e94a2f1cdfa58eb90c805721d58f2c14"},
+]
+
+[[package]]
+name = "chardet"
+version = "5.1.0"
+description = "Universal encoding detector for Python 3"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "chardet-5.1.0-py3-none-any.whl", hash = "sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9"},
+ {file = "chardet-5.1.0.tar.gz", hash = "sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5"},
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "coverage"
+version = "7.2.3"
+description = "Code coverage measurement for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "coverage-7.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e58c0d41d336569d63d1b113bd573db8363bc4146f39444125b7f8060e4e04f5"},
+ {file = "coverage-7.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:344e714bd0fe921fc72d97404ebbdbf9127bac0ca1ff66d7b79efc143cf7c0c4"},
+ {file = "coverage-7.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974bc90d6f6c1e59ceb1516ab00cf1cdfbb2e555795d49fa9571d611f449bcb2"},
+ {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0743b0035d4b0e32bc1df5de70fba3059662ace5b9a2a86a9f894cfe66569013"},
+ {file = "coverage-7.2.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d0391fb4cfc171ce40437f67eb050a340fdbd0f9f49d6353a387f1b7f9dd4fa"},
+ {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a42e1eff0ca9a7cb7dc9ecda41dfc7cbc17cb1d02117214be0561bd1134772b"},
+ {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:be19931a8dcbe6ab464f3339966856996b12a00f9fe53f346ab3be872d03e257"},
+ {file = "coverage-7.2.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72fcae5bcac3333a4cf3b8f34eec99cea1187acd55af723bcbd559adfdcb5535"},
+ {file = "coverage-7.2.3-cp310-cp310-win32.whl", hash = "sha256:aeae2aa38395b18106e552833f2a50c27ea0000122bde421c31d11ed7e6f9c91"},
+ {file = "coverage-7.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:83957d349838a636e768251c7e9979e899a569794b44c3728eaebd11d848e58e"},
+ {file = "coverage-7.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dfd393094cd82ceb9b40df4c77976015a314b267d498268a076e940fe7be6b79"},
+ {file = "coverage-7.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182eb9ac3f2b4874a1f41b78b87db20b66da6b9cdc32737fbbf4fea0c35b23fc"},
+ {file = "coverage-7.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bb1e77a9a311346294621be905ea8a2c30d3ad371fc15bb72e98bfcfae532df"},
+ {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca0f34363e2634deffd390a0fef1aa99168ae9ed2af01af4a1f5865e362f8623"},
+ {file = "coverage-7.2.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55416d7385774285b6e2a5feca0af9652f7f444a4fa3d29d8ab052fafef9d00d"},
+ {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06ddd9c0249a0546997fdda5a30fbcb40f23926df0a874a60a8a185bc3a87d93"},
+ {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fff5aaa6becf2c6a1699ae6a39e2e6fb0672c2d42eca8eb0cafa91cf2e9bd312"},
+ {file = "coverage-7.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ea53151d87c52e98133eb8ac78f1206498c015849662ca8dc246255265d9c3c4"},
+ {file = "coverage-7.2.3-cp311-cp311-win32.whl", hash = "sha256:8f6c930fd70d91ddee53194e93029e3ef2aabe26725aa3c2753df057e296b925"},
+ {file = "coverage-7.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:fa546d66639d69aa967bf08156eb8c9d0cd6f6de84be9e8c9819f52ad499c910"},
+ {file = "coverage-7.2.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b2317d5ed777bf5a033e83d4f1389fd4ef045763141d8f10eb09a7035cee774c"},
+ {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be9824c1c874b73b96288c6d3de793bf7f3a597770205068c6163ea1f326e8b9"},
+ {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c3b2803e730dc2797a017335827e9da6da0e84c745ce0f552e66400abdfb9a1"},
+ {file = "coverage-7.2.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f69770f5ca1994cb32c38965e95f57504d3aea96b6c024624fdd5bb1aa494a1"},
+ {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1127b16220f7bfb3f1049ed4a62d26d81970a723544e8252db0efde853268e21"},
+ {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:aa784405f0c640940595fa0f14064d8e84aff0b0f762fa18393e2760a2cf5841"},
+ {file = "coverage-7.2.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:3146b8e16fa60427e03884301bf8209221f5761ac754ee6b267642a2fd354c48"},
+ {file = "coverage-7.2.3-cp37-cp37m-win32.whl", hash = "sha256:1fd78b911aea9cec3b7e1e2622c8018d51c0d2bbcf8faaf53c2497eb114911c1"},
+ {file = "coverage-7.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0f3736a5d34e091b0a611964c6262fd68ca4363df56185902528f0b75dbb9c1f"},
+ {file = "coverage-7.2.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:981b4df72c93e3bc04478153df516d385317628bd9c10be699c93c26ddcca8ab"},
+ {file = "coverage-7.2.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0045f8f23a5fb30b2eb3b8a83664d8dc4fb58faddf8155d7109166adb9f2040"},
+ {file = "coverage-7.2.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f760073fcf8f3d6933178d67754f4f2d4e924e321f4bb0dcef0424ca0215eba1"},
+ {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c86bd45d1659b1ae3d0ba1909326b03598affbc9ed71520e0ff8c31a993ad911"},
+ {file = "coverage-7.2.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:172db976ae6327ed4728e2507daf8a4de73c7cc89796483e0a9198fd2e47b462"},
+ {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2a3a6146fe9319926e1d477842ca2a63fe99af5ae690b1f5c11e6af074a6b5c"},
+ {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f649dd53833b495c3ebd04d6eec58479454a1784987af8afb77540d6c1767abd"},
+ {file = "coverage-7.2.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7c4ed4e9f3b123aa403ab424430b426a1992e6f4c8fd3cb56ea520446e04d152"},
+ {file = "coverage-7.2.3-cp38-cp38-win32.whl", hash = "sha256:eb0edc3ce9760d2f21637766c3aa04822030e7451981ce569a1b3456b7053f22"},
+ {file = "coverage-7.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:63cdeaac4ae85a179a8d6bc09b77b564c096250d759eed343a89d91bce8b6367"},
+ {file = "coverage-7.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20d1a2a76bb4eb00e4d36b9699f9b7aba93271c9c29220ad4c6a9581a0320235"},
+ {file = "coverage-7.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ea748802cc0de4de92ef8244dd84ffd793bd2e7be784cd8394d557a3c751e21"},
+ {file = "coverage-7.2.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b154aba06df42e4b96fc915512ab39595105f6c483991287021ed95776d934"},
+ {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd214917cabdd6f673a29d708574e9fbdb892cb77eb426d0eae3490d95ca7859"},
+ {file = "coverage-7.2.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2e58e45fe53fab81f85474e5d4d226eeab0f27b45aa062856c89389da2f0d9"},
+ {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:87ecc7c9a1a9f912e306997ffee020297ccb5ea388421fe62a2a02747e4d5539"},
+ {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:387065e420aed3c71b61af7e82c7b6bc1c592f7e3c7a66e9f78dd178699da4fe"},
+ {file = "coverage-7.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ea3f5bc91d7d457da7d48c7a732beaf79d0c8131df3ab278e6bba6297e23c6c4"},
+ {file = "coverage-7.2.3-cp39-cp39-win32.whl", hash = "sha256:ae7863a1d8db6a014b6f2ff9c1582ab1aad55a6d25bac19710a8df68921b6e30"},
+ {file = "coverage-7.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:3f04becd4fcda03c0160d0da9c8f0c246bc78f2f7af0feea1ec0930e7c93fa4a"},
+ {file = "coverage-7.2.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:965ee3e782c7892befc25575fa171b521d33798132692df428a09efacaffe8d0"},
+ {file = "coverage-7.2.3.tar.gz", hash = "sha256:d298c2815fa4891edd9abe5ad6e6cb4207104c7dd9fd13aea3fdebf6f9b91259"},
+]
+
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "distlib"
+version = "0.3.6"
+description = "Distribution utilities"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
+ {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.1.1"
+description = "Backport of PEP 654 (exception groups)"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"},
+ {file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"},
+]
+
+[package.extras]
+test = ["pytest (>=6)"]
+
+[[package]]
+name = "filelock"
+version = "3.12.0"
+description = "A platform independent file lock."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "filelock-3.12.0-py3-none-any.whl", hash = "sha256:ad98852315c2ab702aeb628412cbf7e95b7ce8c3bf9565670b4eaecf1db370a9"},
+ {file = "filelock-3.12.0.tar.gz", hash = "sha256:fc03ae43288c013d2ea83c8597001b1129db351aad9c57fe2409327916b8e718"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
+
+[[package]]
+name = "importlib-metadata"
+version = "6.6.0"
+description = "Read metadata from Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "importlib_metadata-6.6.0-py3-none-any.whl", hash = "sha256:43dd286a2cd8995d5eaef7fee2066340423b818ed3fd70adf0bad5f1fac53fed"},
+ {file = "importlib_metadata-6.6.0.tar.gz", hash = "sha256:92501cdf9cc66ebd3e612f1b4f0c0765dfa42f0fa38ffb319b6bd84dd675d705"},
+]
+
+[package.dependencies]
+typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
+zipp = ">=0.5"
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+perf = ["ipython"]
+testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+description = "brain-dead simple config-ini parsing"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
+ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
+]
+
+[[package]]
+name = "mock"
+version = "5.0.2"
+description = "Rolling backport of unittest.mock for all Pythons"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "mock-5.0.2-py3-none-any.whl", hash = "sha256:0e0bc5ba78b8db3667ad636d964eb963dc97a59f04c6f6214c5f0e4a8f726c56"},
+ {file = "mock-5.0.2.tar.gz", hash = "sha256:06f18d7d65b44428202b145a9a36e99c2ee00d1eb992df0caf881d4664377891"},
+]
+
+[package.extras]
+build = ["blurb", "twine", "wheel"]
+docs = ["sphinx"]
+test = ["pytest", "pytest-cov"]
+
+[[package]]
+name = "nose"
+version = "1.3.7"
+description = "nose extends unittest to make testing easier"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "nose-1.3.7-py2-none-any.whl", hash = "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a"},
+ {file = "nose-1.3.7-py3-none-any.whl", hash = "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac"},
+ {file = "nose-1.3.7.tar.gz", hash = "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98"},
+]
+
+[[package]]
+name = "nose-run-line-number"
+version = "0.0.2"
+description = "Nose plugin to run tests by line number"
+category = "dev"
+optional = false
+python-versions = "*"
+files = [
+ {file = "nose-run-line-number-0.0.2.tar.gz", hash = "sha256:521ed2d1c4259d7cc0cab84225e63b1d6f7c7582b580263a2b7c19f2591cb1d4"},
+]
+
+[[package]]
+name = "packaging"
+version = "23.1"
+description = "Core utilities for Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
+ {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
+]
+
+[[package]]
+name = "platformdirs"
+version = "3.4.0"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "platformdirs-3.4.0-py3-none-any.whl", hash = "sha256:01437886022decaf285d8972f9526397bfae2ac55480ed372ed6d9eca048870a"},
+ {file = "platformdirs-3.4.0.tar.gz", hash = "sha256:a5e1536e5ea4b1c238a1364da17ff2993d5bd28e15600c2c8224008aff6bbcad"},
+]
+
+[package.dependencies]
+typing-extensions = {version = ">=4.5", markers = "python_version < \"3.8\""}
+
+[package.extras]
+docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"]
+
+[[package]]
+name = "pluggy"
+version = "1.0.0"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
+ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+]
+
+[package.dependencies]
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "pyproject-api"
+version = "1.5.1"
+description = "API to interact with the python pyproject.toml based projects"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pyproject_api-1.5.1-py3-none-any.whl", hash = "sha256:4698a3777c2e0f6b624f8a4599131e2a25376d90fe8d146d7ac74c67c6f97c43"},
+ {file = "pyproject_api-1.5.1.tar.gz", hash = "sha256:435f46547a9ff22cf4208ee274fca3e2869aeb062a4834adfc99a4dd64af3cf9"},
+]
+
+[package.dependencies]
+packaging = ">=23"
+tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
+
+[package.extras]
+docs = ["furo (>=2022.12.7)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"]
+testing = ["covdefaults (>=2.2.2)", "importlib-metadata (>=6)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "virtualenv (>=20.17.1)", "wheel (>=0.38.4)"]
+
+[[package]]
+name = "pytest"
+version = "7.3.1"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"},
+ {file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
+
+[[package]]
+name = "pytest-cov"
+version = "4.0.0"
+description = "Pytest plugin for measuring coverage."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"},
+ {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"},
+]
+
+[package.dependencies]
+coverage = {version = ">=5.2.1", extras = ["toml"]}
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
+
+[[package]]
+name = "pytz"
+version = "2023.3"
+description = "World timezone definitions, modern and historical"
+category = "main"
+optional = false
+python-versions = "*"
+files = [
+ {file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"},
+ {file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"},
+]
+
+[[package]]
+name = "tomli"
+version = "2.0.1"
+description = "A lil' TOML parser"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
+ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+]
+
+[[package]]
+name = "tox"
+version = "4.5.1"
+description = "tox is a generic virtualenv management and test command line tool"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "tox-4.5.1-py3-none-any.whl", hash = "sha256:d25a2e6cb261adc489604fafd76cd689efeadfa79709965e965668d6d3f63046"},
+ {file = "tox-4.5.1.tar.gz", hash = "sha256:5a2eac5fb816779dfdf5cb00fecbc27eb0524e4626626bb1de84747b24cacc56"},
+]
+
+[package.dependencies]
+cachetools = ">=5.3"
+chardet = ">=5.1"
+colorama = ">=0.4.6"
+filelock = ">=3.11"
+importlib-metadata = {version = ">=6.4.1", markers = "python_version < \"3.8\""}
+packaging = ">=23.1"
+platformdirs = ">=3.2"
+pluggy = ">=1"
+pyproject-api = ">=1.5.1"
+tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
+typing-extensions = {version = ">=4.5", markers = "python_version < \"3.8\""}
+virtualenv = ">=20.21"
+
+[package.extras]
+docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-argparse-cli (>=1.11)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2022.1.2b11)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
+testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "devpi-process (>=0.3)", "diff-cover (>=7.5)", "distlib (>=0.3.6)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.14)", "psutil (>=5.9.4)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-xdist (>=3.2.1)", "re-assert (>=1.1)", "time-machine (>=2.9)", "wheel (>=0.40)"]
+
+[[package]]
+name = "typing-extensions"
+version = "4.5.0"
+description = "Backported and Experimental Type Hints for Python 3.7+"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
+ {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
+]
+
+[[package]]
+name = "virtualenv"
+version = "20.22.0"
+description = "Virtual Python Environment builder"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "virtualenv-20.22.0-py3-none-any.whl", hash = "sha256:48fd3b907b5149c5aab7c23d9790bea4cac6bc6b150af8635febc4cfeab1275a"},
+ {file = "virtualenv-20.22.0.tar.gz", hash = "sha256:278753c47aaef1a0f14e6db8a4c5e1e040e90aea654d0fc1dc7e0d8a42616cc3"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.6,<1"
+filelock = ">=3.11,<4"
+importlib-metadata = {version = ">=6.4.1", markers = "python_version < \"3.8\""}
+platformdirs = ">=3.2,<4"
+
+[package.extras]
+docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"]
+
+[[package]]
+name = "zipp"
+version = "3.15.0"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
+ {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
+]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = ">=3.7,<4.0"
+content-hash = "c36a1673d6d698c7ad45f256b4a254427861286c1a400d49f20b937c857f65f8"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..13e97655
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,43 @@
+[tool.black]
+target-version = ['py310']
+include = '\.pyi?$'
+skip-string-normalization = true
+
+[tool.isort]
+profile = "black"
+multi_line_output = 3
+include_trailing_comma = true
+force_grid_wrap = 0
+use_parentheses = true
+ensure_newline_before_comments = true
+line_length = 88
+known_first_party = ["business_rules"]
+
+[tool.poetry]
+name = "business-rules"
+version = "2.0.0"
+description = "Python DSL for setting up business intelligence rules that can be configured without code [https://github.com/venmo/business-rules]"
+authors = ["venmo "]
+readme = "README.md"
+packages = [{include = "business_rules"}]
+license='MIT'
+
+[tool.poetry.extras]
+test = ["pytest", "pytest-cov"]
+
+[tool.poetry.dependencies]
+python = ">=3.7,<4.0"
+pytz = "^2023.3"
+
+[tool.poetry.group.dev.dependencies]
+mock = "*"
+nose = "*"
+nose-run-line-number = "*"
+pytest = "*"
+pytest-cov = "*"
+coverage = "^7.2.3"
+tox = "^4.5.1"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 00000000..1548117a
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,5 @@
+[pytest]
+testpaths = tests
+addopts =
+ --cov=business_rules
+ --cov-fail-under=95
diff --git a/requirements-dev.txt b/requirements-dev.txt
deleted file mode 100644
index b20dfcff..00000000
--- a/requirements-dev.txt
+++ /dev/null
@@ -1,8 +0,0 @@
--r requirements.txt
-ipdb==0.10.1
-ipython==5.1
-mock==1.0.1
-nose==1.3.1
-nose-run-line-number==0.0.1
-pytest>=3.0.5
-pytest-cov==2.4.0
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 37161970..00000000
--- a/requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-pytz>=2016.10
-typing>=3.6.1
-six
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 029f3859..00000000
--- a/setup.py
+++ /dev/null
@@ -1,30 +0,0 @@
-#! /usr/bin/env python
-
-from __future__ import absolute_import
-import setuptools
-from setuptools import find_packages
-
-from business_rules import __version__ as version
-
-with open('HISTORY.rst') as f:
- history = f.read()
-
-description = 'Python DSL for setting up business intelligence rules that can be configured without code'
-
-install_requires = [
- 'pytz>=2016.10',
- 'typing',
- 'six',
-]
-
-setuptools.setup(
- name='business-rules',
- version=version,
- description='{0}\n\n{1}'.format(description, history),
- author='Venmo',
- author_email='open-source@venmo.com',
- url='https://github.com/venmo/business-rules',
- packages=find_packages(exclude=['tests']),
- license='MIT',
- install_requires=install_requires
-)
diff --git a/tests/__init__.py b/tests/__init__.py
index 20d2d6ba..2fce3bcf 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,7 +1,6 @@
-from __future__ import absolute_import
-try:
- from unittest2 import TestCase
-except ImportError:
- from unittest import TestCase
+from unittest import TestCase
-assert TestCase
+# Allow us to use Python 3's `assertRaisesRegex` to avoid
+# "DeprecationWarning: Please use assertRaisesRegex instead."
+if not hasattr(TestCase, "assertRaisesRegex"):
+ TestCase.assertRaisesRegex = TestCase.assertRaisesRegexp
diff --git a/tests/actions.py b/tests/actions.py
index 1848858d..cb8663c3 100644
--- a/tests/actions.py
+++ b/tests/actions.py
@@ -1,9 +1,9 @@
from __future__ import absolute_import
+
from business_rules import actions, fields
class TestActions(actions.BaseActions):
-
@actions.rule_action(params={"param": fields.FIELD_TEXT})
def example_action(self, param, **kargs):
pass
diff --git a/tests/example/actions.py b/tests/example/actions.py
index 0aeb2566..f3c70b3f 100755
--- a/tests/example/actions.py
+++ b/tests/example/actions.py
@@ -1,8 +1,8 @@
-from __future__ import absolute_import
+#! python3
import logging
-from business_rules.actions import *
-from business_rules.fields import *
+from business_rules.actions import BaseActions, rule_action
+from business_rules.fields import FIELD_TEXT
logger = logging.getLogger(__name__)
diff --git a/tests/example/main.py b/tests/example/main.py
index 58f58053..c6b3b3b0 100755
--- a/tests/example/main.py
+++ b/tests/example/main.py
@@ -1,9 +1,9 @@
-from __future__ import absolute_import
+#! python3
import logging
from business_rules import run_all
from tests.example.actions import ExampleActions
-from tests.example.basket import Item, Basket
+from tests.example.basket import Basket, Item
from tests.example.variables import ExampleVariables
logging.basicConfig(level=logging.DEBUG)
@@ -36,7 +36,7 @@
"name": "today",
"operator": "after_than_or_equal_to",
"value": "2017-01-16",
- }
+ },
]
},
"actions": [
@@ -44,9 +44,9 @@
"name": "log",
"params": {
"message": "All criteria met!",
- }
+ },
}
- ]
+ ],
},
{
"actions": [
@@ -54,10 +54,10 @@
"name": "log",
"params": {
"message": "Rule with no conditions triggered!",
- }
+ },
}
]
- }
+ },
]
hot_drink = Item(code=1, name='Hot Drink', line_number=1, quantity=1)
diff --git a/tests/example/variables.py b/tests/example/variables.py
index 8be6f4c1..551cc8aa 100755
--- a/tests/example/variables.py
+++ b/tests/example/variables.py
@@ -1,7 +1,14 @@
-from __future__ import absolute_import
+#! python3
import datetime
-from business_rules.variables import *
+from business_rules.variables import (
+ BaseVariables,
+ boolean_rule_variable,
+ datetime_rule_variable,
+ numeric_rule_variable,
+ select_rule_variable,
+ string_rule_variable,
+)
class ExampleVariables(BaseVariables):
@@ -22,7 +29,7 @@ def item_count(self):
@boolean_rule_variable()
def rule_variable(self, **kwargs):
- rule = kwargs.get('rule')
+ kwargs.get('rule')
return True
@datetime_rule_variable()
diff --git a/tests/operators/test_operators.py b/tests/operators/test_operators.py
index 0a9d914a..2deef059 100644
--- a/tests/operators/test_operators.py
+++ b/tests/operators/test_operators.py
@@ -1,18 +1,16 @@
-from __future__ import absolute_import
-import sys
-from datetime import datetime, timedelta, date, time
+from datetime import date, datetime, time, timedelta
from decimal import Decimal
import pytz
from business_rules.operators import (
- StringType,
- NumericType,
- BooleanType,
- SelectType,
- SelectMultipleType,
BaseType,
+ BooleanType,
DateTimeType,
+ NumericType,
+ SelectMultipleType,
+ SelectType,
+ StringType,
TimeType,
)
from tests import TestCase
@@ -21,7 +19,7 @@
class BaseTypeOperatorTests(TestCase):
def test_base_type_cannot_be_created(self):
with self.assertRaises(NotImplementedError):
- BaseType('test')
+ BaseType("test")
class StringOperatorTests(TestCase):
@@ -71,17 +69,14 @@ def test_non_empty(self):
class NumericOperatorTests(TestCase):
def test_instantiate(self):
err_string = "foo is not a valid numeric type"
- with self.assertRaisesRegexp(AssertionError, err_string):
+ with self.assertRaisesRegex(AssertionError, err_string):
NumericType("foo")
def test_numeric_type_validates_and_casts_decimal(self):
ten_dec = Decimal(10)
ten_int = 10
ten_float = 10.0
- if sys.version_info[0] == 2:
- ten_long = int(10)
- else:
- ten_long = int(10) # long and int are same in python3
+ ten_long = int(10) # long and int are same in python3
ten_var_dec = NumericType(ten_dec) # this should not throw an exception
ten_var_int = NumericType(ten_int)
ten_var_float = NumericType(ten_float)
@@ -96,14 +91,14 @@ def test_numeric_equal_to(self):
self.assertTrue(NumericType(10).equal_to(10.0))
self.assertTrue(NumericType(10).equal_to(10.000001))
self.assertTrue(NumericType(10.000001).equal_to(10))
- self.assertTrue(NumericType(Decimal('10.0')).equal_to(10))
- self.assertTrue(NumericType(10).equal_to(Decimal('10.0')))
+ self.assertTrue(NumericType(Decimal("10.0")).equal_to(10))
+ self.assertTrue(NumericType(10).equal_to(Decimal("10.0")))
self.assertFalse(NumericType(10).equal_to(10.00001))
self.assertFalse(NumericType(10).equal_to(11))
def test_other_value_not_numeric(self):
error_string = "10 is not a valid numeric type"
- with self.assertRaisesRegexp(AssertionError, error_string):
+ with self.assertRaisesRegex(AssertionError, error_string):
NumericType(10).equal_to("10")
def test_numeric_greater_than(self):
@@ -140,10 +135,10 @@ def test_numeric_less_than_or_equal_to(self):
class BooleanOperatorTests(TestCase):
def test_instantiate(self):
err_string = "foo is not a valid boolean type"
- with self.assertRaisesRegexp(AssertionError, err_string):
+ with self.assertRaisesRegex(AssertionError, err_string):
BooleanType("foo")
err_string = "None is not a valid boolean type"
- with self.assertRaisesRegexp(AssertionError, err_string):
+ with self.assertRaisesRegex(AssertionError, err_string):
BooleanType(None)
def test_boolean_is_true_and_is_false(self):
@@ -175,46 +170,46 @@ def test_invalid_value(self):
SelectMultipleType(123)
def test_contains_all(self):
- self.assertTrue(SelectMultipleType([1, 2]).
- contains_all([2, 1]))
- self.assertFalse(SelectMultipleType([1, 2]).
- contains_all([2, 3]))
- self.assertTrue(SelectMultipleType([1, 2, "a"]).
- contains_all([2, 1, "A"]))
+ self.assertTrue(SelectMultipleType([1, 2]).contains_all([2, 1]))
+ self.assertFalse(SelectMultipleType([1, 2]).contains_all([2, 3]))
+ self.assertTrue(SelectMultipleType([1, 2, "a"]).contains_all([2, 1, "A"]))
def test_is_contained_by(self):
- self.assertTrue(SelectMultipleType([1, 2]).
- is_contained_by([2, 1, 3]))
- self.assertFalse(SelectMultipleType([1, 2]).
- is_contained_by([2, 3, 4]))
- self.assertTrue(SelectMultipleType([1, 2, "a"]).
- is_contained_by([2, 1, "A"]))
+ self.assertTrue(SelectMultipleType([1, 2]).is_contained_by([2, 1, 3]))
+ self.assertFalse(SelectMultipleType([1, 2]).is_contained_by([2, 3, 4]))
+ self.assertTrue(SelectMultipleType([1, 2, "a"]).is_contained_by([2, 1, "A"]))
def test_shares_at_least_one_element_with(self):
- self.assertTrue(SelectMultipleType([1, 2]).
- shares_at_least_one_element_with([2, 3]))
- self.assertFalse(SelectMultipleType([1, 2]).
- shares_at_least_one_element_with([4, 3]))
- self.assertTrue(SelectMultipleType([1, 2, "a"]).
- shares_at_least_one_element_with([4, "A"]))
+ self.assertTrue(
+ SelectMultipleType([1, 2]).shares_at_least_one_element_with([2, 3])
+ )
+ self.assertFalse(
+ SelectMultipleType([1, 2]).shares_at_least_one_element_with([4, 3])
+ )
+ self.assertTrue(
+ SelectMultipleType([1, 2, "a"]).shares_at_least_one_element_with([4, "A"])
+ )
def test_shares_exactly_one_element_with(self):
- self.assertTrue(SelectMultipleType([1, 2]).
- shares_exactly_one_element_with([2, 3]))
- self.assertFalse(SelectMultipleType([1, 2]).
- shares_exactly_one_element_with([4, 3]))
- self.assertTrue(SelectMultipleType([1, 2, "a"]).
- shares_exactly_one_element_with([4, "A"]))
- self.assertFalse(SelectMultipleType([1, 2, 3]).
- shares_exactly_one_element_with([2, 3, "a"]))
+ self.assertTrue(
+ SelectMultipleType([1, 2]).shares_exactly_one_element_with([2, 3])
+ )
+ self.assertFalse(
+ SelectMultipleType([1, 2]).shares_exactly_one_element_with([4, 3])
+ )
+ self.assertTrue(
+ SelectMultipleType([1, 2, "a"]).shares_exactly_one_element_with([4, "A"])
+ )
+ self.assertFalse(
+ SelectMultipleType([1, 2, 3]).shares_exactly_one_element_with([2, 3, "a"])
+ )
def test_shares_no_elements_with(self):
- self.assertTrue(SelectMultipleType([1, 2]).
- shares_no_elements_with([4, 3]))
- self.assertFalse(SelectMultipleType([1, 2]).
- shares_no_elements_with([2, 3]))
- self.assertFalse(SelectMultipleType([1, 2, "a"]).
- shares_no_elements_with([4, "A"]))
+ self.assertTrue(SelectMultipleType([1, 2]).shares_no_elements_with([4, 3]))
+ self.assertFalse(SelectMultipleType([1, 2]).shares_no_elements_with([2, 3]))
+ self.assertFalse(
+ SelectMultipleType([1, 2, "a"]).shares_no_elements_with([4, "A"])
+ )
class DateTimeOperatorTests(TestCase):
@@ -226,19 +221,36 @@ def setUp(self):
self.TEST_HOUR = 13
self.TEST_MINUTE = 55
self.TEST_SECOND = 25
- self.TEST_DATETIME = '{year}-0{month}-{day}T{hour}:{minute}:{second}'.format(
- year=self.TEST_YEAR, month=self.TEST_MONTH, day=self.TEST_DAY, hour=self.TEST_HOUR, minute=self.TEST_MINUTE,
- second=self.TEST_SECOND
+ self.TEST_DATETIME = "{year}-0{month}-{day}T{hour}:{minute}:{second}".format(
+ year=self.TEST_YEAR,
+ month=self.TEST_MONTH,
+ day=self.TEST_DAY,
+ hour=self.TEST_HOUR,
+ minute=self.TEST_MINUTE,
+ second=self.TEST_SECOND,
)
- self.TEST_DATE = '{year}-0{month}-{day}'.format(
+ self.TEST_DATE = "{year}-0{month}-{day}".format(
year=self.TEST_YEAR, month=self.TEST_MONTH, day=self.TEST_DAY
)
- self.TEST_DATETIME_OBJ = datetime(self.TEST_YEAR, self.TEST_MONTH, self.TEST_DAY, self.TEST_HOUR,
- self.TEST_MINUTE, self.TEST_SECOND)
+ self.TEST_DATETIME_OBJ = datetime(
+ self.TEST_YEAR,
+ self.TEST_MONTH,
+ self.TEST_DAY,
+ self.TEST_HOUR,
+ self.TEST_MINUTE,
+ self.TEST_SECOND,
+ )
self.TEST_DATE_OBJ = date(self.TEST_YEAR, self.TEST_MONTH, self.TEST_DAY)
- self.TEST_DATETIME_UTC_OBJ = datetime(self.TEST_YEAR, self.TEST_MONTH, self.TEST_DAY, self.TEST_HOUR,
- self.TEST_MINUTE, self.TEST_SECOND, tzinfo=pytz.UTC)
+ self.TEST_DATETIME_UTC_OBJ = datetime(
+ self.TEST_YEAR,
+ self.TEST_MONTH,
+ self.TEST_DAY,
+ self.TEST_HOUR,
+ self.TEST_MINUTE,
+ self.TEST_SECOND,
+ tzinfo=pytz.UTC,
+ )
self.datetime_type_date = DateTimeType(self.TEST_DATE)
self.datetime_type_datetime = DateTimeType(self.TEST_DATETIME)
@@ -247,7 +259,7 @@ def setUp(self):
def test_instantiate(self):
err_string = "foo is not a valid datetime type"
- with self.assertRaisesRegexp(AssertionError, err_string):
+ with self.assertRaisesRegex(AssertionError, err_string):
DateTimeType("foo")
def test_datetime_type_validates_and_cast_datetime(self):
@@ -266,22 +278,34 @@ def test_datetime_type_validates_and_cast_datetime(self):
def test_datetime_equal_to(self):
self.assertTrue(self.datetime_type_datetime.equal_to(self.TEST_DATETIME))
self.assertTrue(self.datetime_type_datetime.equal_to(self.TEST_DATETIME_OBJ))
- self.assertTrue(self.datetime_type_datetime.equal_to(self.TEST_DATETIME_UTC_OBJ))
+ self.assertTrue(
+ self.datetime_type_datetime.equal_to(self.TEST_DATETIME_UTC_OBJ)
+ )
self.assertTrue(self.datetime_type_datetime_obj.equal_to(self.TEST_DATETIME))
- self.assertTrue(self.datetime_type_datetime_obj.equal_to(self.TEST_DATETIME_OBJ))
- self.assertTrue(self.datetime_type_datetime_obj.equal_to(self.TEST_DATETIME_UTC_OBJ))
+ self.assertTrue(
+ self.datetime_type_datetime_obj.equal_to(self.TEST_DATETIME_OBJ)
+ )
+ self.assertTrue(
+ self.datetime_type_datetime_obj.equal_to(self.TEST_DATETIME_UTC_OBJ)
+ )
- self.assertTrue(self.datetime_type_datetime_utc_obj.equal_to(self.TEST_DATETIME))
- self.assertTrue(self.datetime_type_datetime_utc_obj.equal_to(self.TEST_DATETIME_OBJ))
- self.assertTrue(self.datetime_type_datetime_utc_obj.equal_to(self.TEST_DATETIME_UTC_OBJ))
+ self.assertTrue(
+ self.datetime_type_datetime_utc_obj.equal_to(self.TEST_DATETIME)
+ )
+ self.assertTrue(
+ self.datetime_type_datetime_utc_obj.equal_to(self.TEST_DATETIME_OBJ)
+ )
+ self.assertTrue(
+ self.datetime_type_datetime_utc_obj.equal_to(self.TEST_DATETIME_UTC_OBJ)
+ )
self.assertTrue(self.datetime_type_date.equal_to(self.TEST_DATE))
self.assertTrue(self.datetime_type_date.equal_to(self.TEST_DATE_OBJ))
def test_other_value_not_datetime(self):
error_string = "2016-10 is not a valid datetime type"
- with self.assertRaisesRegexp(AssertionError, error_string):
+ with self.assertRaisesRegex(AssertionError, error_string):
DateTimeType(self.TEST_DATE).equal_to("2016-10")
def datetime_after_than_asserts(self, datetime_type):
@@ -289,10 +313,18 @@ def datetime_after_than_asserts(self, datetime_type):
self.assertFalse(datetime_type.after_than(self.TEST_DATETIME))
self.assertFalse(datetime_type.after_than(self.TEST_DATETIME_OBJ))
self.assertFalse(datetime_type.after_than(self.TEST_DATETIME_UTC_OBJ))
- self.assertTrue(datetime_type.after_than(self.TEST_DATETIME_OBJ - timedelta(seconds=1)))
- self.assertTrue(datetime_type.after_than(self.TEST_DATETIME_UTC_OBJ - timedelta(seconds=1)))
- self.assertFalse(datetime_type.after_than(self.TEST_DATETIME_OBJ + timedelta(seconds=1)))
- self.assertFalse(datetime_type.after_than(self.TEST_DATETIME_UTC_OBJ + timedelta(seconds=1)))
+ self.assertTrue(
+ datetime_type.after_than(self.TEST_DATETIME_OBJ - timedelta(seconds=1))
+ )
+ self.assertTrue(
+ datetime_type.after_than(self.TEST_DATETIME_UTC_OBJ - timedelta(seconds=1))
+ )
+ self.assertFalse(
+ datetime_type.after_than(self.TEST_DATETIME_OBJ + timedelta(seconds=1))
+ )
+ self.assertFalse(
+ datetime_type.after_than(self.TEST_DATETIME_UTC_OBJ + timedelta(seconds=1))
+ )
def test_datetime_after_than(self):
self.datetime_after_than_asserts(self.datetime_type_datetime)
@@ -300,35 +332,71 @@ def test_datetime_after_than(self):
self.datetime_after_than_asserts(self.datetime_type_datetime_utc_obj)
self.assertFalse(self.datetime_type_date.after_than(self.TEST_DATE))
- self.assertFalse(self.datetime_type_date.after_than(self.TEST_DATETIME_OBJ + timedelta(seconds=1)))
- self.assertFalse(self.datetime_type_date.after_than(self.TEST_DATETIME_UTC_OBJ + timedelta(seconds=1)))
+ self.assertFalse(
+ self.datetime_type_date.after_than(
+ self.TEST_DATETIME_OBJ + timedelta(seconds=1)
+ )
+ )
+ self.assertFalse(
+ self.datetime_type_date.after_than(
+ self.TEST_DATETIME_UTC_OBJ + timedelta(seconds=1)
+ )
+ )
def datetime_after_than_or_equal_to_asserts(self, datetime_type):
# type: (DateTimeType) -> None
self.assertTrue(datetime_type.after_than_or_equal_to(self.TEST_DATETIME))
self.assertTrue(datetime_type.after_than_or_equal_to(self.TEST_DATETIME_OBJ))
- self.assertTrue(datetime_type.after_than_or_equal_to(self.TEST_DATETIME_UTC_OBJ))
- self.assertTrue(datetime_type.after_than_or_equal_to(self.TEST_DATETIME_OBJ - timedelta(seconds=1)))
- self.assertFalse(datetime_type.after_than_or_equal_to(self.TEST_DATETIME_OBJ + timedelta(seconds=1)))
- self.assertTrue(datetime_type.after_than_or_equal_to(self.TEST_DATETIME_UTC_OBJ - timedelta(seconds=1)))
- self.assertFalse(datetime_type.after_than_or_equal_to(self.TEST_DATETIME_UTC_OBJ + timedelta(seconds=1)))
+ self.assertTrue(
+ datetime_type.after_than_or_equal_to(self.TEST_DATETIME_UTC_OBJ)
+ )
+ self.assertTrue(
+ datetime_type.after_than_or_equal_to(
+ self.TEST_DATETIME_OBJ - timedelta(seconds=1)
+ )
+ )
+ self.assertFalse(
+ datetime_type.after_than_or_equal_to(
+ self.TEST_DATETIME_OBJ + timedelta(seconds=1)
+ )
+ )
+ self.assertTrue(
+ datetime_type.after_than_or_equal_to(
+ self.TEST_DATETIME_UTC_OBJ - timedelta(seconds=1)
+ )
+ )
+ self.assertFalse(
+ datetime_type.after_than_or_equal_to(
+ self.TEST_DATETIME_UTC_OBJ + timedelta(seconds=1)
+ )
+ )
def test_datetime_after_than_or_equal_to(self):
self.assertTrue(self.datetime_type_date.after_than_or_equal_to(self.TEST_DATE))
self.datetime_after_than_or_equal_to_asserts(self.datetime_type_datetime)
self.datetime_after_than_or_equal_to_asserts(self.datetime_type_datetime_obj)
- self.datetime_after_than_or_equal_to_asserts(self.datetime_type_datetime_utc_obj)
+ self.datetime_after_than_or_equal_to_asserts(
+ self.datetime_type_datetime_utc_obj
+ )
def datetime_before_than_asserts(self, datetime_type):
# type: (DateTimeType) -> None
self.assertFalse(datetime_type.before_than(self.TEST_DATETIME))
self.assertFalse(datetime_type.before_than(self.TEST_DATETIME_OBJ))
self.assertFalse(datetime_type.before_than(self.TEST_DATETIME_UTC_OBJ))
- self.assertFalse(datetime_type.before_than(self.TEST_DATETIME_OBJ - timedelta(seconds=1)))
- self.assertFalse(datetime_type.before_than(self.TEST_DATETIME_UTC_OBJ - timedelta(seconds=1)))
- self.assertTrue(datetime_type.before_than(self.TEST_DATETIME_OBJ + timedelta(seconds=1)))
- self.assertTrue(datetime_type.before_than(self.TEST_DATETIME_UTC_OBJ + timedelta(seconds=1)))
+ self.assertFalse(
+ datetime_type.before_than(self.TEST_DATETIME_OBJ - timedelta(seconds=1))
+ )
+ self.assertFalse(
+ datetime_type.before_than(self.TEST_DATETIME_UTC_OBJ - timedelta(seconds=1))
+ )
+ self.assertTrue(
+ datetime_type.before_than(self.TEST_DATETIME_OBJ + timedelta(seconds=1))
+ )
+ self.assertTrue(
+ datetime_type.before_than(self.TEST_DATETIME_UTC_OBJ + timedelta(seconds=1))
+ )
def test_datetime_before_than(self):
self.datetime_before_than_asserts(self.datetime_type_datetime)
@@ -336,28 +404,62 @@ def test_datetime_before_than(self):
self.datetime_before_than_asserts(self.datetime_type_datetime_utc_obj)
self.assertFalse(self.datetime_type_date.before_than(self.TEST_DATE))
- self.assertTrue(self.datetime_type_date.before_than(self.TEST_DATETIME_OBJ + timedelta(seconds=1)))
- self.assertTrue(self.datetime_type_date.before_than(self.TEST_DATETIME_UTC_OBJ + timedelta(seconds=1)))
+ self.assertTrue(
+ self.datetime_type_date.before_than(
+ self.TEST_DATETIME_OBJ + timedelta(seconds=1)
+ )
+ )
+ self.assertTrue(
+ self.datetime_type_date.before_than(
+ self.TEST_DATETIME_UTC_OBJ + timedelta(seconds=1)
+ )
+ )
def datetime_before_than_or_equal_to_asserts(self, datetime_type):
# type: (DateTimeType) -> None
self.assertTrue(datetime_type.before_than_or_equal_to(self.TEST_DATETIME))
self.assertTrue(datetime_type.before_than_or_equal_to(self.TEST_DATETIME_OBJ))
- self.assertTrue(datetime_type.before_than_or_equal_to(self.TEST_DATETIME_UTC_OBJ))
- self.assertFalse(datetime_type.before_than_or_equal_to(self.TEST_DATETIME_OBJ - timedelta(seconds=1)))
- self.assertFalse(datetime_type.before_than_or_equal_to(self.TEST_DATETIME_UTC_OBJ - timedelta(seconds=1)))
- self.assertTrue(datetime_type.before_than_or_equal_to(self.TEST_DATETIME_OBJ + timedelta(seconds=1)))
- self.assertTrue(datetime_type.before_than_or_equal_to(self.TEST_DATETIME_UTC_OBJ + timedelta(seconds=1)))
+ self.assertTrue(
+ datetime_type.before_than_or_equal_to(self.TEST_DATETIME_UTC_OBJ)
+ )
+ self.assertFalse(
+ datetime_type.before_than_or_equal_to(
+ self.TEST_DATETIME_OBJ - timedelta(seconds=1)
+ )
+ )
+ self.assertFalse(
+ datetime_type.before_than_or_equal_to(
+ self.TEST_DATETIME_UTC_OBJ - timedelta(seconds=1)
+ )
+ )
+ self.assertTrue(
+ datetime_type.before_than_or_equal_to(
+ self.TEST_DATETIME_OBJ + timedelta(seconds=1)
+ )
+ )
+ self.assertTrue(
+ datetime_type.before_than_or_equal_to(
+ self.TEST_DATETIME_UTC_OBJ + timedelta(seconds=1)
+ )
+ )
def test_datetime_before_than_or_equal_to(self):
self.datetime_before_than_or_equal_to_asserts(self.datetime_type_datetime)
self.datetime_before_than_or_equal_to_asserts(self.datetime_type_datetime_obj)
- self.datetime_before_than_or_equal_to_asserts(self.datetime_type_datetime_utc_obj)
+ self.datetime_before_than_or_equal_to_asserts(
+ self.datetime_type_datetime_utc_obj
+ )
self.assertTrue(self.datetime_type_date.before_than_or_equal_to(self.TEST_DATE))
- self.assertTrue(self.datetime_type_date.before_than_or_equal_to(self.TEST_DATETIME_OBJ + timedelta(seconds=1)))
self.assertTrue(
- self.datetime_type_date.before_than_or_equal_to(self.TEST_DATETIME_UTC_OBJ + timedelta(seconds=1))
+ self.datetime_type_date.before_than_or_equal_to(
+ self.TEST_DATETIME_OBJ + timedelta(seconds=1)
+ )
+ )
+ self.assertTrue(
+ self.datetime_type_date.before_than_or_equal_to(
+ self.TEST_DATETIME_UTC_OBJ + timedelta(seconds=1)
+ )
)
@@ -367,10 +469,12 @@ def setUp(self):
self.TEST_HOUR = 13
self.TEST_MINUTE = 55
self.TEST_SECOND = 00
- self.TEST_TIME = '{hour}:{minute}:{second}'.format(
+ self.TEST_TIME = "{hour}:{minute}:{second}".format(
hour=self.TEST_HOUR, minute=self.TEST_MINUTE, second=self.TEST_SECOND
)
- self.TEST_TIME_NO_SECONDS = '{hour}:{minute}'.format(hour=self.TEST_HOUR, minute=self.TEST_MINUTE)
+ self.TEST_TIME_NO_SECONDS = "{hour}:{minute}".format(
+ hour=self.TEST_HOUR, minute=self.TEST_MINUTE
+ )
self.TEST_TIME_OBJ = time(self.TEST_HOUR, self.TEST_MINUTE, self.TEST_SECOND)
self.time_type_time = TimeType(self.TEST_TIME)
@@ -379,7 +483,7 @@ def setUp(self):
def test_instantiate(self):
err_string = "foo is not a valid time type"
- with self.assertRaisesRegexp(AssertionError, err_string):
+ with self.assertRaisesRegex(AssertionError, err_string):
TimeType("foo")
def test_time_type_validates_and_cast_time(self):
@@ -403,7 +507,7 @@ def test_time_equal_to(self):
def test_other_value_not_time(self):
error_string = "2016-10 is not a valid time type"
- with self.assertRaisesRegexp(AssertionError, error_string):
+ with self.assertRaisesRegex(AssertionError, error_string):
TimeType(self.TEST_TIME_NO_SECONDS).equal_to("2016-10")
def time_after_than_asserts(self, time_type):
@@ -414,7 +518,11 @@ def time_after_than_asserts(self, time_type):
test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute - 1, 59)
self.assertTrue(time_type.after_than(test_time))
- test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute, self.TEST_TIME_OBJ.second + 1)
+ test_time = time(
+ self.TEST_TIME_OBJ.hour,
+ self.TEST_TIME_OBJ.minute,
+ self.TEST_TIME_OBJ.second + 1,
+ )
self.assertFalse(time_type.after_than(test_time))
def test_time_after_than(self):
@@ -423,7 +531,11 @@ def test_time_after_than(self):
self.assertFalse(self.time_type_time.after_than(self.TEST_TIME_NO_SECONDS))
- test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute, self.TEST_TIME_OBJ.second + 1)
+ test_time = time(
+ self.TEST_TIME_OBJ.hour,
+ self.TEST_TIME_OBJ.minute,
+ self.TEST_TIME_OBJ.second + 1,
+ )
self.assertFalse(self.time_type_time.after_than(test_time))
def time_after_than_or_equal_to_asserts(self, time_type):
@@ -434,11 +546,17 @@ def time_after_than_or_equal_to_asserts(self, time_type):
test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute - 1, 59)
self.assertTrue(time_type.after_than_or_equal_to(test_time))
- test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute, self.TEST_TIME_OBJ.second + 1)
+ test_time = time(
+ self.TEST_TIME_OBJ.hour,
+ self.TEST_TIME_OBJ.minute,
+ self.TEST_TIME_OBJ.second + 1,
+ )
self.assertFalse(time_type.after_than_or_equal_to(test_time))
def test_time_after_than_or_equal_to(self):
- self.assertTrue(self.time_type_time.after_than_or_equal_to(self.TEST_TIME_NO_SECONDS))
+ self.assertTrue(
+ self.time_type_time.after_than_or_equal_to(self.TEST_TIME_NO_SECONDS)
+ )
self.time_after_than_or_equal_to_asserts(self.time_type_time_no_seconds)
self.time_after_than_or_equal_to_asserts(self.time_type_time_obj)
@@ -451,7 +569,11 @@ def time_before_than_asserts(self, time_type):
test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute - 1, 59)
self.assertFalse(time_type.before_than(test_time))
- test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute, self.TEST_TIME_OBJ.second + 1)
+ test_time = time(
+ self.TEST_TIME_OBJ.hour,
+ self.TEST_TIME_OBJ.minute,
+ self.TEST_TIME_OBJ.second + 1,
+ )
self.assertTrue(time_type.before_than(test_time))
def test_time_before_than(self):
@@ -460,7 +582,11 @@ def test_time_before_than(self):
self.assertFalse(self.time_type_time.before_than(self.TEST_TIME_NO_SECONDS))
- test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute, self.TEST_TIME_OBJ.second + 1)
+ test_time = time(
+ self.TEST_TIME_OBJ.hour,
+ self.TEST_TIME_OBJ.minute,
+ self.TEST_TIME_OBJ.second + 1,
+ )
self.assertTrue(self.time_type_time.before_than(test_time))
def time_before_than_or_equal_to_asserts(self, time_type):
@@ -471,14 +597,24 @@ def time_before_than_or_equal_to_asserts(self, time_type):
test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute - 1, 59)
self.assertFalse(time_type.before_than_or_equal_to(test_time))
- test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute, self.TEST_TIME_OBJ.second + 1)
+ test_time = time(
+ self.TEST_TIME_OBJ.hour,
+ self.TEST_TIME_OBJ.minute,
+ self.TEST_TIME_OBJ.second + 1,
+ )
self.assertTrue(time_type.before_than_or_equal_to(test_time))
def test_time_before_than_or_equal_to(self):
self.time_before_than_or_equal_to_asserts(self.time_type_time_no_seconds)
self.time_before_than_or_equal_to_asserts(self.time_type_time_obj)
- self.assertTrue(self.time_type_time.before_than_or_equal_to(self.TEST_TIME_NO_SECONDS))
+ self.assertTrue(
+ self.time_type_time.before_than_or_equal_to(self.TEST_TIME_NO_SECONDS)
+ )
- test_time = time(self.TEST_TIME_OBJ.hour, self.TEST_TIME_OBJ.minute, self.TEST_TIME_OBJ.second + 1)
+ test_time = time(
+ self.TEST_TIME_OBJ.hour,
+ self.TEST_TIME_OBJ.minute,
+ self.TEST_TIME_OBJ.second + 1,
+ )
self.assertTrue(self.time_type_time.before_than_or_equal_to(test_time))
diff --git a/tests/operators/test_operators_class.py b/tests/operators/test_operators_class.py
index 28bfa37f..3fc13a76 100644
--- a/tests/operators/test_operators_class.py
+++ b/tests/operators/test_operators_class.py
@@ -1,66 +1,67 @@
-from __future__ import absolute_import
-from business_rules.operators import BaseType, type_operator
-from tests import TestCase
+from unittest import TestCase
+
from mock import MagicMock
+from business_rules.operators import BaseType, type_operator
+
+
class OperatorsClassTests(TestCase):
- """ Test methods on classes that inherit from BaseType.
- """
+ """Test methods on classes that inherit from BaseType."""
def test_base_has_no_operators(self):
self.assertEqual(len(BaseType.get_all_operators()), 0)
def test_get_all_operators(self):
- """ Returns a dictionary listing all the operators on the class
+ """Returns a dictionary listing all the operators on the class
that can be called on that type, with some data about them.
"""
- class SomeType(BaseType):
- @type_operator(input_type='text')
+ class SomeType(BaseType):
+ @type_operator(input_type="text")
def some_operator(self):
return True
def not_an_operator(self):
- return 'yo yo'
+ return "yo yo"
operators = SomeType.get_all_operators()
self.assertEqual(len(operators), 1)
some_operator = operators[0]
- self.assertEqual(some_operator['name'], 'some_operator')
- self.assertEqual(some_operator['label'], 'Some Operator')
- self.assertEqual(some_operator['input_type'], 'text')
-
+ self.assertEqual(some_operator["name"], "some_operator")
+ self.assertEqual(some_operator["label"], "Some Operator")
+ self.assertEqual(some_operator["input_type"], "text")
def test_operator_decorator_casts_argument(self):
- """ Any operator that has the @type_operator decorator
+ """Any operator that has the @type_operator decorator
should call _assert_valid_value_and_cast on the parameter.
"""
+
class SomeType(BaseType):
def __init__(self, value):
self.value = value
_assert_valid_value_and_cast = MagicMock()
- @type_operator('text')
+ @type_operator("text")
def some_operator(self, other_param):
pass
- @type_operator('text', assert_type_for_arguments=False)
+ @type_operator("text", assert_type_for_arguments=False)
def other_operator(self, other_param):
pass
# casts with positional args
- some_type = SomeType('val')
- some_type.some_operator('foo') # positional
- some_type._assert_valid_value_and_cast.assert_called_once_with('foo')
+ some_type = SomeType("val")
+ some_type.some_operator("foo") # positional
+ some_type._assert_valid_value_and_cast.assert_called_once_with("foo")
# casts with keyword args
some_type._assert_valid_value_and_cast.reset_mock()
- some_type.some_operator(other_param='foo2') # keyword
- some_type._assert_valid_value_and_cast.assert_called_once_with('foo2')
+ some_type.some_operator(other_param="foo2") # keyword
+ some_type._assert_valid_value_and_cast.assert_called_once_with("foo2")
# does not cast if that argument is set
some_type._assert_valid_value_and_cast.reset_mock()
- some_type.other_operator('blah')
- some_type.other_operator(other_param='blah')
+ some_type.other_operator("blah")
+ some_type.other_operator(other_param="blah")
self.assertEqual(some_type._assert_valid_value_and_cast.call_count, 0)
diff --git a/tests/operators/test_time_operator.py b/tests/operators/test_time_operator.py
index 67193cce..a3207529 100644
--- a/tests/operators/test_time_operator.py
+++ b/tests/operators/test_time_operator.py
@@ -1,8 +1,7 @@
-from __future__ import absolute_import
-from datetime import time, datetime, timedelta
+from datetime import datetime, time
+from unittest import TestCase
from business_rules.operators import TimeType
-from .. import TestCase
class TimeOperatorTests(TestCase):
@@ -11,16 +10,22 @@ def setUp(self):
self.TEST_HOUR = 13
self.TEST_MINUTE = 55
self.TEST_SECOND = 25
- self.TEST_TIME = '{hour}:{minute}:{second}'.format(
+ self.TEST_TIME = "{hour}:{minute}:{second}".format(
hour=self.TEST_HOUR, minute=self.TEST_MINUTE, second=self.TEST_SECOND
)
self.TEST_TIME_OBJ = time(self.TEST_HOUR, self.TEST_MINUTE, self.TEST_SECOND)
- self.TEST_DATETIME_OBJ = datetime(2017, 1, 1, hour=self.TEST_HOUR, minute=self.TEST_MINUTE,
- second=self.TEST_SECOND)
+ self.TEST_DATETIME_OBJ = datetime(
+ 2017,
+ 1,
+ 1,
+ hour=self.TEST_HOUR,
+ minute=self.TEST_MINUTE,
+ second=self.TEST_SECOND,
+ )
def test_instantiate(self):
err_string = "foo is not a valid time type"
- with self.assertRaisesRegexp(AssertionError, err_string):
+ with self.assertRaisesRegex(AssertionError, err_string):
TimeType("foo")
def test_time_type_validates_and_cast_time(self):
@@ -38,36 +43,67 @@ def test_time_equal_to(self):
def test_other_value_not_time(self):
error_string = "2016-10 is not a valid time type"
- with self.assertRaisesRegexp(AssertionError, error_string):
+ with self.assertRaisesRegex(AssertionError, error_string):
TimeType(self.TEST_TIME).equal_to("2016-10")
def test_time_after_than(self):
- self.assertTrue(TimeType(self.TEST_TIME).after_than(self._relative_time(self.TEST_TIME_OBJ, 0, 0, -1)))
+ self.assertTrue(
+ TimeType(self.TEST_TIME).after_than(
+ self._relative_time(self.TEST_TIME_OBJ, 0, 0, -1)
+ )
+ )
self.assertFalse(TimeType(self.TEST_TIME).after_than(self.TEST_TIME))
- self.assertFalse(TimeType(self.TEST_TIME).after_than(self._relative_time(self.TEST_TIME_OBJ, 0, 0, 1)))
+ self.assertFalse(
+ TimeType(self.TEST_TIME).after_than(
+ self._relative_time(self.TEST_TIME_OBJ, 0, 0, 1)
+ )
+ )
def test_time_after_than_or_equal_to(self):
self.assertTrue(TimeType(self.TEST_TIME).after_than_or_equal_to(self.TEST_TIME))
self.assertTrue(
- TimeType(self.TEST_TIME).after_than_or_equal_to(self._relative_time(self.TEST_TIME_OBJ, 0, 0, -1))
+ TimeType(self.TEST_TIME).after_than_or_equal_to(
+ self._relative_time(self.TEST_TIME_OBJ, 0, 0, -1)
+ )
)
self.assertFalse(
- TimeType(self.TEST_TIME).after_than_or_equal_to(self._relative_time(self.TEST_TIME_OBJ, 0, 0, 1))
+ TimeType(self.TEST_TIME).after_than_or_equal_to(
+ self._relative_time(self.TEST_TIME_OBJ, 0, 0, 1)
+ )
)
def test_time_before_than(self):
- self.assertFalse(TimeType(self.TEST_TIME).before_than(self._relative_time(self.TEST_TIME_OBJ, 0, 0, -1)))
+ self.assertFalse(
+ TimeType(self.TEST_TIME).before_than(
+ self._relative_time(self.TEST_TIME_OBJ, 0, 0, -1)
+ )
+ )
self.assertFalse(TimeType(self.TEST_TIME).before_than(self.TEST_TIME))
- self.assertTrue(TimeType(self.TEST_TIME).before_than(self._relative_time(self.TEST_TIME_OBJ, 0, 0, 1)))
+ self.assertTrue(
+ TimeType(self.TEST_TIME).before_than(
+ self._relative_time(self.TEST_TIME_OBJ, 0, 0, 1)
+ )
+ )
def test_time_before_than_or_equal_to(self):
- self.assertTrue(TimeType(self.TEST_TIME).before_than_or_equal_to(self.TEST_TIME))
+ self.assertTrue(
+ TimeType(self.TEST_TIME).before_than_or_equal_to(self.TEST_TIME)
+ )
self.assertFalse(
- TimeType(self.TEST_TIME_OBJ).before_than_or_equal_to(self._relative_time(self.TEST_TIME_OBJ, 0, 0, -1))
+ TimeType(self.TEST_TIME_OBJ).before_than_or_equal_to(
+ self._relative_time(self.TEST_TIME_OBJ, 0, 0, -1)
+ )
)
self.assertTrue(
- TimeType(self.TEST_TIME).before_than_or_equal_to(self._relative_time(self.TEST_TIME_OBJ, 0, 0, 1)))
+ TimeType(self.TEST_TIME).before_than_or_equal_to(
+ self._relative_time(self.TEST_TIME_OBJ, 0, 0, 1)
+ )
+ )
@staticmethod
def _relative_time(base_time, hours, minutes, seconds):
- return time(base_time.hour+hours, base_time.minute+minutes, base_time.second+seconds)
+ return time(
+ base_time.hour + hours,
+ base_time.minute + minutes,
+ base_time.second + seconds,
+ )
diff --git a/tests/test_actions_class.py b/tests/test_actions_class.py
index a7a52941..b0bbb7e5 100644
--- a/tests/test_actions_class.py
+++ b/tests/test_actions_class.py
@@ -1,23 +1,22 @@
-from __future__ import absolute_import
-from business_rules.actions import BaseActions, rule_action, ActionParam
-from business_rules.fields import FIELD_TEXT, FIELD_NUMERIC
-from . import TestCase
+from unittest import TestCase
+
+from business_rules.actions import BaseActions, rule_action
+from business_rules.fields import FIELD_TEXT
class ActionsClassTests(TestCase):
- """ Test methods on classes that inherit from BaseActions.
- """
+ """Test methods on classes that inherit from BaseActions."""
def test_base_has_no_actions(self):
self.assertEqual(len(BaseActions.get_all_actions()), 0)
def test_get_all_actions(self):
- """ Returns a dictionary listing all the functions on the class that
+ """Returns a dictionary listing all the functions on the class that
have been decorated as actions, with some of the data about them.
"""
class SomeActions(BaseActions):
- @rule_action(params={'foo': FIELD_TEXT})
+ @rule_action(params={"foo": FIELD_TEXT})
def some_action(self, foo):
return "blah"
@@ -26,34 +25,46 @@ def non_action(self):
actions = SomeActions.get_all_actions()
self.assertEqual(len(actions), 1)
- self.assertEqual(actions[0]['name'], 'some_action')
- self.assertEqual(actions[0]['label'], 'Some Action')
- self.assertEqual(actions[0]['params'], [
- {'fieldType': FIELD_TEXT, 'name': 'foo', 'label': 'Foo', 'defaultValue': None},
- ])
+ self.assertEqual(actions[0]["name"], "some_action")
+ self.assertEqual(actions[0]["label"], "Some Action")
+ self.assertEqual(
+ actions[0]["params"],
+ [
+ {
+ "fieldType": FIELD_TEXT,
+ "name": "foo",
+ "label": "Foo",
+ "defaultValue": None,
+ },
+ ],
+ )
# should work on an instance of the class too
self.assertEqual(len(SomeActions().get_all_actions()), 1)
def test_rule_action_doesnt_allow_unknown_field_types(self):
- err_string = "Unknown field type blah specified for action some_action" \
- " param foo"
- with self.assertRaisesRegexp(AssertionError, err_string):
- @rule_action(params={'foo': 'blah'})
+ err_string = (
+ "Unknown field type blah specified for action some_action" " param foo"
+ )
+ with self.assertRaisesRegex(AssertionError, err_string):
+
+ @rule_action(params={"foo": "blah"})
def some_action(self, foo):
pass
def test_rule_action_doesnt_allow_unknown_parameter_name(self):
err_string = "Unknown parameter name foo specified for action some_action"
- with self.assertRaisesRegexp(AssertionError, err_string):
- @rule_action(params={'foo': 'blah'})
+ with self.assertRaisesRegex(AssertionError, err_string):
+
+ @rule_action(params={"foo": "blah"})
def some_action(self):
pass
def test_rule_action_with_no_params_or_label(self):
- """ A rule action should not have to specify paramers or label. """
+ """A rule action should not have to specify paramers or label."""
@rule_action()
- def some_action(self): pass
+ def some_action(self):
+ pass
self.assertTrue(some_action.is_rule_action)
diff --git a/tests/test_engine_logic.py b/tests/test_engine_logic.py
index 627dc423..5996a163 100644
--- a/tests/test_engine_logic.py
+++ b/tests/test_engine_logic.py
@@ -1,34 +1,29 @@
-from __future__ import absolute_import
+from unittest import TestCase
-from mock import patch, MagicMock
-from business_rules import engine
-from business_rules import fields
-from business_rules.actions import BaseActions, ActionParam, rule_action
-from business_rules.fields import FIELD_TEXT, FIELD_NUMERIC
+from mock import MagicMock, patch
+
+from business_rules import engine, fields
+from business_rules.actions import ActionParam, BaseActions, rule_action
+from business_rules.fields import FIELD_NUMERIC, FIELD_TEXT
from business_rules.models import ConditionResult
from business_rules.operators import StringType
from business_rules.variables import BaseVariables
-from . import TestCase
class EngineTests(TestCase):
- # ######### #
- # ## Run ## #
- # ######### #
-
- @patch.object(engine, 'run')
+ @patch.object(engine, "run")
def test_run_all_some_rule_triggered(self, *args):
"""
By default, does not stop on first triggered rule. Returns a list of
booleans indicating whether each rule was triggered.
"""
- rule1 = {'conditions': 'condition1', 'actions': 'action name 1'}
- rule2 = {'conditions': 'condition2', 'actions': 'action name 2'}
+ rule1 = {"conditions": "condition1", "actions": "action name 1"}
+ rule2 = {"conditions": "condition2", "actions": "action name 2"}
variables = BaseVariables()
actions = BaseActions()
def return_action1(rule, *args, **kwargs):
- return rule['actions'] == 'action name 1'
+ return rule["actions"] == "action name 1"
engine.run.side_effect = return_action1
@@ -43,153 +38,154 @@ def return_action1(rule, *args, **kwargs):
self.assertEqual(results, [False, True])
self.assertEqual(engine.run.call_count, 2)
- @patch.object(engine, 'run', return_value=True)
+ @patch.object(engine, "run", return_value=True)
def test_run_all_stop_on_first(self, *args):
- rule1 = {'conditions': 'condition1', 'actions': 'action name 1'}
- rule2 = {'conditions': 'condition2', 'actions': 'action name 2'}
-
+ rule1 = {"conditions": "condition1", "actions": "action name 1"}
+ rule2 = {"conditions": "condition2", "actions": "action name 2"}
variables = BaseVariables()
actions = BaseActions()
- results = engine.run_all([rule1, rule2], variables, actions, stop_on_first_trigger=True)
+ results = engine.run_all(
+ [rule1, rule2], variables, actions, stop_on_first_trigger=True
+ )
self.assertEqual(results, [True, False])
self.assertEqual(engine.run.call_count, 1)
engine.run.assert_called_once_with(rule1, variables, actions)
- @patch.object(engine, 'check_conditions_recursively', return_value=(True, []))
- @patch.object(engine, 'do_actions')
+ @patch.object(engine, "check_conditions_recursively", return_value=(True, []))
+ @patch.object(engine, "do_actions")
def test_run_that_triggers_rule(self, *args):
- rule = {'conditions': 'blah', 'actions': 'blah2'}
-
+ rule = {"conditions": "blah", "actions": "blah2"}
variables = BaseVariables()
actions = BaseActions()
result = engine.run(rule, variables, actions)
-
self.assertEqual(result, True)
- engine.check_conditions_recursively.assert_called_once_with(rule['conditions'], variables, rule)
- engine.do_actions.assert_called_once_with(rule['actions'], actions, [], rule)
+ engine.check_conditions_recursively.assert_called_once_with(
+ rule["conditions"], variables, rule
+ )
+ engine.do_actions.assert_called_once_with(rule["actions"], actions, [], rule)
- @patch.object(engine, 'check_conditions_recursively', return_value=(False, []))
- @patch.object(engine, 'do_actions')
+ @patch.object(engine, "check_conditions_recursively", return_value=(False, []))
+ @patch.object(engine, "do_actions")
def test_run_that_doesnt_trigger_rule(self, *args):
- rule = {'conditions': 'blah', 'actions': 'blah2'}
-
+ rule = {"conditions": "blah", "actions": "blah2"}
variables = BaseVariables()
actions = BaseActions()
result = engine.run(rule, variables, actions)
-
self.assertEqual(result, False)
- engine.check_conditions_recursively.assert_called_once_with(rule['conditions'], variables, rule)
+ engine.check_conditions_recursively.assert_called_once_with(
+ rule["conditions"], variables, rule
+ )
self.assertEqual(engine.do_actions.call_count, 0)
- @patch.object(engine, 'check_condition', return_value=(True,))
+ @patch.object(engine, "check_condition", return_value=(True,))
def test_check_all_conditions_with_all_true(self, *args):
- conditions = {'all': [{'thing1': ''}, {'thing2': ''}]}
+ conditions = {"all": [{"thing1": ""}, {"thing2": ""}]}
variables = BaseVariables()
- rule = {'conditions': conditions, 'actions': []}
+ rule = {"conditions": conditions, "actions": []}
result = engine.check_conditions_recursively(conditions, variables, rule)
self.assertEqual(result, (True, [(True,), (True,)]))
# assert call count and most recent call are as expected
self.assertEqual(engine.check_condition.call_count, 2)
- engine.check_condition.assert_called_with({'thing2': ''}, variables, rule)
+ engine.check_condition.assert_called_with({"thing2": ""}, variables, rule)
# ########################################################## #
# #################### Check conditions #################### #
# ########################################################## #
- @patch.object(engine, 'check_condition', return_value=(False,))
+ @patch.object(engine, "check_condition", return_value=(False,))
def test_check_all_conditions_with_all_false(self, *args):
- conditions = {'all': [{'thing1': ''}, {'thing2': ''}]}
+ conditions = {"all": [{"thing1": ""}, {"thing2": ""}]}
variables = BaseVariables()
rule = {"conditions": conditions, "actions": []}
result = engine.check_conditions_recursively(conditions, variables, rule)
self.assertEqual(result, (False, []))
- engine.check_condition.assert_called_once_with({'thing1': ''}, variables, rule)
+ engine.check_condition.assert_called_once_with({"thing1": ""}, variables, rule)
def test_check_all_condition_with_no_items_fails(self):
- conditions = {'all': []}
+ conditions = {"all": []}
rule = {"conditions": conditions, "actions": []}
variables = BaseVariables()
with self.assertRaises(AssertionError):
engine.check_conditions_recursively(conditions, variables, rule)
- @patch.object(engine, 'check_condition', return_value=(True,))
+ @patch.object(engine, "check_condition", return_value=(True,))
def test_check_any_conditions_with_all_true(self, *args):
- conditions = {'any': [{'thing1': ''}, {'thing2': ''}]}
+ conditions = {"any": [{"thing1": ""}, {"thing2": ""}]}
variables = BaseVariables()
- rule = {'conditions': conditions, 'actions': []}
+ rule = {"conditions": conditions, "actions": []}
result = engine.check_conditions_recursively(conditions, variables, rule)
self.assertEqual(result, (True, [(True,)]))
- engine.check_condition.assert_called_once_with({'thing1': ''}, variables, rule)
+ engine.check_condition.assert_called_once_with({"thing1": ""}, variables, rule)
- @patch.object(engine, 'check_condition', return_value=(False,))
+ @patch.object(engine, "check_condition", return_value=(False,))
def test_check_any_conditions_with_all_false(self, *args):
- conditions = {'any': [{'thing1': ''}, {'thing2': ''}]}
+ conditions = {"any": [{"thing1": ""}, {"thing2": ""}]}
variables = BaseVariables()
- rule = {'conditions': conditions, 'actions': []}
+ rule = {"conditions": conditions, "actions": []}
result = engine.check_conditions_recursively(conditions, variables, rule)
self.assertEqual(result, (False, []))
# assert call count and most recent call are as expected
self.assertEqual(engine.check_condition.call_count, 2)
- engine.check_condition.assert_called_with(conditions['any'][1], variables, rule)
+ engine.check_condition.assert_called_with(conditions["any"][1], variables, rule)
def test_check_any_condition_with_no_items_fails(self):
- conditions = {'any': []}
+ conditions = {"any": []}
variables = BaseVariables()
- rule = {'conditions': conditions, 'actions': []}
+ rule = {"conditions": conditions, "actions": []}
with self.assertRaises(AssertionError):
engine.check_conditions_recursively(conditions, variables, rule)
def test_check_all_and_any_together(self):
- conditions = {'any': [], 'all': []}
+ conditions = {"any": [], "all": []}
variables = BaseVariables()
rule = {"conditions": conditions, "actions": []}
with self.assertRaises(AssertionError):
engine.check_conditions_recursively(conditions, variables, rule)
- @patch.object(engine, 'check_condition')
+ @patch.object(engine, "check_condition")
def test_nested_all_and_any(self, *args):
- conditions = {'all': [
- {'any': [{'name': 1}, {'name': 2}]},
- {'name': 3}
- ]}
-
- rule = {
- 'conditions': conditions,
- 'actions': {}
- }
+ conditions = {"all": [{"any": [{"name": 1}, {"name": 2}]}, {"name": 3}]}
+
+ rule = {"conditions": conditions, "actions": {}}
bv = BaseVariables()
def side_effect(condition, _, rule):
- return ConditionResult(result=condition['name'] in [2, 3], name=condition['name'], operator='', value='',
- parameters='')
+ return ConditionResult(
+ result=condition["name"] in [2, 3],
+ name=condition["name"],
+ operator="",
+ value="",
+ parameters="",
+ )
engine.check_condition.side_effect = side_effect
engine.check_conditions_recursively(conditions, bv, rule)
self.assertEqual(engine.check_condition.call_count, 3)
- engine.check_condition.assert_any_call({'name': 1}, bv, rule)
- engine.check_condition.assert_any_call({'name': 2}, bv, rule)
- engine.check_condition.assert_any_call({'name': 3}, bv, rule)
+ engine.check_condition.assert_any_call({"name": 1}, bv, rule)
+ engine.check_condition.assert_any_call({"name": 2}, bv, rule)
+ engine.check_condition.assert_any_call({"name": 3}, bv, rule)
# ##################################### #
# ####### Operator comparisons ######## #
# ##################################### #
def test_check_operator_comparison(self):
- string_type = StringType('yo yo')
- with patch.object(string_type, 'contains', return_value=True):
+ string_type = StringType("yo yo")
+ with patch.object(string_type, "contains", return_value=True):
result = engine._do_operator_comparison(
- string_type, 'contains', 'its mocked')
+ string_type, "contains", "its mocked"
+ )
self.assertTrue(result)
- string_type.contains.assert_called_once_with('its mocked')
+ string_type.contains.assert_called_once_with("its mocked")
# ##################################### #
# ############## Actions ############## #
@@ -197,23 +193,15 @@ def test_check_operator_comparison(self):
def test_do_actions(self):
function_params_mock = MagicMock()
function_params_mock.varkw = None
- with patch('business_rules.engine.getfullargspec', return_value=function_params_mock):
+ with patch(
+ "business_rules.engine.getfullargspec", return_value=function_params_mock
+ ):
rule_actions = [
- {
- 'name': 'action1'
- },
- {
- 'name': 'action2',
- 'params': {'param1': 'foo', 'param2': 10}
- }
+ {"name": "action1"},
+ {"name": "action2", "params": {"param1": "foo", "param2": 10}},
]
- rule = {
- 'conditions': {
-
- },
- 'actions': rule_actions
- }
+ rule = {"conditions": {}, "actions": rule_actions}
action1_mock = MagicMock()
action2_mock = MagicMock()
@@ -223,103 +211,99 @@ class SomeActions(BaseActions):
def action1(self):
return action1_mock()
- @rule_action(params={'param1': FIELD_TEXT, "param2": FIELD_NUMERIC})
+ @rule_action(params={"param1": FIELD_TEXT, "param2": FIELD_NUMERIC})
def action2(self, param1, param2):
return action2_mock(param1=param1, param2=param2)
defined_actions = SomeActions()
- payload = [(True, 'condition_name', 'operator_name', 'condition_value')]
+ payload = [(True, "condition_name", "operator_name", "condition_value")]
engine.do_actions(rule_actions, defined_actions, payload, rule)
action1_mock.assert_called_once_with()
- action2_mock.assert_called_once_with(param1='foo', param2=10)
+ action2_mock.assert_called_once_with(param1="foo", param2=10)
def test_do_actions_with_injected_parameters(self):
function_params_mock = MagicMock()
function_params_mock.varkw = True
- with patch('business_rules.engine.getfullargspec', return_value=function_params_mock):
+ with patch(
+ "business_rules.engine.getfullargspec", return_value=function_params_mock
+ ):
rule_actions = [
- {
- 'name': 'action1'
- },
- {
- 'name': 'action2',
- 'params': {'param1': 'foo', 'param2': 10}
- }
+ {"name": "action1"},
+ {"name": "action2", "params": {"param1": "foo", "param2": 10}},
]
- rule = {
- 'conditions': {
-
- },
- 'actions': rule_actions
- }
+ rule = {"conditions": {}, "actions": rule_actions}
defined_actions = BaseActions()
defined_actions.action1 = MagicMock()
defined_actions.action1.params = []
defined_actions.action2 = MagicMock()
- defined_actions.action2.params = [{
- 'label': 'action2',
- 'name': 'param1',
- 'fieldType': fields.FIELD_TEXT,
- 'defaultValue': None
- },
+ defined_actions.action2.params = [
+ {
+ "label": "action2",
+ "name": "param1",
+ "fieldType": fields.FIELD_TEXT,
+ "defaultValue": None,
+ },
{
- 'label': 'action2',
- 'name': 'param2',
- 'fieldType': fields.FIELD_NUMERIC,
- 'defaultValue': None
- }
+ "label": "action2",
+ "name": "param2",
+ "fieldType": fields.FIELD_NUMERIC,
+ "defaultValue": None,
+ },
]
- payload = [(True, 'condition_name', 'operator_name', 'condition_value')]
+ payload = [(True, "condition_name", "operator_name", "condition_value")]
engine.do_actions(rule_actions, defined_actions, payload, rule)
- defined_actions.action1.assert_called_once_with(conditions=payload, rule=rule)
- defined_actions.action2.assert_called_once_with(param1='foo', param2=10, conditions=payload, rule=rule)
+ defined_actions.action1.assert_called_once_with(
+ conditions=payload, rule=rule
+ )
+ defined_actions.action2.assert_called_once_with(
+ param1="foo", param2=10, conditions=payload, rule=rule
+ )
def test_do_with_invalid_action(self):
- actions = [{'name': 'fakeone'}]
+ actions = [{"name": "fakeone"}]
err_string = "Action fakeone is not defined in class BaseActions"
- rule = {
- 'conditions': {},
- 'actions': {}
- }
+ rule = {"conditions": {}, "actions": {}}
- checked_conditions_results = [(True, 'condition_name', 'operator_name', 'condition_value')]
+ checked_conditions_results = [
+ (True, "condition_name", "operator_name", "condition_value")
+ ]
- with self.assertRaisesRegexp(AssertionError, err_string):
+ with self.assertRaisesRegex(AssertionError, err_string):
engine.do_actions(actions, BaseActions(), checked_conditions_results, rule)
def test_do_with_parameter_with_default_value(self):
function_params_mock = MagicMock()
function_params_mock.varkw = None
- with patch('business_rules.engine.getfullargspec', return_value=function_params_mock):
- # param2 is not set in rule, but there is a default parameter for it in action which will be used instead
- rule_actions = [
- {
- 'name': 'some_action',
- 'params': {'param1': 'foo'}
- }
- ]
+ with patch(
+ "business_rules.engine.getfullargspec", return_value=function_params_mock
+ ):
+ # param2 is not set in rule, but there is a default parameter for it in
+ # action which will be used instead
+ rule_actions = [{"name": "some_action", "params": {"param1": "foo"}}]
- rule = {
- 'conditions': {
-
- },
- 'actions': rule_actions
- }
+ rule = {"conditions": {}, "actions": rule_actions}
- action_param_with_default_value = ActionParam(field_type=fields.FIELD_NUMERIC, default_value=42)
+ action_param_with_default_value = ActionParam(
+ field_type=fields.FIELD_NUMERIC, default_value=42
+ )
action_mock = MagicMock()
class SomeActions(BaseActions):
- @rule_action(params={'param1': FIELD_TEXT, 'param2': action_param_with_default_value})
+ @rule_action(
+ params={
+ "param1": FIELD_TEXT,
+ "param2": action_param_with_default_value,
+ }
+ )
def some_action(self, param1, param2):
return action_mock(param1=param1, param2=param2)
@@ -327,41 +311,35 @@ def some_action(self, param1, param2):
defined_actions.action = MagicMock()
defined_actions.action.params = {
- 'param1': fields.FIELD_TEXT,
- 'param2': action_param_with_default_value
+ "param1": fields.FIELD_TEXT,
+ "param2": action_param_with_default_value,
}
- payload = [(True, 'condition_name', 'operator_name', 'condition_value')]
+ payload = [(True, "condition_name", "operator_name", "condition_value")]
engine.do_actions(rule_actions, defined_actions, payload, rule)
- action_mock.assert_called_once_with(param1='foo', param2=42)
+ action_mock.assert_called_once_with(param1="foo", param2=42)
def test_default_param_overrides_action_param(self):
function_params_mock = MagicMock()
function_params_mock.varkw = None
- with patch('business_rules.engine.getfullargspec', return_value=function_params_mock):
- rule_actions = [
- {
- 'name': 'some_action',
- 'params': {'param1': False}
- }
- ]
+ with patch(
+ "business_rules.engine.getfullargspec", return_value=function_params_mock
+ ):
+ rule_actions = [{"name": "some_action", "params": {"param1": False}}]
- rule = {
- 'conditions': {
+ rule = {"conditions": {}, "actions": rule_actions}
- },
- 'actions': rule_actions
- }
-
- action_param_with_default_value = ActionParam(field_type=fields.FIELD_TEXT, default_value='bar')
+ action_param_with_default_value = ActionParam(
+ field_type=fields.FIELD_TEXT, default_value="bar"
+ )
action_mock = MagicMock()
class SomeActions(BaseActions):
- @rule_action(params={'param1': action_param_with_default_value})
+ @rule_action(params={"param1": action_param_with_default_value})
def some_action(self, param1):
return action_mock(param1=param1)
@@ -369,10 +347,10 @@ def some_action(self, param1):
defined_actions.action = MagicMock()
defined_actions.action.params = {
- 'param1': action_param_with_default_value,
+ "param1": action_param_with_default_value,
}
- payload = [(True, 'condition_name', 'operator_name', 'condition_value')]
+ payload = [(True, "condition_name", "operator_name", "condition_value")]
engine.do_actions(rule_actions, defined_actions, payload, rule)
@@ -381,15 +359,15 @@ def some_action(self, param1):
class EngineCheckConditionsTests(TestCase):
def test_case1(self):
- """ cond1: true and cond2: false => [] """
+ """cond1: true and cond2: false => []"""
conditions = {
- 'all': [
- {'name': 'true_variable', 'operator': 'is_true', 'value': ''},
- {'name': 'true_variable', 'operator': 'is_false', 'value': ''}
+ "all": [
+ {"name": "true_variable", "operator": "is_true", "value": ""},
+ {"name": "true_variable", "operator": "is_false", "value": ""},
]
}
variables = TrueVariables()
- rule = {'conditions': conditions, 'actions': []}
+ rule = {"conditions": conditions, "actions": []}
result = engine.check_conditions_recursively(conditions, variables, rule)
self.assertEqual(result, (False, []))
@@ -399,13 +377,13 @@ def test_case2(self):
cond1: false and cond2: true => []
"""
conditions = {
- 'all': [
- {'name': 'true_variable', 'operator': 'is_false', 'value': ''},
- {'name': 'true_variable', 'operator': 'is_true', 'value': ''}
+ "all": [
+ {"name": "true_variable", "operator": "is_false", "value": ""},
+ {"name": "true_variable", "operator": "is_true", "value": ""},
]
}
variables = TrueVariables()
- rule = {'conditions': conditions, 'actions': []}
+ rule = {"conditions": conditions, "actions": []}
result = engine.check_conditions_recursively(conditions, variables, rule)
self.assertEqual(result, (False, []))
@@ -415,61 +393,97 @@ def test_case3(self):
cond1: true and cond2: true => [cond1, cond2]
"""
conditions = {
- 'all': [
- {'name': 'true_variable', 'operator': 'is_true', 'value': ''},
- {'name': 'true_variable', 'operator': 'is_true', 'value': ''}
+ "all": [
+ {"name": "true_variable", "operator": "is_true", "value": ""},
+ {"name": "true_variable", "operator": "is_true", "value": ""},
]
}
variables = TrueVariables()
- rule = {'conditions': conditions, 'actions': []}
+ rule = {"conditions": conditions, "actions": []}
result = engine.check_conditions_recursively(conditions, variables, rule)
- self.assertEqual(result, (True, [
- ConditionResult(result=True, name='true_variable', operator='is_true', value='', parameters={}),
- ConditionResult(result=True, name='true_variable', operator='is_true', value='', parameters={}),
- ]))
+ self.assertEqual(
+ result,
+ (
+ True,
+ [
+ ConditionResult(
+ result=True,
+ name="true_variable",
+ operator="is_true",
+ value="",
+ parameters={},
+ ),
+ ConditionResult(
+ result=True,
+ name="true_variable",
+ operator="is_true",
+ value="",
+ parameters={},
+ ),
+ ],
+ ),
+ )
def test_case4(self):
"""
cond1: true and (cond2: false or cond3: true) => [cond1, cond3]
"""
conditions = {
- 'all': [
- {'name': 'true_variable', 'operator': 'is_true', 'value': '1'},
+ "all": [
+ {"name": "true_variable", "operator": "is_true", "value": "1"},
{
- 'any': [
- {'name': 'true_variable', 'operator': 'is_false', 'value': '2'},
- {'name': 'true_variable', 'operator': 'is_true', 'value': '3'}
+ "any": [
+ {"name": "true_variable", "operator": "is_false", "value": "2"},
+ {"name": "true_variable", "operator": "is_true", "value": "3"},
]
- }
+ },
]
}
variables = TrueVariables()
- rule = {'conditions': conditions, 'actions': []}
+ rule = {"conditions": conditions, "actions": []}
result = engine.check_conditions_recursively(conditions, variables, rule)
- self.assertEqual(result, (True, [
- ConditionResult(result=True, name='true_variable', operator='is_true', value='1', parameters={}),
- ConditionResult(result=True, name='true_variable', operator='is_true', value='3', parameters={}),
- ]))
+ self.assertEqual(
+ result,
+ (
+ True,
+ [
+ ConditionResult(
+ result=True,
+ name="true_variable",
+ operator="is_true",
+ value="1",
+ parameters={},
+ ),
+ ConditionResult(
+ result=True,
+ name="true_variable",
+ operator="is_true",
+ value="3",
+ parameters={},
+ ),
+ ],
+ ),
+ )
def test_case5(self):
"""
cond1: false and (cond2: false or cond3: true) => []
"""
conditions = {
- 'all': [
- {'name': 'true_variable', 'operator': 'is_false', 'value': '1'},
+ "all": [
+ {"name": "true_variable", "operator": "is_false", "value": "1"},
{
- 'any': [
- {'name': 'true_variable', 'operator': 'is_false', 'value': '2'},
- {'name': 'true_variable', 'operator': 'is_true', 'value': '3'}
+ "any": [
+ {"name": "true_variable", "operator": "is_false", "value": "2"},
+ {"name": "true_variable", "operator": "is_true", "value": "3"},
]
- }
+ },
]
}
variables = TrueVariables()
- rule = {'conditions': conditions, 'actions': []}
+ rule = {"conditions": conditions, "actions": []}
result = engine.check_conditions_recursively(conditions, variables, rule)
self.assertEqual(result, (False, []))
@@ -479,171 +493,279 @@ def test_case6(self):
cond1: true or (cond2: false or cond3: true) => [cond1]
"""
conditions = {
- 'any': [
- {'name': 'true_variable', 'operator': 'is_true', 'value': '1'},
+ "any": [
+ {"name": "true_variable", "operator": "is_true", "value": "1"},
{
- 'any': [
- {'name': 'true_variable', 'operator': 'is_false', 'value': '2'},
- {'name': 'true_variable', 'operator': 'is_true', 'value': '3'}
+ "any": [
+ {"name": "true_variable", "operator": "is_false", "value": "2"},
+ {"name": "true_variable", "operator": "is_true", "value": "3"},
]
- }
+ },
]
}
variables = TrueVariables()
- rule = {'conditions': conditions, 'actions': []}
+ rule = {"conditions": conditions, "actions": []}
result = engine.check_conditions_recursively(conditions, variables, rule)
- self.assertEqual(result, (True, [
- ConditionResult(result=True, name='true_variable', operator='is_true', value='1', parameters={}),
- ]))
+ self.assertEqual(
+ result,
+ (
+ True,
+ [
+ ConditionResult(
+ result=True,
+ name="true_variable",
+ operator="is_true",
+ value="1",
+ parameters={},
+ ),
+ ],
+ ),
+ )
def test_case7(self):
"""
cond1: false or (cond2: false or cond3: true) => [cond3]
"""
conditions = {
- 'any': [
- {'name': 'true_variable', 'operator': 'is_false', 'value': '1'},
+ "any": [
+ {"name": "true_variable", "operator": "is_false", "value": "1"},
{
- 'any': [
- {'name': 'true_variable', 'operator': 'is_false', 'value': '2'},
- {'name': 'true_variable', 'operator': 'is_true', 'value': '3'}
+ "any": [
+ {"name": "true_variable", "operator": "is_false", "value": "2"},
+ {"name": "true_variable", "operator": "is_true", "value": "3"},
]
- }
+ },
]
}
variables = TrueVariables()
- rule = {'conditions': conditions, 'actions': []}
+ rule = {"conditions": conditions, "actions": []}
result = engine.check_conditions_recursively(conditions, variables, rule)
- self.assertEqual(result, (True, [
- ConditionResult(result=True, name='true_variable', operator='is_true', value='3', parameters={}),
- ]))
+ self.assertEqual(
+ result,
+ (
+ True,
+ [
+ ConditionResult(
+ result=True,
+ name="true_variable",
+ operator="is_true",
+ value="3",
+ parameters={},
+ ),
+ ],
+ ),
+ )
def test_case8(self):
"""
cond1: false or (cond2: true and cond3: true) => [cond2, cond3]
"""
conditions = {
- 'any': [
- {'name': 'true_variable', 'operator': 'is_false', 'value': '1'},
+ "any": [
+ {"name": "true_variable", "operator": "is_false", "value": "1"},
{
- 'all': [
- {'name': 'true_variable', 'operator': 'is_true', 'value': '2'},
- {'name': 'true_variable', 'operator': 'is_true', 'value': '3'}
+ "all": [
+ {"name": "true_variable", "operator": "is_true", "value": "2"},
+ {"name": "true_variable", "operator": "is_true", "value": "3"},
]
- }
+ },
]
}
variables = TrueVariables()
- rule = {'conditions': conditions, 'actions': []}
+ rule = {"conditions": conditions, "actions": []}
result = engine.check_conditions_recursively(conditions, variables, rule)
- self.assertEqual(result, (True, [
- ConditionResult(result=True, name='true_variable', operator='is_true', value='2', parameters={}),
- ConditionResult(result=True, name='true_variable', operator='is_true', value='3', parameters={}),
- ]))
+ self.assertEqual(
+ result,
+ (
+ True,
+ [
+ ConditionResult(
+ result=True,
+ name="true_variable",
+ operator="is_true",
+ value="2",
+ parameters={},
+ ),
+ ConditionResult(
+ result=True,
+ name="true_variable",
+ operator="is_true",
+ value="3",
+ parameters={},
+ ),
+ ],
+ ),
+ )
def test_case9(self):
"""
(cond2: true and cond3: true) or cond1: true => [cond2, cond3]
"""
conditions = {
- 'any': [
+ "any": [
{
- 'all': [
- {'name': 'true_variable', 'operator': 'is_true', 'value': '2'},
- {'name': 'true_variable', 'operator': 'is_true', 'value': '3'}
+ "all": [
+ {"name": "true_variable", "operator": "is_true", "value": "2"},
+ {"name": "true_variable", "operator": "is_true", "value": "3"},
]
},
- {'name': 'true_variable', 'operator': 'is_false', 'value': '1'},
+ {"name": "true_variable", "operator": "is_false", "value": "1"},
]
}
variables = TrueVariables()
- rule = {'conditions': conditions, 'actions': []}
+ rule = {"conditions": conditions, "actions": []}
result = engine.check_conditions_recursively(conditions, variables, rule)
- self.assertEqual(result, (True, [
- ConditionResult(result=True, name='true_variable', operator='is_true', value='2', parameters={}),
- ConditionResult(result=True, name='true_variable', operator='is_true', value='3', parameters={}),
- ]))
+ self.assertEqual(
+ result,
+ (
+ True,
+ [
+ ConditionResult(
+ result=True,
+ name="true_variable",
+ operator="is_true",
+ value="2",
+ parameters={},
+ ),
+ ConditionResult(
+ result=True,
+ name="true_variable",
+ operator="is_true",
+ value="3",
+ parameters={},
+ ),
+ ],
+ ),
+ )
def test_case10(self):
"""
cond1: true or cond2: false => [cond1]
"""
conditions = {
- 'any': [
- {'name': 'true_variable', 'operator': 'is_true', 'value': '1'},
- {'name': 'true_variable', 'operator': 'is_false', 'value': '2'},
+ "any": [
+ {"name": "true_variable", "operator": "is_true", "value": "1"},
+ {"name": "true_variable", "operator": "is_false", "value": "2"},
]
}
variables = TrueVariables()
- rule = {'conditions': conditions, 'actions': []}
+ rule = {"conditions": conditions, "actions": []}
result = engine.check_conditions_recursively(conditions, variables, rule)
- self.assertEqual(result, (True, [
- ConditionResult(result=True, name='true_variable', operator='is_true', value='1', parameters={}),
- ]))
+ self.assertEqual(
+ result,
+ (
+ True,
+ [
+ ConditionResult(
+ result=True,
+ name="true_variable",
+ operator="is_true",
+ value="1",
+ parameters={},
+ ),
+ ],
+ ),
+ )
def test_case11(self):
"""
cond1: false or cond2: true => [cond2]
"""
conditions = {
- 'any': [
- {'name': 'true_variable', 'operator': 'is_false', 'value': '1'},
- {'name': 'true_variable', 'operator': 'is_true', 'value': '2'},
+ "any": [
+ {"name": "true_variable", "operator": "is_false", "value": "1"},
+ {"name": "true_variable", "operator": "is_true", "value": "2"},
]
}
variables = TrueVariables()
- rule = {'conditions': conditions, 'actions': []}
+ rule = {"conditions": conditions, "actions": []}
result = engine.check_conditions_recursively(conditions, variables, rule)
- self.assertEqual(result, (True, [
- ConditionResult(result=True, name='true_variable', operator='is_true', value='2', parameters={}),
- ]))
+ self.assertEqual(
+ result,
+ (
+ True,
+ [
+ ConditionResult(
+ result=True,
+ name="true_variable",
+ operator="is_true",
+ value="2",
+ parameters={},
+ ),
+ ],
+ ),
+ )
def test_case12(self):
"""
cond1: true or cond2: true => [cond1]
"""
conditions = {
- 'any': [
- {'name': 'true_variable', 'operator': 'is_true', 'value': '1'},
- {'name': 'true_variable', 'operator': 'is_true', 'value': '2'},
+ "any": [
+ {"name": "true_variable", "operator": "is_true", "value": "1"},
+ {"name": "true_variable", "operator": "is_true", "value": "2"},
]
}
variables = TrueVariables()
- rule = {'conditions': conditions, 'actions': []}
+ rule = {"conditions": conditions, "actions": []}
result = engine.check_conditions_recursively(conditions, variables, rule)
- self.assertEqual(result, (True, [
- ConditionResult(result=True, name='true_variable', operator='is_true', value='1', parameters={}),
- ]))
+ self.assertEqual(
+ result,
+ (
+ True,
+ [
+ ConditionResult(
+ result=True,
+ name="true_variable",
+ operator="is_true",
+ value="1",
+ parameters={},
+ ),
+ ],
+ ),
+ )
def test_case13(self):
"""
(cond1: true and cond2: false) or cond3: true => [cond3]
"""
conditions = {
- 'any': [
+ "any": [
{
- 'all': [
- {'name': 'true_variable', 'operator': 'is_true', 'value': '1'},
- {'name': 'true_variable', 'operator': 'is_false', 'value': '2'},
+ "all": [
+ {"name": "true_variable", "operator": "is_true", "value": "1"},
+ {"name": "true_variable", "operator": "is_false", "value": "2"},
]
},
- {'name': 'true_variable', 'operator': 'is_true', 'value': '3'},
+ {"name": "true_variable", "operator": "is_true", "value": "3"},
]
}
variables = TrueVariables()
- rule = {'conditions': conditions, 'actions': []}
+ rule = {"conditions": conditions, "actions": []}
result = engine.check_conditions_recursively(conditions, variables, rule)
- self.assertEqual(result, (True, [
- ConditionResult(result=True, name='true_variable', operator='is_true', value='3', parameters={}),
- ]))
+ self.assertEqual(
+ result,
+ (
+ True,
+ [
+ ConditionResult(
+ result=True,
+ name="true_variable",
+ operator="is_true",
+ value="3",
+ parameters={},
+ ),
+ ],
+ ),
+ )
class TrueVariables(BaseVariables):
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 56aea91b..6b17052e 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -1,9 +1,16 @@
from __future__ import absolute_import
-from business_rules.actions import rule_action, BaseActions
+
+from unittest import TestCase
+
+from business_rules.actions import BaseActions, rule_action
from business_rules.engine import check_condition, run_all
-from business_rules.fields import FIELD_TEXT, FIELD_NUMERIC, FIELD_SELECT
-from business_rules.variables import BaseVariables, string_rule_variable, numeric_rule_variable, boolean_rule_variable
-from . import TestCase
+from business_rules.fields import FIELD_NUMERIC, FIELD_SELECT, FIELD_TEXT
+from business_rules.variables import (
+ BaseVariables,
+ boolean_rule_variable,
+ numeric_rule_variable,
+ string_rule_variable,
+)
class SomeVariables(BaseVariables):
@@ -19,223 +26,186 @@ def ten(self):
def true_bool(self):
return True
- @numeric_rule_variable(params=[{'field_type': FIELD_NUMERIC, 'name': 'x', 'label': 'X'}])
+ @numeric_rule_variable(
+ params=[{"field_type": FIELD_NUMERIC, "name": "x", "label": "X"}]
+ )
def x_plus_one(self, x):
return x + 1
@boolean_rule_variable()
def rule_received(self, **kwargs):
- rule = kwargs.get('rule')
+ rule = kwargs.get("rule")
assert rule is not None
return rule is not None
- @string_rule_variable(label="StringLabel", options=['one', 'two', 'three'])
+ @string_rule_variable(label="StringLabel", options=["one", "two", "three"])
def string_variable_with_options(self):
return "foo"
@string_rule_variable(public=False)
def private_string_variable(self):
- return 'foo'
+ return "foo"
class SomeActions(BaseActions):
@rule_action(params={"foo": FIELD_NUMERIC})
- def some_action(self, foo): pass
+ def some_action(self, foo):
+ pass
@rule_action(label="woohoo", params={"bar": FIELD_TEXT})
- def some_other_action(self, bar): pass
-
- @rule_action(params=[
- {
- 'fieldType': FIELD_SELECT,
- 'name': 'baz',
- 'label': 'Baz',
- 'options': [
- {'label': 'Chose Me', 'name': 'chose_me'},
- {'label': 'Or Me', 'name': 'or_me'}
- ]
- }])
- def some_select_action(self, baz): pass
+ def some_other_action(self, bar):
+ pass
+
+ @rule_action(
+ params=[
+ {
+ "fieldType": FIELD_SELECT,
+ "name": "baz",
+ "label": "Baz",
+ "options": [
+ {"label": "Chose Me", "name": "chose_me"},
+ {"label": "Or Me", "name": "or_me"},
+ ],
+ }
+ ]
+ )
+ def some_select_action(self, baz):
+ pass
@rule_action()
- def action_with_no_params(self): pass
+ def action_with_no_params(self):
+ pass
class IntegrationTests(TestCase):
- """ Integration test, using the library like a user would.
- """
+ """Integration test, using the library like a user would."""
def test_true_boolean_variable(self):
- condition = {
- 'name': 'true_bool',
- 'operator': 'is_true',
- 'value': ''
- }
+ condition = {"name": "true_bool", "operator": "is_true", "value": ""}
- rule = {
- 'conditions': condition
- }
+ rule = {"conditions": condition}
condition_result = check_condition(condition, SomeVariables(), rule)
self.assertTrue(condition_result.result)
def test_false_boolean_variable(self):
- condition = {
- 'name': 'true_bool',
- 'operator': 'is_false',
- 'value': ''
- }
+ condition = {"name": "true_bool", "operator": "is_false", "value": ""}
- rule = {
- 'conditions': condition
- }
+ rule = {"conditions": condition}
condition_result = check_condition(condition, SomeVariables(), rule)
self.assertFalse(condition_result.result)
def test_check_true_condition_happy_path(self):
- condition = {'name': 'foo',
- 'operator': 'contains',
- 'value': 'o'}
+ condition = {"name": "foo", "operator": "contains", "value": "o"}
- rule = {
- 'conditions': condition
- }
+ rule = {"conditions": condition}
condition_result = check_condition(condition, SomeVariables(), rule)
self.assertTrue(condition_result.result)
def test_check_false_condition_happy_path(self):
- condition = {'name': 'foo',
- 'operator': 'contains',
- 'value': 'm'}
+ condition = {"name": "foo", "operator": "contains", "value": "m"}
- rule = {
- 'conditions': condition
- }
+ rule = {"conditions": condition}
condition_result = check_condition(condition, SomeVariables(), rule)
self.assertFalse(condition_result.result)
def test_numeric_variable_with_params(self):
condition = {
- 'name': 'x_plus_one',
- 'operator': 'equal_to',
- 'value': 10,
- 'params': {'x': 9}
+ "name": "x_plus_one",
+ "operator": "equal_to",
+ "value": 10,
+ "params": {"x": 9},
}
- rule = {
- 'conditions': condition
- }
+ rule = {"conditions": condition}
condition_result = check_condition(condition, SomeVariables(), rule)
self.assertTrue(condition_result.result)
def test_check_incorrect_method_name(self):
- condition = {
- 'name': 'food',
- 'operator': 'equal_to',
- 'value': 'm'
- }
-
- rule = {
- 'conditions': condition
- }
+ condition = {"name": "food", "operator": "equal_to", "value": "m"}
- err_string = 'Variable food is not defined in class SomeVariables'
+ rule = {"conditions": condition}
- with self.assertRaisesRegexp(AssertionError, err_string):
+ err_string = "Variable food is not defined in class SomeVariables"
+ with self.assertRaisesRegex(AssertionError, err_string):
check_condition(condition, SomeVariables(), rule)
def test_check_incorrect_operator_name(self):
- condition = {
- 'name': 'foo',
- 'operator': 'equal_tooooze',
- 'value': 'foo'
- }
+ condition = {"name": "foo", "operator": "equal_tooooze", "value": "foo"}
- rule = {
- 'conditions': condition
- }
+ rule = {"conditions": condition}
with self.assertRaises(AssertionError):
check_condition(condition, SomeVariables(), rule)
def test_check_missing_params(self):
condition = {
- 'name': 'x_plus_one',
- 'operator': 'equal_to',
- 'value': 10,
- 'params': {}
+ "name": "x_plus_one",
+ "operator": "equal_to",
+ "value": 10,
+ "params": {},
}
- rule = {
- 'conditions': condition
- }
+ rule = {"conditions": condition}
- err_string = 'Missing parameters x for variable x_plus_one'
+ err_string = "Missing parameters x for variable x_plus_one"
- with self.assertRaisesRegexp(AssertionError, err_string):
+ with self.assertRaisesRegex(AssertionError, err_string):
check_condition(condition, SomeVariables(), rule)
def test_check_invalid_params(self):
condition = {
- 'name': 'x_plus_one',
- 'operator': 'equal_to',
- 'value': 10,
- 'params': {'x': 9, 'y': 9}
+ "name": "x_plus_one",
+ "operator": "equal_to",
+ "value": 10,
+ "params": {"x": 9, "y": 9},
}
- rule = {
- 'conditions': condition
- }
+ rule = {"conditions": condition}
- err_string = 'Invalid parameters y for variable x_plus_one'
+ err_string = "Invalid parameters y for variable x_plus_one"
- with self.assertRaisesRegexp(AssertionError, err_string):
+ with self.assertRaisesRegex(AssertionError, err_string):
check_condition(condition, SomeVariables(), rule)
def test_variable_received_rules(self):
condition = {
- 'name': 'rule_received',
- 'operator': 'is_true',
- 'value': 'true',
+ "name": "rule_received",
+ "operator": "is_true",
+ "value": "true",
}
- rule = {
- 'conditions': condition
- }
+ rule = {"conditions": condition}
condition_result = check_condition(condition, SomeVariables(), rule)
self.assertTrue(condition_result)
def test_string_variable_with_options_with_wrong_value(self):
condition = {
- 'name': 'string_variable_with_options',
- 'operator': 'equal_to',
- 'value': 'foo',
+ "name": "string_variable_with_options",
+ "operator": "equal_to",
+ "value": "foo",
}
- rule = {
- 'conditions': condition
- }
+ rule = {"conditions": condition}
condition_result = check_condition(condition, SomeVariables(), rule)
self.assertTrue(condition_result)
def test_run_with_no_conditions(self):
- actions = [
- {
- 'name': 'action_with_no_params'
- }
- ]
+ actions = [{"name": "action_with_no_params"}]
- rule = {
- 'actions': actions
- }
+ rule = {"actions": actions}
- result = run_all(rule_list=[rule], defined_variables=SomeVariables(), defined_actions=SomeActions())
+ result = run_all(
+ rule_list=[rule],
+ defined_variables=SomeVariables(),
+ defined_actions=SomeActions(),
+ )
self.assertTrue(result)
diff --git a/tests/test_operators.py b/tests/test_operators.py
new file mode 100644
index 00000000..ce61733b
--- /dev/null
+++ b/tests/test_operators.py
@@ -0,0 +1,188 @@
+from decimal import Decimal
+from unittest import TestCase
+
+from business_rules.operators import (
+ BooleanType,
+ NumericType,
+ SelectMultipleType,
+ SelectType,
+ StringType,
+)
+
+
+class StringOperatorTests(TestCase):
+ def test_operator_decorator(self):
+ self.assertTrue(StringType("foo").equal_to.is_operator)
+
+ def test_string_equal_to(self):
+ self.assertTrue(StringType("foo").equal_to("foo"))
+ self.assertFalse(StringType("foo").equal_to("Foo"))
+
+ def test_string_equal_to_case_insensitive(self):
+ self.assertTrue(StringType("foo").equal_to_case_insensitive("FOo"))
+ self.assertTrue(StringType("foo").equal_to_case_insensitive("foo"))
+ self.assertFalse(StringType("foo").equal_to_case_insensitive("blah"))
+
+ def test_string_starts_with(self):
+ self.assertTrue(StringType("hello").starts_with("he"))
+ self.assertFalse(StringType("hello").starts_with("hey"))
+ self.assertFalse(StringType("hello").starts_with("He"))
+
+ def test_string_ends_with(self):
+ self.assertTrue(StringType("hello").ends_with("lo"))
+ self.assertFalse(StringType("hello").ends_with("boom"))
+ self.assertFalse(StringType("hello").ends_with("Lo"))
+
+ def test_string_contains(self):
+ self.assertTrue(StringType("hello").contains("ell"))
+ self.assertTrue(StringType("hello").contains("he"))
+ self.assertTrue(StringType("hello").contains("lo"))
+ self.assertFalse(StringType("hello").contains("asdf"))
+ self.assertFalse(StringType("hello").contains("ElL"))
+
+ def test_string_matches_regex(self):
+ self.assertTrue(StringType("hello").matches_regex(r"^h"))
+ self.assertFalse(StringType("hello").matches_regex(r"^sh"))
+
+ def test_non_empty(self):
+ self.assertTrue(StringType("hello").non_empty())
+ self.assertFalse(StringType("").non_empty())
+ self.assertFalse(StringType(None).non_empty())
+
+
+class NumericOperatorTests(TestCase):
+ def test_instantiate(self):
+ err_string = "foo is not a valid numeric type"
+ with self.assertRaisesRegex(AssertionError, err_string):
+ NumericType("foo")
+
+ def test_numeric_type_validates_and_casts_decimal(self):
+ ten_dec = Decimal(10)
+ ten_int = 10
+ ten_float = 10.0
+ ten_long = int(10) # long and int are same in python3
+ ten_var_dec = NumericType(ten_dec) # this should not throw an exception
+ ten_var_int = NumericType(ten_int)
+ ten_var_float = NumericType(ten_float)
+ ten_var_long = NumericType(ten_long)
+ self.assertTrue(isinstance(ten_var_dec.value, Decimal))
+ self.assertTrue(isinstance(ten_var_int.value, Decimal))
+ self.assertTrue(isinstance(ten_var_float.value, Decimal))
+ self.assertTrue(isinstance(ten_var_long.value, Decimal))
+
+ def test_numeric_equal_to(self):
+ self.assertTrue(NumericType(10).equal_to(10))
+ self.assertTrue(NumericType(10).equal_to(10.0))
+ self.assertTrue(NumericType(10).equal_to(10.000001))
+ self.assertTrue(NumericType(10.000001).equal_to(10))
+ self.assertTrue(NumericType(Decimal("10.0")).equal_to(10))
+ self.assertTrue(NumericType(10).equal_to(Decimal("10.0")))
+ self.assertFalse(NumericType(10).equal_to(10.00001))
+ self.assertFalse(NumericType(10).equal_to(11))
+
+ def test_other_value_not_numeric(self):
+ error_string = "10 is not a valid numeric type"
+ with self.assertRaisesRegex(AssertionError, error_string):
+ NumericType(10).equal_to("10")
+
+ def test_numeric_greater_than(self):
+ self.assertTrue(NumericType(10).greater_than(1))
+ self.assertFalse(NumericType(10).greater_than(11))
+ self.assertTrue(NumericType(10.1).greater_than(10))
+ self.assertFalse(NumericType(10.000001).greater_than(10))
+ self.assertTrue(NumericType(10.000002).greater_than(10))
+
+ def test_numeric_greater_than_or_equal_to(self):
+ self.assertTrue(NumericType(10).greater_than_or_equal_to(1))
+ self.assertFalse(NumericType(10).greater_than_or_equal_to(11))
+ self.assertTrue(NumericType(10.1).greater_than_or_equal_to(10))
+ self.assertTrue(NumericType(10.000001).greater_than_or_equal_to(10))
+ self.assertTrue(NumericType(10.000002).greater_than_or_equal_to(10))
+ self.assertTrue(NumericType(10).greater_than_or_equal_to(10))
+
+ def test_numeric_less_than(self):
+ self.assertTrue(NumericType(1).less_than(10))
+ self.assertFalse(NumericType(11).less_than(10))
+ self.assertTrue(NumericType(10).less_than(10.1))
+ self.assertFalse(NumericType(10).less_than(10.000001))
+ self.assertTrue(NumericType(10).less_than(10.000002))
+
+ def test_numeric_less_than_or_equal_to(self):
+ self.assertTrue(NumericType(1).less_than_or_equal_to(10))
+ self.assertFalse(NumericType(11).less_than_or_equal_to(10))
+ self.assertTrue(NumericType(10).less_than_or_equal_to(10.1))
+ self.assertTrue(NumericType(10).less_than_or_equal_to(10.000001))
+ self.assertTrue(NumericType(10).less_than_or_equal_to(10.000002))
+ self.assertTrue(NumericType(10).less_than_or_equal_to(10))
+
+
+class BooleanOperatorTests(TestCase):
+ def test_instantiate(self):
+ err_string = "foo is not a valid boolean type"
+ with self.assertRaisesRegex(AssertionError, err_string):
+ BooleanType("foo")
+ err_string = "None is not a valid boolean type"
+ with self.assertRaisesRegex(AssertionError, err_string):
+ BooleanType(None)
+
+ def test_boolean_is_true_and_is_false(self):
+ self.assertTrue(BooleanType(True).is_true())
+ self.assertFalse(BooleanType(True).is_false())
+ self.assertFalse(BooleanType(False).is_true())
+ self.assertTrue(BooleanType(False).is_false())
+
+
+class SelectOperatorTests(TestCase):
+ def test_contains(self):
+ self.assertTrue(SelectType([1, 2]).contains(2))
+ self.assertFalse(SelectType([1, 2]).contains(3))
+ self.assertTrue(SelectType([1, 2, "a"]).contains("A"))
+
+ def test_does_not_contain(self):
+ self.assertTrue(SelectType([1, 2]).does_not_contain(3))
+ self.assertFalse(SelectType([1, 2]).does_not_contain(2))
+ self.assertFalse(SelectType([1, 2, "a"]).does_not_contain("A"))
+
+
+class SelectMultipleOperatorTests(TestCase):
+ def test_contains_all(self):
+ self.assertTrue(SelectMultipleType([1, 2]).contains_all([2, 1]))
+ self.assertFalse(SelectMultipleType([1, 2]).contains_all([2, 3]))
+ self.assertTrue(SelectMultipleType([1, 2, "a"]).contains_all([2, 1, "A"]))
+
+ def test_is_contained_by(self):
+ self.assertTrue(SelectMultipleType([1, 2]).is_contained_by([2, 1, 3]))
+ self.assertFalse(SelectMultipleType([1, 2]).is_contained_by([2, 3, 4]))
+ self.assertTrue(SelectMultipleType([1, 2, "a"]).is_contained_by([2, 1, "A"]))
+
+ def test_shares_at_least_one_element_with(self):
+ self.assertTrue(
+ SelectMultipleType([1, 2]).shares_at_least_one_element_with([2, 3])
+ )
+ self.assertFalse(
+ SelectMultipleType([1, 2]).shares_at_least_one_element_with([4, 3])
+ )
+ self.assertTrue(
+ SelectMultipleType([1, 2, "a"]).shares_at_least_one_element_with([4, "A"])
+ )
+
+ def test_shares_exactly_one_element_with(self):
+ self.assertTrue(
+ SelectMultipleType([1, 2]).shares_exactly_one_element_with([2, 3])
+ )
+ self.assertFalse(
+ SelectMultipleType([1, 2]).shares_exactly_one_element_with([4, 3])
+ )
+ self.assertTrue(
+ SelectMultipleType([1, 2, "a"]).shares_exactly_one_element_with([4, "A"])
+ )
+ self.assertFalse(
+ SelectMultipleType([1, 2, 3]).shares_exactly_one_element_with([2, 3, "a"])
+ )
+
+ def test_shares_no_elements_with(self):
+ self.assertTrue(SelectMultipleType([1, 2]).shares_no_elements_with([4, 3]))
+ self.assertFalse(SelectMultipleType([1, 2]).shares_no_elements_with([2, 3]))
+ self.assertFalse(
+ SelectMultipleType([1, 2, "a"]).shares_no_elements_with([4, "A"])
+ )
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 5cdddefa..fffdd188 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,11 +1,11 @@
from __future__ import absolute_import
+
import pytest
-from business_rules import fields
-from business_rules import utils
+from business_rules import fields, utils
from business_rules.fields import FIELD_DATETIME, FIELD_TIME
from tests import actions, variables
-from tests.test_integration import SomeVariables, SomeActions
+from tests.test_integration import SomeActions, SomeVariables
def test_fn_name_to_pretty_label():
@@ -56,7 +56,7 @@ def test_export_rule_data():
{
"name": "action_with_no_params",
"label": "Action With No Params",
- "params": None
+ "params": None,
},
{
"name": "some_action",
@@ -66,9 +66,9 @@ def test_export_rule_data():
'fieldType': 'numeric',
'label': 'Foo',
'name': 'foo',
- 'defaultValue': None
+ 'defaultValue': None,
}
- ]
+ ],
},
{
"name": "some_other_action",
@@ -78,9 +78,9 @@ def test_export_rule_data():
'fieldType': 'text',
'label': 'Bar',
'name': 'bar',
- 'defaultValue': None
+ 'defaultValue': None,
}
- ]
+ ],
},
{
"name": "some_select_action",
@@ -91,12 +91,12 @@ def test_export_rule_data():
'name': 'baz',
'label': 'Baz',
'options': [
- {'label': 'Chose Me', 'name': 'chose_me'},
- {'label': 'Or Me', 'name': 'or_me'}
- ]
+ {'label': 'Chose Me', 'name': 'chose_me'},
+ {'label': 'Or Me', 'name': 'or_me'},
+ ],
}
- ]
- }
+ ],
+ },
]
assert all_data.get("variables") == [
@@ -106,7 +106,7 @@ def test_export_rule_data():
"field_type": "string",
"options": [],
"params": [],
- "public": True
+ "public": True,
},
{
'name': 'private_string_variable',
@@ -114,7 +114,7 @@ def test_export_rule_data():
'field_type': 'string',
'options': [],
'params': [],
- 'public': False
+ 'public': False,
},
{
'name': 'rule_received',
@@ -122,7 +122,7 @@ def test_export_rule_data():
'field_type': 'boolean',
'options': [],
'params': [],
- "public": True
+ "public": True,
},
{
'name': 'string_variable_with_options',
@@ -130,7 +130,7 @@ def test_export_rule_data():
'field_type': 'string',
'options': ['one', 'two', 'three'],
'params': [],
- "public": True
+ "public": True,
},
{
"name": "ten",
@@ -138,7 +138,7 @@ def test_export_rule_data():
"field_type": "numeric",
"options": [],
"params": [],
- "public": True
+ "public": True,
},
{
'name': 'true_bool',
@@ -146,128 +146,119 @@ def test_export_rule_data():
'field_type': 'boolean',
'options': [],
"params": [],
- "public": True
+ "public": True,
},
{
'name': 'x_plus_one',
'label': 'X Plus One',
'field_type': 'numeric',
'options': [],
- 'params': [
- {'field_type': 'numeric', 'name': 'x', 'label': 'X'}
- ],
- "public": True
- }
+ 'params': [{'field_type': 'numeric', 'name': 'x', 'label': 'X'}],
+ "public": True,
+ },
]
assert all_data.get("variable_type_operators") == {
- 'boolean': [{'input_type': 'none',
- 'label': 'Is False',
- 'name': 'is_false'},
- {'input_type': 'none',
- 'label': 'Is True',
- 'name': 'is_true'}],
+ 'boolean': [
+ {'input_type': 'none', 'label': 'Is False', 'name': 'is_false'},
+ {'input_type': 'none', 'label': 'Is True', 'name': 'is_true'},
+ ],
'datetime': [
- {
- 'input_type': FIELD_DATETIME,
- 'label': 'After Than',
- 'name': 'after_than'
- },
+ {'input_type': FIELD_DATETIME, 'label': 'After Than', 'name': 'after_than'},
{
'input_type': FIELD_DATETIME,
'label': 'After Than Or Equal To',
- 'name': 'after_than_or_equal_to'
+ 'name': 'after_than_or_equal_to',
},
{
'input_type': FIELD_DATETIME,
'label': 'Before Than',
- 'name': 'before_than'
+ 'name': 'before_than',
},
{
'input_type': FIELD_DATETIME,
'label': 'Before Than Or Equal To',
- 'name': 'before_than_or_equal_to'
+ 'name': 'before_than_or_equal_to',
},
+ {'input_type': FIELD_DATETIME, 'label': 'Equal To', 'name': 'equal_to'},
+ ],
+ 'numeric': [
+ {'input_type': 'numeric', 'label': 'Equal To', 'name': 'equal_to'},
+ {'input_type': 'numeric', 'label': 'Greater Than', 'name': 'greater_than'},
{
- 'input_type': FIELD_DATETIME,
- 'label': 'Equal To',
- 'name': 'equal_to'
+ 'input_type': 'numeric',
+ 'label': 'Greater Than Or Equal To',
+ 'name': 'greater_than_or_equal_to',
+ },
+ {'input_type': 'numeric', 'label': 'Less Than', 'name': 'less_than'},
+ {
+ 'input_type': 'numeric',
+ 'label': 'Less Than Or Equal To',
+ 'name': 'less_than_or_equal_to',
},
],
- 'numeric': [{'input_type': 'numeric',
- 'label': 'Equal To',
- 'name': 'equal_to'},
- {'input_type': 'numeric', 'label': 'Greater Than',
- 'name': 'greater_than'},
- {'input_type': 'numeric',
- 'label': 'Greater Than Or Equal To',
- 'name': 'greater_than_or_equal_to'},
- {'input_type': 'numeric', 'label': 'Less Than',
- 'name': 'less_than'},
- {'input_type': 'numeric',
- 'label': 'Less Than Or Equal To',
- 'name': 'less_than_or_equal_to'}],
- 'select': [{'input_type': 'select', 'label': 'Contains',
- 'name': 'contains'},
- {'input_type': 'select',
- 'label': 'Does Not Contain',
- 'name': 'does_not_contain'}],
- 'select_multiple': [{'input_type': 'select_multiple',
- 'label': 'Contains All',
- 'name': 'contains_all'},
- {'input_type': 'select_multiple',
- 'label': 'Is Contained By',
- 'name': 'is_contained_by'},
- {'input_type': 'select_multiple',
- 'label': 'Shares At Least One Element With',
- 'name': 'shares_at_least_one_element_with'},
- {'input_type': 'select_multiple',
- 'label': 'Shares Exactly One Element With',
- 'name': 'shares_exactly_one_element_with'},
- {'input_type': 'select_multiple',
- 'label': 'Shares No Elements With',
- 'name': 'shares_no_elements_with'}],
- 'string': [{'input_type': 'text', 'label': 'Contains',
- 'name': 'contains'},
- {'input_type': 'text', 'label': 'Ends With',
- 'name': 'ends_with'},
- {'input_type': 'text', 'label': 'Equal To',
- 'name': 'equal_to'},
- {'input_type': 'text',
- 'label': 'Equal To (case insensitive)',
- 'name': 'equal_to_case_insensitive'},
- {'input_type': 'text', 'label': 'Matches Regex',
- 'name': 'matches_regex'},
- {'input_type': 'none', 'label': 'Non Empty',
- 'name': 'non_empty'},
- {'input_type': 'text', 'label': 'Starts With',
- 'name': 'starts_with'}],
- 'time': [
+ 'select': [
+ {'input_type': 'select', 'label': 'Contains', 'name': 'contains'},
{
- 'input_type': FIELD_TIME,
- 'label': 'After Than',
- 'name': 'after_than'
+ 'input_type': 'select',
+ 'label': 'Does Not Contain',
+ 'name': 'does_not_contain',
+ },
+ ],
+ 'select_multiple': [
+ {
+ 'input_type': 'select_multiple',
+ 'label': 'Contains All',
+ 'name': 'contains_all',
},
{
- 'input_type': FIELD_TIME,
- 'label': 'After Than Or Equal To',
- 'name': 'after_than_or_equal_to'
+ 'input_type': 'select_multiple',
+ 'label': 'Is Contained By',
+ 'name': 'is_contained_by',
},
{
- 'input_type': FIELD_TIME,
- 'label': 'Before Than',
- 'name': 'before_than'
+ 'input_type': 'select_multiple',
+ 'label': 'Shares At Least One Element With',
+ 'name': 'shares_at_least_one_element_with',
},
+ {
+ 'input_type': 'select_multiple',
+ 'label': 'Shares Exactly One Element With',
+ 'name': 'shares_exactly_one_element_with',
+ },
+ {
+ 'input_type': 'select_multiple',
+ 'label': 'Shares No Elements With',
+ 'name': 'shares_no_elements_with',
+ },
+ ],
+ 'string': [
+ {'input_type': 'text', 'label': 'Contains', 'name': 'contains'},
+ {'input_type': 'text', 'label': 'Ends With', 'name': 'ends_with'},
+ {'input_type': 'text', 'label': 'Equal To', 'name': 'equal_to'},
+ {
+ 'input_type': 'text',
+ 'label': 'Equal To (case insensitive)',
+ 'name': 'equal_to_case_insensitive',
+ },
+ {'input_type': 'text', 'label': 'Matches Regex', 'name': 'matches_regex'},
+ {'input_type': 'none', 'label': 'Non Empty', 'name': 'non_empty'},
+ {'input_type': 'text', 'label': 'Starts With', 'name': 'starts_with'},
+ ],
+ 'time': [
+ {'input_type': FIELD_TIME, 'label': 'After Than', 'name': 'after_than'},
{
'input_type': FIELD_TIME,
- 'label': 'Before Than Or Equal To',
- 'name': 'before_than_or_equal_to'
+ 'label': 'After Than Or Equal To',
+ 'name': 'after_than_or_equal_to',
},
+ {'input_type': FIELD_TIME, 'label': 'Before Than', 'name': 'before_than'},
{
'input_type': FIELD_TIME,
- 'label': 'Equal To',
- 'name': 'equal_to'
+ 'label': 'Before Than Or Equal To',
+ 'name': 'before_than_or_equal_to',
},
+ {'input_type': FIELD_TIME, 'label': 'Equal To', 'name': 'equal_to'},
],
}
@@ -276,51 +267,24 @@ def test_validate_rule_data_success():
valid_rule = {
'conditions': {
'all': [
- {
- 'name': 'bool_variable',
- 'operator': 'is_false',
- 'value': ''
- },
- {
- 'name': 'str_variable',
- 'operator': 'contains',
- 'value': 'test'
- },
+ {'name': 'bool_variable', 'operator': 'is_false', 'value': ''},
+ {'name': 'str_variable', 'operator': 'contains', 'value': 'test'},
{
'name': 'select_multiple_variable',
'operator': 'contains_all',
- 'value': [1, 2, 3]
- },
- {
- 'name': 'numeric_variable',
- 'operator': 'equal_to',
- 'value': 1
+ 'value': [1, 2, 3],
},
+ {'name': 'numeric_variable', 'operator': 'equal_to', 'value': 1},
{
'name': 'datetime_variable',
'operator': 'equal_to',
- 'value': '2016-01-01'
+ 'value': '2016-01-01',
},
- {
- 'name': 'time_variable',
- 'operator': 'equal_to',
- 'value': '10:00:00'
- },
- {
- 'name': 'select_variable',
- 'operator': 'contains',
- 'value': [1]
- }
+ {'name': 'time_variable', 'operator': 'equal_to', 'value': '10:00:00'},
+ {'name': 'select_variable', 'operator': 'contains', 'value': [1]},
]
},
- 'actions': [
- {
- 'name': 'example_action',
- 'params': {
- 'param': 1
- }
- }
- ]
+ 'actions': [{'name': 'example_action', 'params': {'param': 1}}],
}
utils.validate_rule_data(variables.TestVariables, actions.TestActions, valid_rule)
@@ -329,35 +293,24 @@ def test_validate_rule_data_nested_success():
valid_rule = {
'conditions': {
'any': [
- {
- 'name': 'bool_variable',
- 'operator': 'is_false',
- 'value': ''
- },
+ {'name': 'bool_variable', 'operator': 'is_false', 'value': ''},
{
"all": [
{
'name': 'str_variable',
'operator': 'contains',
- 'value': 'test'
+ 'value': 'test',
},
{
'name': 'select_multiple_variable',
'operator': 'contains_all',
- 'value': [1, 2, 3]
+ 'value': [1, 2, 3],
},
]
- }
+ },
]
},
- 'actions': [
- {
- 'name': 'example_action',
- 'params': {
- 'param': 1
- }
- }
- ]
+ 'actions': [{'name': 'example_action', 'params': {'param': 1}}],
}
utils.validate_rule_data(variables.TestVariables, actions.TestActions, valid_rule)
@@ -368,96 +321,70 @@ def test_validate_rule_data_empty_dict():
def test_validate_rule_data_no_conditions():
- invalid_rule = {
- 'actions': []
- }
+ invalid_rule = {'actions': []}
utils.validate_rule_data(variables.TestVariables, actions.TestActions, invalid_rule)
def test_validate_rule_data_no_actions():
- invalid_rule = {
- 'conditions': {}
- }
+ invalid_rule = {'conditions': {}}
with pytest.raises(AssertionError):
- utils.validate_rule_data(variables.TestVariables, actions.TestActions, invalid_rule)
+ utils.validate_rule_data(
+ variables.TestVariables, actions.TestActions, invalid_rule
+ )
def test_validate_rule_data_unknown_action():
- invalid_rule = {
- 'conditions': {},
- 'actions': [
- {
- 'name': 'unknown',
- 'params': {}
- }
- ]
- }
+ invalid_rule = {'conditions': {}, 'actions': [{'name': 'unknown', 'params': {}}]}
with pytest.raises(AssertionError):
- utils.validate_rule_data(variables.TestVariables, actions.TestActions, invalid_rule)
+ utils.validate_rule_data(
+ variables.TestVariables, actions.TestActions, invalid_rule
+ )
def test_validate_rule_data_missing_action_name():
- invalid_rule = {
- 'conditions': {},
- 'actions': [
- {
- 'params': {}
- }
- ]
- }
+ invalid_rule = {'conditions': {}, 'actions': [{'params': {}}]}
with pytest.raises(AssertionError):
- utils.validate_rule_data(variables.TestVariables, actions.TestActions, invalid_rule)
+ utils.validate_rule_data(
+ variables.TestVariables, actions.TestActions, invalid_rule
+ )
def test_validate_rule_data_unknown_condition_name():
invalid_rule = {
'conditions': {
- 'any': [
- {
- 'name': 'unknown',
- 'operator': 'unknown',
- 'value': 'unknown'
- }
- ]
+ 'any': [{'name': 'unknown', 'operator': 'unknown', 'value': 'unknown'}]
},
- 'actions': []
+ 'actions': [],
}
with pytest.raises(AssertionError):
- utils.validate_rule_data(variables.TestVariables, actions.TestActions, invalid_rule)
+ utils.validate_rule_data(
+ variables.TestVariables, actions.TestActions, invalid_rule
+ )
def test_validate_rule_data_unknown_condition_operator():
invalid_rule = {
'conditions': {
- 'any': [
- {
- 'name': 'bool_variable',
- 'operator': 'unknown',
- 'value': ''
- }
- ]
+ 'any': [{'name': 'bool_variable', 'operator': 'unknown', 'value': ''}]
},
- 'actions': []
+ 'actions': [],
}
with pytest.raises(AssertionError):
- utils.validate_rule_data(variables.TestVariables, actions.TestActions, invalid_rule)
+ utils.validate_rule_data(
+ variables.TestVariables, actions.TestActions, invalid_rule
+ )
def test_validate_rule_data_missing_condition_operator():
invalid_rule = {
- 'conditions': {
- 'any': [
- {
- 'name': 'bool_variable',
- 'value': ''
- }
- ]
- },
- 'actions': []
+ 'conditions': {'any': [{'name': 'bool_variable', 'value': ''}]},
+ 'actions': [],
}
with pytest.raises(AssertionError):
- utils.validate_rule_data(variables.TestVariables, actions.TestActions, invalid_rule)
+ utils.validate_rule_data(
+ variables.TestVariables, actions.TestActions, invalid_rule
+ )
def test_validate_rule_data_bool_value_ignored():
@@ -467,11 +394,11 @@ def test_validate_rule_data_bool_value_ignored():
{
'name': 'bool_variable',
'operator': 'is_true',
- 'value': 'any value here'
+ 'value': 'any value here',
}
]
},
- 'actions': []
+ 'actions': [],
}
utils.validate_rule_data(variables.TestVariables, actions.TestActions, invalid_rule)
@@ -479,18 +406,14 @@ def test_validate_rule_data_bool_value_ignored():
def test_validate_rule_data_bad_condition_value():
invalid_rule = {
'conditions': {
- 'any': [
- {
- 'name': 'string_variable',
- 'operator': 'equal_to',
- 'value': 123
- }
- ]
+ 'any': [{'name': 'string_variable', 'operator': 'equal_to', 'value': 123}]
},
- 'actions': []
+ 'actions': [],
}
with pytest.raises(AssertionError):
- utils.validate_rule_data(variables.TestVariables, actions.TestActions, invalid_rule)
+ utils.validate_rule_data(
+ variables.TestVariables, actions.TestActions, invalid_rule
+ )
def test_validate_rule_data_unknown_condition_key():
@@ -501,62 +424,49 @@ def test_validate_rule_data_unknown_condition_key():
'name': 'string_variable',
'operator': 'equal_to',
'value': 'test',
- 'unknown': 'unknown'
+ 'unknown': 'unknown',
}
]
},
- 'actions': []
+ 'actions': [],
}
with pytest.raises(AssertionError):
- utils.validate_rule_data(variables.TestVariables, actions.TestActions, invalid_rule)
+ utils.validate_rule_data(
+ variables.TestVariables, actions.TestActions, invalid_rule
+ )
def test_validate_rule_multiple_special_keys_in_condition():
- """ A rule cannot contain more than one 'any' or 'all' keys """
+ """A rule cannot contain more than one 'any' or 'all' keys"""
invalid_rule = {
'conditions': {
- 'any': [
- {
- 'name': 'bool_variable',
- 'operator': 'is_false',
- 'value': ''
- }
- ],
- 'all': [
- {
- 'name': 'bool_variable',
- 'operator': 'is_false',
- 'value': ''
- }
- ]
+ 'any': [{'name': 'bool_variable', 'operator': 'is_false', 'value': ''}],
+ 'all': [{'name': 'bool_variable', 'operator': 'is_false', 'value': ''}],
},
- 'actions': []
+ 'actions': [],
}
with pytest.raises(AssertionError):
- utils.validate_rule_data(variables.TestVariables, actions.TestActions, invalid_rule)
+ utils.validate_rule_data(
+ variables.TestVariables, actions.TestActions, invalid_rule
+ )
def test_validate_rule_actions_is_not_a_list():
invalid_rule = {
'conditions': {
- 'any': [
- {
- 'name': 'bool_variable',
- 'operator': 'is_false',
- 'value': ''
- }
- ]
+ 'any': [{'name': 'bool_variable', 'operator': 'is_false', 'value': ''}]
},
- 'actions': {}
+ 'actions': {},
}
with pytest.raises(AssertionError):
- utils.validate_rule_data(variables.TestVariables, actions.TestActions, invalid_rule)
+ utils.validate_rule_data(
+ variables.TestVariables, actions.TestActions, invalid_rule
+ )
def test_validate_rule_contions_is_not_a_dictionary():
- invalid_rule = {
- 'conditions': [],
- 'actions': {}
- }
+ invalid_rule = {'conditions': [], 'actions': {}}
with pytest.raises(AssertionError):
- utils.validate_rule_data(variables.TestVariables, actions.TestActions, invalid_rule)
+ utils.validate_rule_data(
+ variables.TestVariables, actions.TestActions, invalid_rule
+ )
diff --git a/tests/test_variables.py b/tests/test_variables.py
index 0e7425ab..5a646743 100644
--- a/tests/test_variables.py
+++ b/tests/test_variables.py
@@ -1,44 +1,45 @@
-from __future__ import absolute_import
-from . import TestCase
-from business_rules.utils import fn_name_to_pretty_label
-from business_rules.variables import (
- rule_variable,
- numeric_rule_variable,
- string_rule_variable,
- boolean_rule_variable,
- select_rule_variable,
- select_multiple_rule_variable,
- datetime_rule_variable,
- time_rule_variable,
-)
+from unittest import TestCase
from business_rules.operators import (
- NumericType,
- StringType,
BooleanType,
- SelectType,
- SelectMultipleType,
DateTimeType,
+ NumericType,
+ SelectMultipleType,
+ SelectType,
+ StringType,
TimeType,
)
+from business_rules.utils import fn_name_to_pretty_label
+from business_rules.variables import (
+ boolean_rule_variable,
+ datetime_rule_variable,
+ numeric_rule_variable,
+ rule_variable,
+ select_multiple_rule_variable,
+ select_rule_variable,
+ string_rule_variable,
+ time_rule_variable,
+)
class RuleVariableTests(TestCase):
- """ Tests for the base rule_variable decorator.
- """
+ """Tests for the base rule_variable decorator."""
def test_pretty_label(self):
self.assertEqual(
- fn_name_to_pretty_label('some_name_Of_a_thing'),
- 'Some Name Of A Thing')
- self.assertEqual(fn_name_to_pretty_label('hi'), 'Hi')
+ fn_name_to_pretty_label("some_name_Of_a_thing"), "Some Name Of A Thing"
+ )
+ self.assertEqual(fn_name_to_pretty_label("hi"), "Hi")
def test_rule_variable_requires_instance_of_base_type(self):
- err_string = "a_string is not instance of BaseType in rule_variable " \
- "field_type"
- with self.assertRaisesRegexp(AssertionError, err_string):
- @rule_variable('a_string')
- def some_test_function(self): pass
+ err_string = (
+ "a_string is not instance of BaseType in rule_variable " "field_type"
+ )
+ with self.assertRaisesRegex(AssertionError, err_string):
+
+ @rule_variable("a_string")
+ def some_test_function(self):
+ pass
def test_rule_variable_decorator_internals(self):
"""
@@ -47,134 +48,153 @@ def test_rule_variable_decorator_internals(self):
:return:
"""
- def some_test_function(self): pass
+ def some_test_function(self):
+ pass
- wrapper = rule_variable(StringType, 'Foo Name', options=['op1', 'op2'])
+ wrapper = rule_variable(StringType, "Foo Name", options=["op1", "op2"])
func = wrapper(some_test_function)
self.assertTrue(func.is_rule_variable)
- self.assertEqual(func.label, 'Foo Name')
+ self.assertEqual(func.label, "Foo Name")
self.assertEqual(func.field_type, StringType)
- self.assertEqual(func.options, ['op1', 'op2'])
+ self.assertEqual(func.options, ["op1", "op2"])
def test_rule_variable_works_as_decorator(self):
- @rule_variable(StringType, 'Blah')
- def some_test_function(self): pass
+ @rule_variable(StringType, "Blah")
+ def some_test_function(self):
+ pass
self.assertTrue(some_test_function.is_rule_variable)
def test_rule_variable_decorator_auto_fills_label(self):
@rule_variable(StringType)
- def some_test_function(self): pass
+ def some_test_function(self):
+ pass
- self.assertTrue(some_test_function.label, 'Some Test Function')
+ self.assertTrue(some_test_function.label, "Some Test Function")
- ###
- ### rule_variable wrappers for each variable type
- ###
+ #
+ # rule_variable wrappers for each variable type
+ #
def test_rule_variable_function_with_parameter_not_defined(self):
with self.assertRaises(AssertionError):
- @rule_variable(NumericType, params={'parameter_not_defined': 'type'})
- def variable_function(): pass
+
+ @rule_variable(NumericType, params={"parameter_not_defined": "type"})
+ def variable_function():
+ pass
def test_rule_variable_function_with_parameter_invalid_type(self):
with self.assertRaises(AssertionError):
- @rule_variable(NumericType, params={'parameter': 'invalid_type'})
- def variable_function(parameter): pass
+
+ @rule_variable(NumericType, params={"parameter": "invalid_type"})
+ def variable_function(parameter):
+ pass
def test_numeric_rule_variable(self):
- @numeric_rule_variable('My Label')
- def numeric_var(): pass
+ @numeric_rule_variable("My Label")
+ def numeric_var():
+ pass
- self.assertTrue(getattr(numeric_var, 'is_rule_variable'))
- self.assertEqual(getattr(numeric_var, 'field_type'), NumericType)
- self.assertEqual(getattr(numeric_var, 'label'), 'My Label')
+ self.assertTrue(numeric_var.is_rule_variable)
+ self.assertEqual(numeric_var.field_type, NumericType)
+ self.assertEqual(numeric_var.label, "My Label")
def test_numeric_rule_variable_no_parens(self):
@numeric_rule_variable
- def numeric_var(): pass
+ def numeric_var():
+ pass
- self.assertTrue(getattr(numeric_var, 'is_rule_variable'))
- self.assertEqual(getattr(numeric_var, 'field_type'), NumericType)
+ self.assertTrue(numeric_var.is_rule_variable)
+ self.assertEqual(numeric_var.field_type, NumericType)
def test_string_rule_variable(self):
- @string_rule_variable(label='My Label')
- def string_var(): pass
+ @string_rule_variable(label="My Label")
+ def string_var():
+ pass
- self.assertTrue(getattr(string_var, 'is_rule_variable'))
- self.assertEqual(getattr(string_var, 'field_type'), StringType)
- self.assertEqual(getattr(string_var, 'label'), 'My Label')
+ self.assertTrue(string_var.is_rule_variable)
+ self.assertEqual(string_var.field_type, StringType)
+ self.assertEqual(string_var.label, "My Label")
def test_string_rule_variable_no_parens(self):
@string_rule_variable
- def string_var(): pass
+ def string_var():
+ pass
- self.assertTrue(getattr(string_var, 'is_rule_variable'))
- self.assertEqual(getattr(string_var, 'field_type'), StringType)
+ self.assertTrue(string_var.is_rule_variable)
+ self.assertEqual(string_var.field_type, StringType)
def test_boolean_rule_variable(self):
- @boolean_rule_variable(label='My Label')
- def boolean_var(): pass
+ @boolean_rule_variable(label="My Label")
+ def boolean_var():
+ pass
- self.assertTrue(getattr(boolean_var, 'is_rule_variable'))
- self.assertEqual(getattr(boolean_var, 'field_type'), BooleanType)
- self.assertEqual(getattr(boolean_var, 'label'), 'My Label')
+ self.assertTrue(boolean_var.is_rule_variable)
+ self.assertEqual(boolean_var.field_type, BooleanType)
+ self.assertEqual(boolean_var.label, "My Label")
def test_boolean_rule_variable_no_parens(self):
@boolean_rule_variable
- def boolean_var(): pass
+ def boolean_var():
+ pass
- self.assertTrue(getattr(boolean_var, 'is_rule_variable'))
- self.assertEqual(getattr(boolean_var, 'field_type'), BooleanType)
+ self.assertTrue(boolean_var.is_rule_variable)
+ self.assertEqual(boolean_var.field_type, BooleanType)
def test_select_rule_variable(self):
- options = {'foo': 'bar'}
+ options = {"foo": "bar"}
@select_rule_variable(options=options)
- def select_var(): pass
+ def select_var():
+ pass
- self.assertTrue(getattr(select_var, 'is_rule_variable'))
- self.assertEqual(getattr(select_var, 'field_type'), SelectType)
- self.assertEqual(getattr(select_var, 'options'), options)
+ self.assertTrue(select_var.is_rule_variable)
+ self.assertEqual(select_var.field_type, SelectType)
+ self.assertEqual(select_var.options, options)
def test_select_multiple_rule_variable(self):
- options = {'foo': 'bar'}
+ options = {"foo": "bar"}
@select_multiple_rule_variable(options=options)
- def select_multiple_var(): pass
+ def select_multiple_var():
+ pass
- self.assertTrue(getattr(select_multiple_var, 'is_rule_variable'))
- self.assertEqual(getattr(select_multiple_var, 'field_type'), SelectMultipleType)
- self.assertEqual(getattr(select_multiple_var, 'options'), options)
+ self.assertTrue(select_multiple_var.is_rule_variable)
+ self.assertEqual(select_multiple_var.field_type, SelectMultipleType)
+ self.assertEqual(select_multiple_var.options, options)
def test_datetime_variable(self):
@datetime_rule_variable()
- def datetime_variable(): pass
+ def datetime_variable():
+ pass
- self.assertTrue(getattr(datetime_variable, 'is_rule_variable'))
- self.assertEqual(getattr(datetime_variable, 'field_type'), DateTimeType)
- self.assertEqual(getattr(datetime_variable, 'label'), 'Datetime Variable')
+ self.assertTrue(datetime_variable.is_rule_variable)
+ self.assertEqual(datetime_variable.field_type, DateTimeType)
+ self.assertEqual(datetime_variable.label, "Datetime Variable")
def test_datetime_variable_with_label(self):
- @datetime_rule_variable(label='Custom Label')
- def datetime_variable(): pass
+ @datetime_rule_variable(label="Custom Label")
+ def datetime_variable():
+ pass
- self.assertTrue(getattr(datetime_variable, 'is_rule_variable'))
- self.assertEqual(getattr(datetime_variable, 'field_type'), DateTimeType)
- self.assertEqual(getattr(datetime_variable, 'label'), 'Custom Label')
+ self.assertTrue(datetime_variable.is_rule_variable)
+ self.assertEqual(datetime_variable.field_type, DateTimeType)
+ self.assertEqual(datetime_variable.label, "Custom Label")
def test_time_variable(self):
@time_rule_variable()
- def time_variable(): pass
+ def time_variable():
+ pass
- self.assertTrue(getattr(time_variable, 'is_rule_variable'))
- self.assertEqual(getattr(time_variable, 'field_type'), TimeType)
- self.assertEqual(getattr(time_variable, 'label'), 'Time Variable')
+ self.assertTrue(time_variable.is_rule_variable)
+ self.assertEqual(time_variable.field_type, TimeType)
+ self.assertEqual(time_variable.label, "Time Variable")
def test_time_variable_with_label(self):
- @time_rule_variable(label='Custom Label')
- def datetime_variable(): pass
+ @time_rule_variable(label="Custom Label")
+ def datetime_variable():
+ pass
- self.assertTrue(getattr(datetime_variable, 'is_rule_variable'))
- self.assertEqual(getattr(datetime_variable, 'field_type'), TimeType)
- self.assertEqual(getattr(datetime_variable, 'label'), 'Custom Label')
+ self.assertTrue(datetime_variable.is_rule_variable)
+ self.assertEqual(datetime_variable.field_type, TimeType)
+ self.assertEqual(datetime_variable.label, "Custom Label")
diff --git a/tests/test_variables_class.py b/tests/test_variables_class.py
index e0538a39..4d326467 100644
--- a/tests/test_variables_class.py
+++ b/tests/test_variables_class.py
@@ -1,18 +1,17 @@
-from __future__ import absolute_import
+from unittest import TestCase
+
from business_rules.operators import StringType
from business_rules.variables import BaseVariables, rule_variable
-from . import TestCase
class VariablesClassTests(TestCase):
- """ Test methods on classes that inherit from BaseVariables
- """
+ """Test methods on classes that inherit from BaseVariables"""
def test_base_has_no_variables(self):
self.assertEqual(len(BaseVariables.get_all_variables()), 0)
def test_get_all_variables(self):
- """ Returns a dictionary listing all the functions on the class that
+ """Returns a dictionary listing all the functions on the class that
have been decorated as variables, with some of the data about them.
"""
@@ -26,10 +25,10 @@ def non_rule(self):
vars = SomeVariables.get_all_variables()
self.assertEqual(len(vars), 1)
- self.assertEqual(vars[0]['name'], 'this_is_rule_1')
- self.assertEqual(vars[0]['label'], 'This Is Rule 1')
- self.assertEqual(vars[0]['field_type'], 'string')
- self.assertEqual(vars[0]['options'], [])
+ self.assertEqual(vars[0]["name"], "this_is_rule_1")
+ self.assertEqual(vars[0]["label"], "This Is Rule 1")
+ self.assertEqual(vars[0]["field_type"], "string")
+ self.assertEqual(vars[0]["options"], [])
# should work on an instance of the class too
self.assertEqual(len(SomeVariables().get_all_variables()), 1)
diff --git a/tests/variables.py b/tests/variables.py
index 830ff90d..b25a7e9c 100644
--- a/tests/variables.py
+++ b/tests/variables.py
@@ -1,9 +1,9 @@
-from __future__ import absolute_import
+from datetime import datetime
+
from business_rules import variables
class TestVariables(variables.BaseVariables):
-
@variables.boolean_rule_variable()
def bool_variable(self):
return True
@@ -26,7 +26,7 @@ def datetime_variable(self):
@variables.time_rule_variable()
def time_variable(self):
- return time.time()
+ return datetime.time()
@variables.select_rule_variable()
def select_variable(self):
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 00000000..a79479d8
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,34 @@
+# Tox (https://tox.readthedocs.io/) is a tool for running tests in multiple virtualenvs. This configuration file helps
+# to run the test suite against different combinations of libraries and Python versions.
+# To use it locally, "pip install tox" and then run "tox --skip-missing-interpreters" from this directory.
+
+[tox]
+isolated_build = true
+minversion = 3.24.3
+envlist = py{37,38,39,310,311,312}
+
+[gh-actions]
+# Mapping of Python versions (MAJOR.MINOR) to Tox factors.
+# When running Tox inside GitHub Actions, the `tox-gh-actions` plugin automatically:
+# 1. Identifies the Python version used to run Tox.
+# 2. Determines the corresponding Tox factor for that Python version, based on the `python` mapping below.
+# 3. Narrows down the Tox `envlist` to environments that match the factor.
+# For more details, please see the `tox-gh-actions` README [0] and architecture documentation [1].
+# [0] https://github.com/ymyzk/tox-gh-actions/tree/v2.8.1
+# [1] https://github.com/ymyzk/tox-gh-actions/blob/v2.8.1/ARCHITECTURE.md
+python =
+ 3.7: py37
+ 3.8: py38
+ 3.9: py39
+ 3.10: py310
+ 3.11: py311
+ 3.12: py312
+
+[testenv]
+allowlist_externals =
+ pytest
+usedevelop = true
+extras =
+ test
+commands =
+ pytest -vv {posargs}