├── .circleci
└── config.yml
├── .github
└── workflows
│ └── python-publish.yml
├── .gitignore
├── .idea
├── .gitignore
├── inspectionProfiles
│ └── profiles_settings.xml
├── misc.xml
├── modules.xml
├── rSettings.xml
├── simple-rule-engine.iml
├── sonarlint
│ └── issuestore
│ │ ├── 0
│ │ ├── 4
│ │ │ └── 042c27a354884f09f5d31c0a95db82840062e510
│ │ └── 5
│ │ │ └── 0520c6701ebb2c77c5873b7be5de4e4aa2ff3e4a
│ │ ├── 2
│ │ └── 7
│ │ │ └── 27e55c3a20eaf49770d24e0aad4ae77fa7deea0e
│ │ ├── 3
│ │ └── b
│ │ │ └── 3b4054f3fd9f380ca1c0e3e8003c5e2e8d061c32
│ │ ├── 4
│ │ └── 7
│ │ │ └── 472ccd2fa68dc3a15a12426e0ca69978a67c073e
│ │ ├── 5
│ │ └── c
│ │ │ └── 5c7f15cacdc734f0a3c5ed298c617621e4b1e4c6
│ │ ├── 6
│ │ ├── 6
│ │ │ └── 666862c5aadc8587d36c32dc0d4d9420f3d5b317
│ │ └── c
│ │ │ └── 6c18f1381f6457e97d0ca451118b306e42df8cce
│ │ ├── 7
│ │ ├── 9
│ │ │ ├── 7910974b1389e7aa5121215b4b7103607f50e0f7
│ │ │ └── 7955bbca8c43a10e7c98209d9c1edb83bae53c92
│ │ └── a
│ │ │ └── 7a786759d093a7b2ca926c0eb65d353e4cd321da
│ │ ├── 8
│ │ └── 3
│ │ │ └── 835cbf19c78f3006b133045087a1275ce43f341e
│ │ ├── b
│ │ └── d
│ │ │ └── bddb10333a503cdc3bb3eb44b7868b22fe4647b1
│ │ ├── c
│ │ └── 2
│ │ │ └── c23b4c4e1f696bd943620e1daecad4e09efc44a8
│ │ └── index.pb
└── vcs.xml
├── LICENSE
├── README.md
├── environment.yml
├── images
├── decision_rule.png
└── score_rule.png
├── pyproject.toml
├── pytest.ini
├── simpleruleengine.png
└── simpleruleengine
├── __init__.py
├── __pycache__
└── __init__.cpython-37.pyc
├── conditional
├── __init__.py
├── conditional.py
├── when_all.py
└── when_any.py
├── exception
├── __init__.py
└── rule_row_exceptions.py
├── expression
├── __init__.py
├── expression.py
└── expression_builder.py
├── operator
├── __init__.py
├── __pycache__
│ ├── Operator.cpython-37.pyc
│ └── __init__.cpython-37.pyc
├── between.py
├── boolean_operator.py
├── equal.py
├── greater_than.py
├── greater_than_equal.py
├── less_than.py
├── less_than_equal.py
├── not_equal.py
├── numeric_operator.py
├── operator.py
├── string_in.py
├── string_not_in.py
└── string_operator.py
├── rule
├── __init__.py
├── rule.py
├── rule_decision.py
├── rule_score.py
└── schema
│ └── decision_rule_row_schema.json
├── rulerow
├── __init__.py
├── rule_row_decision.py
└── rule_row_score.py
├── ruleset
├── __init__.py
├── rule_set_decision.py
└── rule_set_score.py
├── test_expression.py
├── test_expression_builder.py
├── test_operator.py
├── test_rule_decision.py
├── test_rule_row_decision.py
├── test_rule_row_score.py
├── test_rule_score.py
├── test_rule_set_decision.py
├── test_rule_set_score.py
├── test_when_all.py
├── test_when_any.py
├── token
├── __init__.py
├── __pycache__
│ ├── __init__.cpython-37.pyc
│ └── test_NumericToken.cpython-37-pytest-6.1.1.pyc
├── boolean_token.py
├── numeric_token.py
├── rule_token.py
├── string_token.py
└── token.py
└── utils
├── __init__.py
├── __pycache__
├── __init__.cpython-37.pyc
└── type_util.cpython-37.pyc
└── type_util.py
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: '2.1'
2 | orbs:
3 | python: circleci/python@1.0.0
4 | jobs:
5 | build:
6 | executor: python/default
7 | steps:
8 | - checkout
9 | - python/install-packages:
10 | args: pytest
11 | pkg-manager: pipenv
12 | - run:
13 | command: |
14 | pipenv run pytest .
15 | name: Test it
16 | workflows:
17 | main:
18 | jobs:
19 | - build
20 |
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will upload a Python Package using Twine when a release is created
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3 |
4 | # This workflow uses actions that are not certified by GitHub.
5 | # They are provided by a third-party and are governed by
6 | # separate terms of service, privacy policy, and support
7 | # documentation.
8 |
9 | name: Upload Python Package
10 |
11 | on:
12 | release:
13 | types: [published]
14 |
15 | permissions:
16 | contents: read
17 |
18 | jobs:
19 | deploy:
20 |
21 | runs-on: ubuntu-latest
22 |
23 | steps:
24 | - uses: actions/checkout@v3
25 | - name: Set up Python
26 | uses: actions/setup-python@v3
27 | with:
28 | python-version: '3.x'
29 | - name: Install dependencies
30 | run: |
31 | python -m pip install --upgrade pip
32 | pip install build
33 | - name: Build package
34 | run: python -m build
35 | - name: Publish package
36 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
37 | with:
38 | user: __token__
39 | password: ${{ secrets.PYPI_API_TOKEN }}
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | .idea
132 | .idea/
133 | .pytest_cache
134 | __pycache__
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/rSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/simple-rule-engine.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/sonarlint/issuestore/0/4/042c27a354884f09f5d31c0a95db82840062e510:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/.idea/sonarlint/issuestore/0/4/042c27a354884f09f5d31c0a95db82840062e510
--------------------------------------------------------------------------------
/.idea/sonarlint/issuestore/0/5/0520c6701ebb2c77c5873b7be5de4e4aa2ff3e4a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/.idea/sonarlint/issuestore/0/5/0520c6701ebb2c77c5873b7be5de4e4aa2ff3e4a
--------------------------------------------------------------------------------
/.idea/sonarlint/issuestore/2/7/27e55c3a20eaf49770d24e0aad4ae77fa7deea0e:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/.idea/sonarlint/issuestore/2/7/27e55c3a20eaf49770d24e0aad4ae77fa7deea0e
--------------------------------------------------------------------------------
/.idea/sonarlint/issuestore/3/b/3b4054f3fd9f380ca1c0e3e8003c5e2e8d061c32:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/.idea/sonarlint/issuestore/3/b/3b4054f3fd9f380ca1c0e3e8003c5e2e8d061c32
--------------------------------------------------------------------------------
/.idea/sonarlint/issuestore/4/7/472ccd2fa68dc3a15a12426e0ca69978a67c073e:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/.idea/sonarlint/issuestore/4/7/472ccd2fa68dc3a15a12426e0ca69978a67c073e
--------------------------------------------------------------------------------
/.idea/sonarlint/issuestore/5/c/5c7f15cacdc734f0a3c5ed298c617621e4b1e4c6:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/.idea/sonarlint/issuestore/5/c/5c7f15cacdc734f0a3c5ed298c617621e4b1e4c6
--------------------------------------------------------------------------------
/.idea/sonarlint/issuestore/6/6/666862c5aadc8587d36c32dc0d4d9420f3d5b317:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/.idea/sonarlint/issuestore/6/6/666862c5aadc8587d36c32dc0d4d9420f3d5b317
--------------------------------------------------------------------------------
/.idea/sonarlint/issuestore/6/c/6c18f1381f6457e97d0ca451118b306e42df8cce:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/.idea/sonarlint/issuestore/6/c/6c18f1381f6457e97d0ca451118b306e42df8cce
--------------------------------------------------------------------------------
/.idea/sonarlint/issuestore/7/9/7910974b1389e7aa5121215b4b7103607f50e0f7:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/.idea/sonarlint/issuestore/7/9/7910974b1389e7aa5121215b4b7103607f50e0f7
--------------------------------------------------------------------------------
/.idea/sonarlint/issuestore/7/9/7955bbca8c43a10e7c98209d9c1edb83bae53c92:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/.idea/sonarlint/issuestore/7/9/7955bbca8c43a10e7c98209d9c1edb83bae53c92
--------------------------------------------------------------------------------
/.idea/sonarlint/issuestore/7/a/7a786759d093a7b2ca926c0eb65d353e4cd321da:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/.idea/sonarlint/issuestore/7/a/7a786759d093a7b2ca926c0eb65d353e4cd321da
--------------------------------------------------------------------------------
/.idea/sonarlint/issuestore/8/3/835cbf19c78f3006b133045087a1275ce43f341e:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/.idea/sonarlint/issuestore/8/3/835cbf19c78f3006b133045087a1275ce43f341e
--------------------------------------------------------------------------------
/.idea/sonarlint/issuestore/b/d/bddb10333a503cdc3bb3eb44b7868b22fe4647b1:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/.idea/sonarlint/issuestore/b/d/bddb10333a503cdc3bb3eb44b7868b22fe4647b1
--------------------------------------------------------------------------------
/.idea/sonarlint/issuestore/c/2/c23b4c4e1f696bd943620e1daecad4e09efc44a8:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/.idea/sonarlint/issuestore/c/2/c23b4c4e1f696bd943620e1daecad4e09efc44a8
--------------------------------------------------------------------------------
/.idea/sonarlint/issuestore/index.pb:
--------------------------------------------------------------------------------
1 |
2 | [
3 | +simpleruleengine/conditional/Conditional.py,4\7\472ccd2fa68dc3a15a12426e0ca69978a67c073e
4 | L
5 | simpleruleengine/__init__.py,6\6\666862c5aadc8587d36c32dc0d4d9420f3d5b317
6 | Q
7 | !simpleruleengine/rule/__init__.py,b\d\bddb10333a503cdc3bb3eb44b7868b22fe4647b1
8 | j
9 | :simpleruleengine/rule/schema/decision_rule_row_schema.json,7\a\7a786759d093a7b2ca926c0eb65d353e4cd321da
10 | T
11 | $simpleruleengine/rulerow/__init__.py,c\2\c23b4c4e1f696bd943620e1daecad4e09efc44a8
12 | T
13 | $simpleruleengine/ruleset/__init__.py,0\5\0520c6701ebb2c77c5873b7be5de4e4aa2ff3e4a
14 | R
15 | "simpleruleengine/token/__init__.py,0\4\042c27a354884f09f5d31c0a95db82840062e510
16 | X
17 | (simpleruleengine/conditional/__init__.py,6\c\6c18f1381f6457e97d0ca451118b306e42df8cce
18 | U
19 | %simpleruleengine/operator/__init__.py,7\9\7910974b1389e7aa5121215b4b7103607f50e0f7
20 | U
21 | %simpleruleengine/operator/Operator.py,3\b\3b4054f3fd9f380ca1c0e3e8003c5e2e8d061c32
22 | R
23 | "simpleruleengine/utils/__init__.py,2\7\27e55c3a20eaf49770d24e0aad4ae77fa7deea0e
24 | S
25 | #simpleruleengine/utils/type_util.py,7\9\7955bbca8c43a10e7c98209d9c1edb83bae53c92
26 | O
27 | simpleruleengine/token/Token.py,5\c\5c7f15cacdc734f0a3c5ed298c617621e4b1e4c6
28 | T
29 | $simpleruleengine/operator/Between.py,8\3\835cbf19c78f3006b133045087a1275ce43f341e
30 | a
31 | 1simpleruleengine/exception/rule_row_exceptions.py,5\2\5264cc052c95e6a26c0694a8b2e0e0b13b1f63bc
32 | 9
33 | README.md,8\e\8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d
34 | :
35 |
36 | .gitignore,a\5\a5cc2925ca8258af241be7e5b0381edf30266302
37 | [
38 | +simpleruleengine/conditional/conditional.py,2\3\23582ae632f092ef6cee1e574c5fbabd6f9eab38
39 | X
40 | (simpleruleengine/conditional/when_all.py,3\c\3c92f490defacece37bec333a887f706afcb3eb9
41 | X
42 | (simpleruleengine/conditional/when_any.py,2\8\28e713e01949f0c35cb9c442a9cf370953c00998
43 | Q
44 | !simpleruleengine/test_when_any.py,e\4\e49c44e6dbdea3537ca51cddb0b539691539e62b
45 | S
46 | #simpleruleengine/rule/rule_score.py,4\c\4ca98e1956769e9de25de4dd213f2b70b974117a
47 | M
48 | simpleruleengine/rule/rule.py,5\6\56d85ae57dbcbb4972b9d2a9bfd4c35634ad3c40
49 | Q
50 | !simpleruleengine/test_when_all.py,f\5\f57bbfa42f8d3e26510537246ddca4df90227d3d
51 | Y
52 | )simpleruleengine/expression/expression.py,9\6\96190bc2f64efd057f68af5bc7da678cbaeff09e
53 | S
54 | #simpleruleengine/test_expression.py,5\0\50a73d4fc3aa859d6ca62cf20a940fa08e856d9f
55 | ?
56 | environment.yml,5\4\54c391de2500e0856c23737daf64f9d931569110
57 | 7
58 | LICENSE,0\3\0398ccd0f49298b10a3d76a47800d2ebecd49859
59 | >
60 | pyproject.toml,5\d\5d07e7d72637aa0d59c89d381fe6dc4cf46e2491
61 | ]
62 | -simpleruleengine/operator/numeric_operator.py,0\c\0c05477c8940cf96863f8d1c88a64d63abe63a04
63 | V
64 | &simpleruleengine/operator/string_in.py,b\e\be1cfab07c9e333c3cbc770902d83e1a1e31f83e
65 | \
66 | ,simpleruleengine/operator/string_operator.py,3\9\39d587a08ea27d6466f59fb98090ad5a75166ec2
67 | Q
68 | !simpleruleengine/test_operator.py,e\0\e0d85c3444279646f82844e7349798a206a33f1e
69 | ]
70 | -simpleruleengine/rulerow/rule_row_decision.py,9\d\9d60a41b363e3e09ae421606329be7e89d5f62be
71 | Z
72 | *simpleruleengine/test_rule_row_decision.py,4\5\4589a6e32b6d000ec5dd2248d065d354b7cb92da
73 | W
74 | 'simpleruleengine/test_rule_row_score.py,6\9\696414d458f8597924237ca4d25484c3a310744f
75 | Z
76 | *simpleruleengine/operator/string_not_in.py,5\e\5eea562ed10becdaf8491dd4d7f94b9f463100a0
77 | Z
78 | *simpleruleengine/rulerow/rule_row_score.py,9\0\90c58de0739037c2ee8cd376128fc77b6bfcb5ad
79 | Z
80 | *simpleruleengine/ruleset/rule_set_score.py,e\e\eeb408e2c3ddbc3b290fd813668c29bf42a6d0d9
81 | W
82 | 'simpleruleengine/test_rule_set_score.py,e\f\efc2f901fdb4116cc971443e0220b24b9a896e9a
83 | Z
84 | *simpleruleengine/test_rule_set_decision.py,3\1\3103a05717caf62199057652f3dcb13f13f5968b
85 | V
86 | &simpleruleengine/test_rule_decision.py,2\7\270196b74ec552fd9af8b8958e0f6af08ed729e5
87 | ]
88 | -simpleruleengine/ruleset/rule_set_decision.py,9\f\9f2ad8aec57599c98d04dbd15bd3c030b1dc4af7
89 | V
90 | &simpleruleengine/rule/rule_decision.py,2\7\27c94171124c84bc44bfd92f522977fc68514ea1
91 | S
92 | #simpleruleengine/test_rule_score.py,e\0\e00586a75f56a508c94115c90b82bfbde96ef830
93 | T
94 | $simpleruleengine/operator/between.py,7\e\7ed9416478f2aeb7690293f5b1458c09a7d32cd4
95 | R
96 | "simpleruleengine/operator/equal.py,3\e\3ea3d633854b5ac086c02b65824e0304ea5e5257
97 | W
98 | 'simpleruleengine/token/numeric_token.py,e\9\e9932b18da2bacaa58fd13572086703eb21aa87e
99 | V
100 | &simpleruleengine/token/string_token.py,d\3\d355fe6a40c02f6a55c052337d5609edaa6cc05e
101 | O
102 | simpleruleengine/token/token.py,e\9\e9256bd9e649180edb3b54f891b4d09a7679a062
103 | Y
104 | )simpleruleengine/operator/greater_than.py,d\0\d0bd557c7d3f9fc456e2e153a219f4ad348911ee
105 | U
106 | %simpleruleengine/operator/operator.py,2\3\23c10acded4a9d4316ab0d5a541daaa586067518
107 | ]
108 | -simpleruleengine/operator/boolean_operator.py,2\9\295f20e2d9cfccd4c831595d890f198fbd4bb8c5
109 | W
110 | 'simpleruleengine/token/boolean_token.py,1\b\1bbd57d33ff73f91d07fe56718598efa1403e8db
111 | [
112 | +simpleruleengine/test_expression_builder.py,6\d\6dff60e5d74aee1ebd7bf20bf8dc6fe20457b13a
113 | a
114 | 1simpleruleengine/expression/expression_builder.py,c\2\c2df3992af5e99d675b4e87abaabd900a65c002c
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2018 The Python Packaging Authority
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # simple-rule-engine
2 |
3 | A __lightweight__ yet __powerful__ rule engine that allows declarative specification of business rules and **saves tons of repeated development work**.
4 |
5 | - This library has been utilized in authoring & evaluation of a number of complex credit decisioning, upgrade/downgrade, and lender evaulation criteria rules at [FundsCorner](https://medium.com/fundscornertech)
6 | - This library can also be considered as a _Policy Framework_ for validating IaC (Infrastructure as Code).
7 |
8 | [](https://www.codefactor.io/repository/github/jeyabalajis/simple-rule-engine)
9 | [](https://dl.circleci.com/status-badge/redirect/gh/jeyabalajis/simple-rule-engine/tree/main)
10 |
11 | ## At a glance
12 |
13 | simple-rule-engine is a Python library that enables declarative specification of decision or scoring rules.
14 |
15 | ### Example Decision matrix
16 |
17 | | Bureau Score | Marital Status | Decision
18 | | :----------: | :----------------: | --------:|
19 | | between 650 and 800 | in [Married, Unspecified] | GO |
20 |
21 | ### Rule specification
22 |
23 | ```python
24 | from simpleruleengine.conditional.when_all import WhenAll
25 | from simpleruleengine.expression.expression import Expression
26 | from simpleruleengine.operator.between import Between
27 | from simpleruleengine.operator.string_in import In
28 | from simpleruleengine.rulerow.rule_row_decision import RuleRowDecision
29 | from simpleruleengine.ruleset.rule_set_decision import RuleSetDecision
30 | from simpleruleengine.token.numeric_token import NumericToken
31 | from simpleruleengine.token.string_token import StringToken
32 |
33 | if __name__ == "__main__":
34 | cibil_score_between_650_800 = Expression(
35 | NumericToken("cibil_score"),
36 | Between(floor=650, ceiling=800)
37 | )
38 | marital_status_in_married_unspecified = Expression(
39 | StringToken("marital_status"),
40 | In("Married", "Unspecified")
41 | )
42 |
43 | rule_row_decision_go = RuleRowDecision(
44 | WhenAll(
45 | cibil_score_between_650_800,
46 | marital_status_in_married_unspecified
47 | ),
48 | "GO"
49 | )
50 | rule_set_decision = RuleSetDecision(rule_row_decision_go)
51 |
52 | fact = dict(
53 | cibil_score=700,
54 | marital_status="Married"
55 | )
56 | assert rule_set_decision.evaluate(fact) == "GO"
57 | ```
58 |
59 | ## Key Features
60 | 1. Ability to __declaratively__ author both Scoring and Decision Rules.
61 | 2. The library offers composable functional syntax that can be extended with various format adapters. See [here](https://github.com/jeyabalajis/simple-serverless-rule-engine) for an example of such an extension.
62 | 2. Ability to __version control__ rule declarations thus enabling auditing of rule changes over a period of time.
63 | 3. Ability to author **_chained rules_**. Evaluation of one rule can refer to the result of another rule, thus enabling
64 | modular, hierarchical rules.
65 |
66 | ## Installation
67 |
68 | [pypi repository](https://pypi.org/project/simpleruleengine/)
69 |
70 | ```commandline
71 | pip install simpleruleengine==2.0.3
72 | ```
73 |
74 | ## Source Code
75 |
76 | [simple-rule-engine GitHub Repository](https://github.com/jeyabalajis/simple-rule-engine)
77 |
78 | # Simple Rule Engine - Motivation and Under-the-Hood
79 |
80 | https://jeyabalajis.gitbook.io/simple-rule-engine/
81 |
--------------------------------------------------------------------------------
/environment.yml:
--------------------------------------------------------------------------------
1 | name: simple-rule-engine
2 | channels:
3 | - conda-forge
4 | - defaults
5 | dependencies:
6 | - atomicwrites=1.4.0=py_0
7 | - attrs=20.3.0=pyhd3eb1b0_0
8 | - ca-certificates=2022.9.14=h5b45459_0
9 | - certifi=2022.9.14=pyhd8ed1ab_0
10 | - colorama=0.4.4=py_0
11 | - importlib-metadata=2.0.0=py_1
12 | - importlib_metadata=2.0.0=1
13 | - iniconfig=1.1.1=py_0
14 | - more-itertools=8.5.0=py_0
15 | - openssl=1.1.1l=h8ffe710_0
16 | - packaging=20.4=py_0
17 | - pluggy=0.13.1=py37_0
18 | - py=1.9.0=py_0
19 | - pyparsing=2.4.7=py_0
20 | - pytest=6.1.1=py37_0
21 | - python=3.7.9=h60c2a47_0
22 | - python-fastjsonschema=2.16.2=pyhd8ed1ab_0
23 | - python_abi=3.7=1_cp37m
24 | - setuptools=50.3.1=py37haa95532_1
25 | - six=1.15.0=py_0
26 | - sqlite=3.33.0=h2a8f88b_0
27 | - toml=0.10.1=py_0
28 | - vc=14.1=h0510ff6_4
29 | - vs2015_runtime=14.16.27012=hf0eaf9b_3
30 | - wheel=0.35.1=py_0
31 | - wincertstore=0.2=py37_0
32 | - zipp=3.4.0=pyhd3eb1b0_0
33 | - zlib=1.2.11=h62dcd97_4
34 | - pip:
35 | - pip==22.2.2
36 |
--------------------------------------------------------------------------------
/images/decision_rule.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/images/decision_rule.png
--------------------------------------------------------------------------------
/images/score_rule.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/images/score_rule.png
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=61.0"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "simpleruleengine"
7 | version = "2.0.3"
8 | authors = [
9 | { name="Jeyabalaji Subramanian", email="jeyabalaji.subramanian@gmail.com" },
10 | ]
11 | description = "A lightweight rule engine that allows declarative specification of business rules."
12 | readme = "README.md"
13 | requires-python = ">=3.7"
14 | classifiers = [
15 | "Programming Language :: Python :: 3",
16 | "License :: OSI Approved :: MIT License",
17 | "Operating System :: OS Independent",
18 | ]
19 |
20 | [project.urls]
21 | "Homepage" = "https://github.com/jeyabalajis/simple-rule-engine"
22 | "Bug Tracker" = "https://github.com/jeyabalajis/simple-rule-engine/issues"
23 |
24 | [tool.setuptools]
25 | packages = [
26 | "simpleruleengine",
27 | "simpleruleengine.conditional",
28 | "simpleruleengine.exception",
29 | "simpleruleengine.expression",
30 | "simpleruleengine.operator",
31 | "simpleruleengine.rule",
32 | "simpleruleengine.rulerow",
33 | "simpleruleengine.ruleset",
34 | "simpleruleengine.token",
35 | "simpleruleengine.utils"
36 | ]
37 |
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 |
--------------------------------------------------------------------------------
/simpleruleengine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine.png
--------------------------------------------------------------------------------
/simpleruleengine/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine/__init__.py
--------------------------------------------------------------------------------
/simpleruleengine/__pycache__/__init__.cpython-37.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine/__pycache__/__init__.cpython-37.pyc
--------------------------------------------------------------------------------
/simpleruleengine/conditional/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine/conditional/__init__.py
--------------------------------------------------------------------------------
/simpleruleengine/conditional/conditional.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from typing import Union
3 |
4 | from simpleruleengine.expression.expression import Expression
5 | from simpleruleengine.utils.type_util import is_dict
6 |
7 |
8 | class Conditional(ABC):
9 | """ Conditional is an abstract base class for validating a set of Tokens or Conditionals """
10 |
11 | def __init__(self, *expressions):
12 | self.expressions = expressions
13 |
14 | @abstractmethod
15 | def evaluate(self, token_dict: dict) -> bool:
16 | if not is_dict(token_dict):
17 | raise ValueError("Only dict is allowed for token_dict")
18 |
19 | return True
20 |
21 | def get_token_dict_structure(self) -> dict:
22 | """get_tokens_dict returns a dict of expressions with token_name as key.
23 | This can be used by consumer to fill values before calling evaluate
24 | """
25 | token_dict = {}
26 | for expression in self.expressions:
27 | token_dict_for_expression = expression.token.get_token_dict_structure()
28 | for key, value in token_dict_for_expression.items():
29 | token_dict[key] = value
30 |
31 | return token_dict
32 |
--------------------------------------------------------------------------------
/simpleruleengine/conditional/when_all.py:
--------------------------------------------------------------------------------
1 | from simpleruleengine.conditional.conditional import Conditional
2 | from simpleruleengine.expression.expression import Expression
3 | from typing import Union
4 |
5 |
6 | class WhenAll(Conditional):
7 | def __init__(self, *expressions: Union[Expression, Conditional]):
8 | super().__init__(*expressions)
9 |
10 | def evaluate(self, token_dict: dict) -> bool:
11 | super(WhenAll, self).evaluate(token_dict)
12 | result = True
13 | for expression in self.expressions:
14 | result = result and expression.evaluate(token_dict)
15 | return result
16 |
--------------------------------------------------------------------------------
/simpleruleengine/conditional/when_any.py:
--------------------------------------------------------------------------------
1 | from simpleruleengine.conditional.conditional import Conditional
2 | from simpleruleengine.expression.expression import Expression
3 | from typing import Union
4 |
5 |
6 | class WhenAny(Conditional):
7 | def __init__(self, *expressions: Union[Expression, Conditional]):
8 | super().__init__(*expressions)
9 |
10 | def evaluate(self, token_dict: dict) -> bool:
11 | super(WhenAny, self).evaluate(token_dict)
12 | result = False
13 | for expression in self.expressions:
14 | result = result or expression.evaluate(token_dict)
15 | return result
16 |
--------------------------------------------------------------------------------
/simpleruleengine/exception/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine/exception/__init__.py
--------------------------------------------------------------------------------
/simpleruleengine/exception/rule_row_exceptions.py:
--------------------------------------------------------------------------------
1 | class RuleRowNotEvaluatedException(Exception):
2 | pass
3 |
--------------------------------------------------------------------------------
/simpleruleengine/expression/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine/expression/__init__.py
--------------------------------------------------------------------------------
/simpleruleengine/expression/expression.py:
--------------------------------------------------------------------------------
1 | from simpleruleengine.operator.operator import Operator
2 | from simpleruleengine.token.token import Token
3 |
4 |
5 | class Expression:
6 | def __init__(self, token: Token, operator: Operator):
7 | self.token = token
8 | self.operator = operator
9 |
10 | def evaluate(self, token_dict: dict) -> bool:
11 | return self.operator.evaluate(self.token.get_token_value(token_dict))
12 |
--------------------------------------------------------------------------------
/simpleruleengine/expression/expression_builder.py:
--------------------------------------------------------------------------------
1 | from simpleruleengine.expression.expression import Expression
2 | from simpleruleengine.operator.between import Between
3 | from simpleruleengine.operator.greater_than import Gt
4 | from simpleruleengine.operator.operator import Operator
5 | from simpleruleengine.operator.string_in import In
6 | from simpleruleengine.token.numeric_token import NumericToken
7 | from simpleruleengine.token.string_token import StringToken
8 | from simpleruleengine.token.token import Token
9 |
10 | from functools import wraps
11 |
12 |
13 | def new_object(method):
14 | @wraps(method)
15 | def inner(self, *args, **kwargs):
16 | obj = self.__class__.__new__(self.__class__)
17 | obj.__dict__ = self.__dict__.copy()
18 | method(obj, *args, **kwargs)
19 | return obj
20 |
21 | return inner
22 |
23 |
24 | class ExpressionBuilder:
25 | def __init__(self):
26 | self.token = None
27 | self.operator = None
28 |
29 | @new_object
30 | def numeric_token(self, token_name: str):
31 | self.token = NumericToken(name=token_name)
32 |
33 | @new_object
34 | def string_token(self, token_name: str):
35 | self.token = StringToken(name=token_name)
36 |
37 | @new_object
38 | def greater_than(self, value_to_evaluate):
39 | self.operator = Gt(base_value=value_to_evaluate)
40 |
41 | @new_object
42 | def between(self, floor, ceiling):
43 | self.operator = Between(floor=floor, ceiling=ceiling)
44 |
45 | @new_object
46 | def in_list(self, *base_value):
47 | self.operator = In(*base_value)
48 |
49 | def build(self):
50 | assert isinstance(self.token, Token)
51 | assert isinstance(self.operator, Operator)
52 |
53 | return Expression(token=self.token, operator=self.operator)
54 |
--------------------------------------------------------------------------------
/simpleruleengine/operator/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine/operator/__init__.py
--------------------------------------------------------------------------------
/simpleruleengine/operator/__pycache__/Operator.cpython-37.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine/operator/__pycache__/Operator.cpython-37.pyc
--------------------------------------------------------------------------------
/simpleruleengine/operator/__pycache__/__init__.cpython-37.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine/operator/__pycache__/__init__.cpython-37.pyc
--------------------------------------------------------------------------------
/simpleruleengine/operator/between.py:
--------------------------------------------------------------------------------
1 | from operator import ge
2 | from operator import le
3 |
4 | from simpleruleengine.operator.numeric_operator import NumericOperator
5 |
6 |
7 | class Between(NumericOperator):
8 | def __init__(self, *, floor: float, ceiling: float):
9 | super().__init__(floor)
10 | self.floor = floor
11 | self.ceiling = ceiling
12 |
13 | def evaluate(self, value_to_evaluate):
14 | return ge(value_to_evaluate, self.floor) and le(value_to_evaluate, self.ceiling)
15 |
--------------------------------------------------------------------------------
/simpleruleengine/operator/boolean_operator.py:
--------------------------------------------------------------------------------
1 | from abc import abstractmethod
2 |
3 | from simpleruleengine.operator.operator import Operator
4 |
5 |
6 | class BooleanOperator(Operator):
7 | def __init__(self, base_value: bool):
8 | self.__assert_boolean(base_value)
9 | self._base_value = base_value
10 |
11 | @property
12 | def base_value(self):
13 | return self._base_value
14 |
15 | @base_value.setter
16 | def base_value(self, base_value):
17 | self.__assert_boolean(base_value)
18 | self._base_value = base_value
19 |
20 | def evaluate(self, value_to_evaluate):
21 | return value_to_evaluate is self._base_value
22 |
23 | @classmethod
24 | def __assert_boolean(cls, base_value):
25 | if not type(base_value).__name__ == "bool":
26 | raise ValueError("Only bool type allowed")
27 |
--------------------------------------------------------------------------------
/simpleruleengine/operator/equal.py:
--------------------------------------------------------------------------------
1 | from operator import eq
2 |
3 | from simpleruleengine.operator.numeric_operator import NumericOperator
4 |
5 |
6 | class Eq(NumericOperator):
7 | def __init__(self, base_value):
8 | super().__init__(base_value)
9 |
10 | def evaluate(self, value_to_evaluate):
11 | return eq(value_to_evaluate, self._base_value)
12 |
--------------------------------------------------------------------------------
/simpleruleengine/operator/greater_than.py:
--------------------------------------------------------------------------------
1 | from operator import gt
2 |
3 | from simpleruleengine.operator.numeric_operator import NumericOperator
4 |
5 |
6 | class Gt(NumericOperator):
7 | def __init__(self, base_value):
8 | super().__init__(base_value)
9 |
10 | def evaluate(self, value_to_evaluate):
11 | return gt(value_to_evaluate, self._base_value)
12 |
--------------------------------------------------------------------------------
/simpleruleengine/operator/greater_than_equal.py:
--------------------------------------------------------------------------------
1 | from operator import ge
2 |
3 | from simpleruleengine.operator.numeric_operator import NumericOperator
4 |
5 |
6 | class Gte(NumericOperator):
7 | def __init__(self, base_value):
8 | super().__init__(base_value)
9 |
10 | def evaluate(self, value_to_evaluate):
11 | return ge(value_to_evaluate, self._base_value)
12 |
--------------------------------------------------------------------------------
/simpleruleengine/operator/less_than.py:
--------------------------------------------------------------------------------
1 | from operator import lt
2 |
3 | from simpleruleengine.operator.numeric_operator import NumericOperator
4 |
5 |
6 | class Lt(NumericOperator):
7 | def __init__(self, base_value):
8 | super().__init__(base_value)
9 |
10 | def evaluate(self, value_to_evaluate):
11 | return lt(value_to_evaluate, self._base_value)
12 |
--------------------------------------------------------------------------------
/simpleruleengine/operator/less_than_equal.py:
--------------------------------------------------------------------------------
1 | from operator import le
2 |
3 | from simpleruleengine.operator.numeric_operator import NumericOperator
4 |
5 |
6 | class Lte(NumericOperator):
7 | def __init__(self, base_value):
8 | super().__init__(base_value)
9 |
10 | def evaluate(self, value_to_evaluate):
11 | return le(value_to_evaluate, self._base_value)
12 |
--------------------------------------------------------------------------------
/simpleruleengine/operator/not_equal.py:
--------------------------------------------------------------------------------
1 | from operator import eq
2 |
3 | from simpleruleengine.operator.numeric_operator import NumericOperator
4 |
5 |
6 | class NotEq(NumericOperator):
7 | def __init__(self, base_value):
8 | super().__init__(base_value)
9 |
10 | def evaluate(self, value_to_evaluate):
11 | return False if eq(value_to_evaluate, self._base_value) else True
12 |
--------------------------------------------------------------------------------
/simpleruleengine/operator/numeric_operator.py:
--------------------------------------------------------------------------------
1 | from abc import abstractmethod
2 |
3 | from simpleruleengine.operator.operator import Operator
4 | from simpleruleengine.utils.type_util import numeric
5 |
6 |
7 | class NumericOperator(Operator):
8 | def __init__(self, base_value):
9 | self.__assert_numeric(base_value)
10 | self._base_value = base_value
11 |
12 | @property
13 | def base_value(self):
14 | return self._base_value
15 |
16 | @base_value.setter
17 | def base_value(self, base_value):
18 | self.__assert_numeric(base_value)
19 | self._base_value = base_value
20 |
21 | @classmethod
22 | def __assert_numeric(cls, base_value):
23 | if not numeric(base_value):
24 | raise ValueError("Only Integer and Float allowed")
25 |
26 | @abstractmethod
27 | def evaluate(self, value_to_evaluate):
28 | pass
29 |
--------------------------------------------------------------------------------
/simpleruleengine/operator/operator.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 |
4 | class Operator(ABC):
5 |
6 | @abstractmethod
7 | def evaluate(self, value_to_evaluate):
8 | pass
9 |
--------------------------------------------------------------------------------
/simpleruleengine/operator/string_in.py:
--------------------------------------------------------------------------------
1 | from simpleruleengine.operator.string_operator import StringOperator
2 |
3 |
4 | class In(StringOperator):
5 | def __init__(self, *base_value):
6 | super().__init__(base_value)
7 |
8 | def evaluate(self, value_to_evaluate):
9 | return value_to_evaluate in self.base_value
10 |
--------------------------------------------------------------------------------
/simpleruleengine/operator/string_not_in.py:
--------------------------------------------------------------------------------
1 | from simpleruleengine.operator.string_operator import StringOperator
2 |
3 |
4 | class NotIn(StringOperator):
5 | def __init__(self, *base_value):
6 | super().__init__(base_value)
7 |
8 | def evaluate(self, value_to_evaluate):
9 | return value_to_evaluate not in self.base_value
10 |
--------------------------------------------------------------------------------
/simpleruleengine/operator/string_operator.py:
--------------------------------------------------------------------------------
1 | from abc import abstractmethod
2 |
3 | from simpleruleengine.operator.operator import Operator
4 | from simpleruleengine.utils.type_util import string, string_list
5 |
6 |
7 | class StringOperator(Operator):
8 | def __init__(self, base_value):
9 | self.__assert_string(base_value)
10 | self._base_value = base_value
11 |
12 | @property
13 | def base_value(self):
14 | return self._base_value
15 |
16 | @base_value.setter
17 | def base_value(self, base_value):
18 | self.__assert_string(base_value)
19 | self._base_value = base_value
20 |
21 | @classmethod
22 | def __assert_string(cls, base_value):
23 | if not (string(base_value) or string_list(base_value)):
24 | raise ValueError("Only String or List of String or Tuple of String allowed")
25 |
26 | @abstractmethod
27 | def evaluate(self, value_to_evaluate):
28 | pass
29 |
--------------------------------------------------------------------------------
/simpleruleengine/rule/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine/rule/__init__.py
--------------------------------------------------------------------------------
/simpleruleengine/rule/rule.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from simpleruleengine.utils.type_util import is_dict
3 |
4 |
5 | class Rule(ABC):
6 | def __init__(self, *tokens):
7 | self.rule_sets = tokens
8 |
9 | @abstractmethod
10 | def execute(self, token_dict: dict) -> bool:
11 | if not is_dict(token_dict):
12 | raise ValueError("Only dict is allowed for token_dict")
13 |
14 | return True
15 |
16 | @abstractmethod
17 | def get_token_dict_structure(self) -> dict:
18 | return dict()
19 |
20 |
--------------------------------------------------------------------------------
/simpleruleengine/rule/rule_decision.py:
--------------------------------------------------------------------------------
1 | from simpleruleengine.ruleset.rule_set_decision import RuleSetDecision
2 | from simpleruleengine.rule.rule import Rule
3 | from typing import Any
4 |
5 |
6 | class RuleDecision(Rule):
7 | def __init__(self, *rule_sets: RuleSetDecision):
8 | super().__init__(rule_sets)
9 | self.rule_sets = rule_sets
10 |
11 | def execute(self, token_dict: dict) -> Any:
12 | super(RuleDecision, self).execute(token_dict)
13 | result = None
14 | for rule_set in self.rule_sets:
15 | result = rule_set.evaluate(token_dict=token_dict)
16 |
17 | return result
18 |
19 | def get_token_dict_structure(self) -> dict:
20 | return super(RuleDecision, self).get_token_dict_structure()
21 |
--------------------------------------------------------------------------------
/simpleruleengine/rule/rule_score.py:
--------------------------------------------------------------------------------
1 | from simpleruleengine.ruleset.rule_set_score import RuleSetScore
2 | from typing import List
3 | from simpleruleengine.rule.rule import Rule
4 |
5 |
6 | class RuleScore(Rule):
7 | def __init__(self, *rule_sets: RuleSetScore):
8 | super().__init__(rule_sets)
9 | self.rule_sets = rule_sets
10 |
11 | def execute(self, token_dict: dict) -> float:
12 | super(RuleScore, self).execute(token_dict)
13 | total_score = 0
14 | for rule_set in self.rule_sets:
15 | total_score += rule_set.evaluate(token_dict=token_dict)
16 |
17 | return total_score
18 |
19 | def get_token_dict_structure(self) -> dict:
20 | return super(RuleScore, self).get_token_dict_structure()
21 |
--------------------------------------------------------------------------------
/simpleruleengine/rule/schema/decision_rule_row_schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$rule": "http://json-schema.org/draft-07/schema#",
3 | "type": "object",
4 | "oneOf": [
5 | {
6 | "required": [
7 | "all_of"
8 | ]
9 | },
10 | {
11 | "required": [
12 | "any_of"
13 | ]
14 | }
15 | ],
16 | "properties": {
17 | "any_of": {
18 | "$ref": "#/definitions/conditional"
19 | },
20 | "all_of": {
21 | "$ref": "#/definitions/conditional"
22 | }
23 | },
24 | "definitions": {
25 | "conditional": {
26 | "type": "object",
27 | "required": [
28 | "elements"
29 | ],
30 | "properties": {
31 | "elements": {
32 | "type": "array",
33 | "minItems": 1,
34 | "uniqueItems": true,
35 | "items": {
36 | "additionalProperties": false,
37 | "properties": {
38 | "token": {
39 | "$ref": "#/definitions/token"
40 | },
41 | "any_of": {
42 | "$ref": "#/definitions/conditional"
43 | },
44 | "all_of": {
45 | "$ref": "#/definitions/conditional"
46 | }
47 | },
48 | "oneOf": [
49 | {
50 | "required": [
51 | "token"
52 | ]
53 | },
54 | {
55 | "required": [
56 | "any_of"
57 | ]
58 | },
59 | {
60 | "required": [
61 | "all_of"
62 | ]
63 | }
64 | ]
65 | }
66 | }
67 | }
68 | },
69 | "token": {
70 | "type": "object",
71 | "properties": {
72 | "token_name": {
73 | "type": "string"
74 | },
75 | "operator": {
76 | "$ref": "#/definitions/operator"
77 | }
78 | }
79 | },
80 | "operator": {
81 | "type": "object",
82 | "required": [
83 | "operation",
84 | "operator_type"
85 | ],
86 | "properties": {
87 | "operation": {
88 | "type": "string",
89 | "enum": [
90 | ">=",
91 | "<=",
92 | "=",
93 | "!=",
94 | ">",
95 | "<",
96 | "in",
97 | "not_in"
98 | ]
99 | },
100 | "operator_type": {
101 | "type": "string",
102 | "enum": [
103 | "string",
104 | "numeric"
105 | ]
106 | },
107 | "base_value_string": {
108 | "type": "string"
109 | },
110 | "base_value_array_string": {
111 | "type": "array",
112 | "items": {
113 | "type": "string"
114 | }
115 | },
116 | "base_value_numeric": {
117 | "type": "number"
118 | }
119 | },
120 | "oneOf": [
121 | {
122 | "properties": {
123 | "operator_type": {
124 | "enum": [
125 | "string"
126 | ]
127 | }
128 | },
129 | "required": [
130 | "base_value_string"
131 | ]
132 | },
133 | {
134 | "properties": {
135 | "operator_type": {
136 | "enum": [
137 | "string"
138 | ]
139 | }
140 | },
141 | "required": [
142 | "base_value_array_string"
143 | ]
144 | },
145 | {
146 | "properties": {
147 | "operator_type": {
148 | "enum": [
149 | "numeric"
150 | ]
151 | }
152 | },
153 | "required": [
154 | "base_value_numeric"
155 | ]
156 | }
157 | ]
158 | }
159 | }
160 | }
--------------------------------------------------------------------------------
/simpleruleengine/rulerow/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine/rulerow/__init__.py
--------------------------------------------------------------------------------
/simpleruleengine/rulerow/rule_row_decision.py:
--------------------------------------------------------------------------------
1 | from simpleruleengine.conditional.conditional import Conditional
2 | from simpleruleengine.exception.rule_row_exceptions import RuleRowNotEvaluatedException
3 |
4 |
5 | class RuleRowDecision:
6 | def __init__(self, antecedent: Conditional, consequent: any):
7 | self.__validate_antecedent(antecedent)
8 | self.antecedent: Conditional = antecedent
9 | self.consequent: any = consequent
10 |
11 | def evaluate(self, token_dict: dict) -> any:
12 | if self.antecedent.evaluate(token_dict):
13 | return self.consequent
14 | raise RuleRowNotEvaluatedException
15 |
16 | @classmethod
17 | def __validate_antecedent(cls, antecedent):
18 | if not isinstance(antecedent, Conditional):
19 | raise TypeError("Only Conditional allowed for antecedent")
20 |
--------------------------------------------------------------------------------
/simpleruleengine/rulerow/rule_row_score.py:
--------------------------------------------------------------------------------
1 | from simpleruleengine.conditional.conditional import Conditional
2 | from simpleruleengine.exception.rule_row_exceptions import RuleRowNotEvaluatedException
3 |
4 |
5 | class RuleRowScore:
6 |
7 | def __init__(self, antecedent: Conditional, consequent: float):
8 | self.__validate_antecedent(antecedent)
9 | self.__validate_consequent(consequent)
10 |
11 | self.antecedent: Conditional = antecedent
12 | self.consequent: float = float(consequent)
13 |
14 | def evaluate(self, token_dict: dict) -> float:
15 | if self.antecedent.evaluate(token_dict):
16 | return self.consequent
17 | raise RuleRowNotEvaluatedException
18 |
19 | @classmethod
20 | def __validate_antecedent(cls, antecedent):
21 | if not isinstance(antecedent, Conditional):
22 | raise TypeError("Only Conditional allowed for antecedent")
23 |
24 | @classmethod
25 | def __validate_consequent(cls, consequent):
26 | if not (isinstance(consequent, float) or isinstance(consequent, int)):
27 | raise TypeError("Only int or float allowed for consequent")
28 |
--------------------------------------------------------------------------------
/simpleruleengine/ruleset/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine/ruleset/__init__.py
--------------------------------------------------------------------------------
/simpleruleengine/ruleset/rule_set_decision.py:
--------------------------------------------------------------------------------
1 | from simpleruleengine.rulerow.rule_row_decision import RuleRowDecision
2 | from simpleruleengine.exception.rule_row_exceptions import RuleRowNotEvaluatedException
3 |
4 |
5 | class RuleSetDecision:
6 | NO_DECISION_ROW_EVALUATED = "NO_DECISION_ROW_EVALUATED"
7 |
8 | def __init__(self, *rule_rows: RuleRowDecision):
9 | self.validate_rule_rows_type(rule_rows)
10 | self.rule_rows = rule_rows
11 |
12 | def evaluate(self, token_dict: dict):
13 | for rule_row in self.rule_rows:
14 | try:
15 | _result = rule_row.evaluate(token_dict)
16 | except RuleRowNotEvaluatedException:
17 | continue
18 |
19 | return _result
20 |
21 | return self.NO_DECISION_ROW_EVALUATED
22 |
23 | @classmethod
24 | def validate_rule_rows_type(cls, rule_rows):
25 | for rule_row in rule_rows:
26 | if not isinstance(rule_row, RuleRowDecision):
27 | raise TypeError("Only RuleRowDecision type allowed for rule rows")
28 |
--------------------------------------------------------------------------------
/simpleruleengine/ruleset/rule_set_score.py:
--------------------------------------------------------------------------------
1 | from simpleruleengine.rulerow.rule_row_score import RuleRowScore
2 | from simpleruleengine.exception.rule_row_exceptions import RuleRowNotEvaluatedException
3 |
4 |
5 | class RuleSetScore:
6 | def __init__(self, *rule_rows: RuleRowScore, weight: float):
7 | self.validate_rule_rows_type(rule_rows)
8 | self.validate_weight(weight)
9 | self.rule_rows = rule_rows
10 | self.weight = weight
11 |
12 | def evaluate(self, token_dict: dict):
13 | score = 0
14 | for rule_row in self.rule_rows:
15 | try:
16 | score = rule_row.evaluate(token_dict)
17 | return score * self.weight
18 | except RuleRowNotEvaluatedException:
19 | continue
20 | return score
21 |
22 | @classmethod
23 | def validate_rule_rows_type(cls, rule_rows):
24 | for rule_row in rule_rows:
25 | if not isinstance(rule_row, RuleRowScore):
26 | raise TypeError("Only RuleRowScore type allowed for rule rows")
27 |
28 | @classmethod
29 | def validate_weight(cls, weight):
30 | if not (isinstance(weight, int) or isinstance(weight, float)):
31 | raise TypeError("Only int or float type allowed for weight")
32 |
33 | if float(weight) > 1 or float(weight) < 0:
34 | raise ValueError("weight must be greater than zero and less than 1")
35 |
--------------------------------------------------------------------------------
/simpleruleengine/test_expression.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from simpleruleengine.expression.expression import Expression
4 | from simpleruleengine.operator.greater_than_equal import Gte
5 | from simpleruleengine.operator.boolean_operator import BooleanOperator
6 | from simpleruleengine.operator.string_in import In
7 | from simpleruleengine.token.boolean_token import BooleanToken
8 | from simpleruleengine.token.numeric_token import NumericToken
9 | from simpleruleengine.token.string_token import StringToken
10 | from simpleruleengine.operator.equal import Eq
11 | from simpleruleengine.operator.not_equal import NotEq
12 |
13 |
14 | class TestExpression(TestCase):
15 | def test_evaluate_numeric_token(self):
16 | numeric_token_age = NumericToken(name="age")
17 | age_gte_35 = Expression(numeric_token_age, Gte(35))
18 |
19 | fact = dict(age=40)
20 |
21 | assert age_gte_35.evaluate(token_dict=fact) is True
22 |
23 | assert Expression(numeric_token_age, Eq(35)).evaluate(dict(age=35))
24 | assert Expression(numeric_token_age, NotEq(35)).evaluate(dict(age=40))
25 |
26 | def test_evaluate_string_token(self):
27 | string_token_pet = StringToken(name="pet")
28 | pet_in_dog_cat = Expression(string_token_pet, In("dog", "cat"))
29 |
30 | fact = dict(pet="cat")
31 | assert pet_in_dog_cat.evaluate(token_dict=fact) is True
32 |
33 | fact = dict(pet="parrot")
34 | assert pet_in_dog_cat.evaluate(token_dict=fact) is False
35 |
36 | def test_evaluate_boolean_token_true(self):
37 | boolean_token_big_shot = BooleanToken("big_shot")
38 | big_shot_true = Expression(boolean_token_big_shot, BooleanOperator(True))
39 |
40 | fact = dict(big_shot=True)
41 | assert big_shot_true.evaluate(token_dict=fact) is True
42 |
43 | fact = dict(big_shot=False)
44 | assert big_shot_true.evaluate(token_dict=fact) is False
45 |
46 | def test_evaluate_boolean_token_false(self):
47 | boolean_token_big_shot = BooleanToken("big_shot")
48 | big_shot_true = Expression(boolean_token_big_shot, BooleanOperator(False))
49 |
50 | fact = dict(big_shot=True)
51 | assert big_shot_true.evaluate(token_dict=fact) is False
52 |
53 | fact = dict(big_shot=False)
54 | assert big_shot_true.evaluate(token_dict=fact) is True
55 |
--------------------------------------------------------------------------------
/simpleruleengine/test_expression_builder.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from simpleruleengine.expression.expression_builder import ExpressionBuilder
4 |
5 |
6 | class TestExpressionBuilder(TestCase):
7 | def test_build(self):
8 | numeric_expression = ExpressionBuilder().numeric_token("age").greater_than(40).build()
9 | assert numeric_expression.evaluate(dict(age=45)) is True
10 |
11 | cibil_score_between = ExpressionBuilder().numeric_token("cibil_score").between(650, 800).build()
12 | assert cibil_score_between.evaluate(dict(cibil_score=700)) is True
13 |
14 | marital_status_in = ExpressionBuilder().string_token("marital_status").in_list("Married", "Unspecified").build()
15 | assert marital_status_in.evaluate(dict(marital_status="Bachelor")) is False
16 |
--------------------------------------------------------------------------------
/simpleruleengine/test_operator.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 | from simpleruleengine.operator.between import Between
3 | from simpleruleengine.operator.greater_than_equal import Gte
4 | from simpleruleengine.operator.string_in import In
5 | from simpleruleengine.operator.boolean_operator import BooleanOperator
6 | from simpleruleengine.operator.equal import Eq
7 | from simpleruleengine.operator.not_equal import NotEq
8 |
9 |
10 | class TestOperator(TestCase):
11 | def test_evaluate_between_true(self):
12 | assert Between(floor=650, ceiling=800).evaluate(675)
13 |
14 | def test_evaluate_between_false(self):
15 | assert Between(floor=650, ceiling=800).evaluate(625) is not True
16 |
17 | def test_evaluate_gte_true(self):
18 | assert Gte(650).evaluate(675) is True
19 |
20 | def test_evaluate_gte_false(self):
21 | assert Gte(650).evaluate(649) is False
22 |
23 | def test_evaluate_in_true(self):
24 | assert In("dog", "cat").evaluate("dog") is True
25 |
26 | def test_evaluate_boolean_true(self):
27 | assert BooleanOperator(True).evaluate(True) is True
28 |
29 | def test_evaluate_boolean_false(self):
30 | assert BooleanOperator(False).evaluate(False) is True
31 |
32 | def test_evaluate_equal_true(self):
33 | assert Eq(2).evaluate(3) is False
34 |
35 | def test_evaluate_not_equal_true(self):
36 | assert NotEq(2).evaluate(3) is True
37 | assert NotEq(2.25).evaluate(2.26) is True
38 |
--------------------------------------------------------------------------------
/simpleruleengine/test_rule_decision.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from simpleruleengine.conditional.when_all import WhenAll
4 | from simpleruleengine.conditional.when_any import WhenAny
5 | from simpleruleengine.expression.expression import Expression
6 | from simpleruleengine.operator.between import Between
7 | from simpleruleengine.operator.greater_than import Gt
8 | from simpleruleengine.operator.greater_than_equal import Gte
9 | from simpleruleengine.operator.string_in import In
10 | from simpleruleengine.operator.less_than_equal import Lte
11 | from simpleruleengine.operator.string_not_in import NotIn
12 | from simpleruleengine.rule.rule_decision import RuleDecision
13 | from simpleruleengine.rulerow.rule_row_decision import RuleRowDecision
14 | from simpleruleengine.ruleset.rule_set_decision import RuleSetDecision
15 | from simpleruleengine.token.numeric_token import NumericToken
16 | from simpleruleengine.token.string_token import StringToken
17 |
18 | OWNED_BY_FAMILY = "Owned by Family"
19 |
20 | OWNED_BY_SELF = "Owned by Self"
21 |
22 | OWNED = "Not Owned"
23 |
24 |
25 | class TestRuleDecision(TestCase):
26 | def test_evaluate(self):
27 | age_gt_35 = Expression(NumericToken("age"), Gt(35))
28 | pet_in_dog_cat = Expression(StringToken("pet"), In("dog", "cat"))
29 | rule_row_decision_go = RuleRowDecision(
30 | WhenAll(age_gt_35, pet_in_dog_cat),
31 | "GO"
32 | )
33 |
34 | age_lte_35 = Expression(NumericToken("age"), Lte(35))
35 | pet_not_in_dog_cat = Expression(StringToken("pet"), NotIn("dog", "cat"))
36 | rule_row_decision_no_go = RuleRowDecision(
37 | WhenAll(age_lte_35, pet_not_in_dog_cat),
38 | "NO_GO"
39 | )
40 |
41 | rule_set_decision = RuleSetDecision(rule_row_decision_go, rule_row_decision_no_go)
42 |
43 | # evaluate a fact now against the rule for no go decision
44 | fact_for_no_go = {"age": 25, "pet": "parrot"}
45 | assert rule_set_decision.evaluate(fact_for_no_go) == "NO_GO"
46 |
47 | rule_decision = RuleDecision(rule_set_decision)
48 | assert rule_decision.execute(fact_for_no_go) == "NO_GO"
49 |
--------------------------------------------------------------------------------
/simpleruleengine/test_rule_row_decision.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from simpleruleengine.conditional.when_all import WhenAll
4 | from simpleruleengine.expression.expression import Expression
5 | from simpleruleengine.operator.greater_than import Gt
6 | from simpleruleengine.operator.string_in import In
7 | from simpleruleengine.rulerow.rule_row_decision import RuleRowDecision
8 | from simpleruleengine.token.numeric_token import NumericToken
9 | from simpleruleengine.token.string_token import StringToken
10 |
11 |
12 | class TestRuleRowDecision(TestCase):
13 | def test_evaluate(self):
14 | age_gt_35 = Expression(NumericToken("age"), Gt(35))
15 | pet_in_dog_cat = Expression(StringToken("pet"), In("dog", "cat"))
16 | rule_row_decision_go = RuleRowDecision(
17 | WhenAll(age_gt_35, pet_in_dog_cat),
18 | "GO"
19 | )
20 |
21 | fact = {"age": 40, "pet": "dog"}
22 | assert rule_row_decision_go.evaluate(fact) == "GO"
23 |
--------------------------------------------------------------------------------
/simpleruleengine/test_rule_row_score.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from simpleruleengine.conditional.when_all import WhenAll
4 | from simpleruleengine.expression.expression import Expression
5 | from simpleruleengine.operator.greater_than import Gt
6 | from simpleruleengine.rulerow.rule_row_score import RuleRowScore
7 | from simpleruleengine.token.numeric_token import NumericToken
8 | import pytest
9 | from simpleruleengine.exception.rule_row_exceptions import RuleRowNotEvaluatedException
10 |
11 |
12 | class TestRuleRowScore(TestCase):
13 | def test_evaluate_negative(self):
14 | no_of_bl_pl_paid_off_gt_2 = Expression(NumericToken("no_of_bl_paid_off_successfully"), Gt(2))
15 | _and = WhenAll(no_of_bl_pl_paid_off_gt_2)
16 |
17 | _score_row = RuleRowScore(antecedent=_and, consequent=70)
18 |
19 | _token_dict = {"no_of_bl_paid_off_successfully": 1}
20 | with pytest.raises(RuleRowNotEvaluatedException):
21 | _score_row.evaluate(_token_dict)
22 |
23 | def test_evaluate_positive(self):
24 | no_of_bl_pl_paid_off_gt_2 = Expression(NumericToken("no_of_bl_paid_off_successfully"), Gt(2))
25 | _and = WhenAll(no_of_bl_pl_paid_off_gt_2)
26 |
27 | _score_row = RuleRowScore(antecedent=_and, consequent=70)
28 |
29 | _token_dict = {"no_of_bl_paid_off_successfully": 3}
30 | if _score_row.evaluate(_token_dict) != 70.0:
31 | self.fail()
32 |
--------------------------------------------------------------------------------
/simpleruleengine/test_rule_score.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 | import json
3 |
4 | from simpleruleengine.conditional.when_all import WhenAll
5 | from simpleruleengine.expression.expression import Expression
6 | from simpleruleengine.operator.equal import Eq
7 | from simpleruleengine.operator.greater_than import Gt
8 | from simpleruleengine.operator.greater_than_equal import Gte
9 | from simpleruleengine.operator.less_than import Lt
10 | from simpleruleengine.operator.less_than_equal import Lte
11 | from simpleruleengine.rule.rule_score import RuleScore
12 | from simpleruleengine.rulerow.rule_row_score import RuleRowScore
13 | from simpleruleengine.ruleset.rule_set_score import RuleSetScore
14 | from simpleruleengine.token.numeric_token import NumericToken
15 |
16 |
17 | class TestRuleScore(TestCase):
18 | def test_evaluate_complex_score(self):
19 | no_run_bl_pl_gte_7_score_minus_100 = RuleRowScore(
20 | WhenAll(
21 | Expression(NumericToken("no_of_running_bl_pl"), Gte(7))
22 | ),
23 | -100
24 | )
25 | no_run_bl_pl_gte_4_score_minus_40 = RuleRowScore(
26 | WhenAll(
27 | Expression(NumericToken("no_of_running_bl_pl"), Gte(4))
28 | ),
29 | -40
30 | )
31 | no_run_bl_pl_gte_2_score_30 = RuleRowScore(
32 | WhenAll(
33 | Expression(NumericToken("no_of_running_bl_pl"), Gte(2))
34 | ),
35 | 30
36 | )
37 | no_run_bl_pl_gte_0_score_100 = RuleRowScore(
38 | WhenAll(
39 | Expression(NumericToken("no_of_running_bl_pl"), Gte(0))
40 | ),
41 | 100
42 | )
43 |
44 | no_of_run_bl_pl_rule_set = RuleSetScore(
45 | no_run_bl_pl_gte_7_score_minus_100,
46 | no_run_bl_pl_gte_4_score_minus_40,
47 | no_run_bl_pl_gte_2_score_30,
48 | no_run_bl_pl_gte_0_score_100,
49 | weight=0.5
50 | )
51 |
52 | fact_no_run_bl_pl_2 = dict(no_of_running_bl_pl=2)
53 | assert no_of_run_bl_pl_rule_set.evaluate(fact_no_run_bl_pl_2) == 15.0
54 |
55 | last_loan_drawn_in_months_eq_0_score_30 = RuleRowScore(
56 | WhenAll(
57 | Expression(NumericToken("last_loan_drawn_in_months"), Eq(0))
58 | ),
59 | 30
60 | )
61 | last_loan_drawn_in_months_lt_3_score_minus_30 = RuleRowScore(
62 | WhenAll(
63 | Expression(NumericToken("last_loan_drawn_in_months"), Lt(3))
64 | ),
65 | -30
66 | )
67 | last_loan_drawn_in_months_lte_12_score_40 = RuleRowScore(
68 | WhenAll(
69 | Expression(NumericToken("last_loan_drawn_in_months"), Lte(12))
70 | ),
71 | 40
72 | )
73 | last_loan_drawn_in_months_gt_12_score_100 = RuleRowScore(
74 | WhenAll(
75 | Expression(NumericToken("last_loan_drawn_in_months"), Gt(12))
76 | ),
77 | 100
78 | )
79 |
80 | last_loan_drawn_in_months_rule_set = RuleSetScore(
81 | last_loan_drawn_in_months_eq_0_score_30,
82 | last_loan_drawn_in_months_lt_3_score_minus_30,
83 | last_loan_drawn_in_months_lte_12_score_40,
84 | last_loan_drawn_in_months_gt_12_score_100,
85 | weight=0.5
86 | )
87 |
88 | fact_last_loan_drawn_in_months_lte_12 = dict(last_loan_drawn_in_months=6)
89 | assert last_loan_drawn_in_months_rule_set.evaluate(fact_last_loan_drawn_in_months_lte_12) == 20.0
90 |
91 | fact_rule_score = dict(last_loan_drawn_in_months=6, no_of_running_bl_pl=2)
92 | rule_score = RuleScore(
93 | no_of_run_bl_pl_rule_set,
94 | last_loan_drawn_in_months_rule_set
95 | )
96 | assert rule_score.execute(fact_rule_score) == 35.0
97 | print(json.dumps(rule_score.__dict__, default=vars))
98 |
--------------------------------------------------------------------------------
/simpleruleengine/test_rule_set_decision.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | import pytest
4 |
5 | from simpleruleengine.conditional.when_all import WhenAll
6 | from simpleruleengine.conditional.when_any import WhenAny
7 | from simpleruleengine.expression.expression import Expression
8 | from simpleruleengine.operator.between import Between
9 | from simpleruleengine.operator.greater_than import Gt
10 | from simpleruleengine.operator.greater_than_equal import Gte
11 | from simpleruleengine.operator.string_in import In
12 | from simpleruleengine.operator.less_than_equal import Lte
13 | from simpleruleengine.operator.string_not_in import NotIn
14 | from simpleruleengine.rulerow.rule_row_decision import RuleRowDecision
15 | from simpleruleengine.ruleset.rule_set_decision import RuleSetDecision
16 | from simpleruleengine.token.numeric_token import NumericToken
17 | from simpleruleengine.token.string_token import StringToken
18 |
19 | OWNED_BY_FAMILY = "Owned by Family"
20 |
21 | OWNED_BY_SELF = "Owned by Self"
22 |
23 | OWNED = "Not Owned"
24 |
25 |
26 | class TestRuleSetDecision(TestCase):
27 | def test_evaluate_exception(self):
28 | with pytest.raises(TypeError):
29 | RuleSetDecision("test_1", "test_2")
30 |
31 | def test_evaluate(self):
32 | age_gt_35 = Expression(NumericToken("age"), Gt(35))
33 | pet_in_dog_cat = Expression(StringToken("pet"), In("dog", "cat"))
34 | rule_row_decision_go = RuleRowDecision(
35 | WhenAll(age_gt_35, pet_in_dog_cat),
36 | "GO"
37 | )
38 |
39 | age_lte_35 = Expression(NumericToken("age"), Lte(35))
40 | pet_not_in_dog_cat = Expression(StringToken("pet"), NotIn("dog", "cat"))
41 | rule_row_decision_no_go = RuleRowDecision(
42 | WhenAll(age_lte_35, pet_not_in_dog_cat),
43 | "NO_GO"
44 | )
45 |
46 | rule_set_decision = RuleSetDecision(rule_row_decision_go, rule_row_decision_no_go)
47 |
48 | # evaluate a fact now against the rule for no go decision
49 | fact_for_no_go = {"age": 25, "pet": "parrot"}
50 | assert rule_set_decision.evaluate(fact_for_no_go) == "NO_GO"
51 |
52 | def test_evaluate_simple_decision(self):
53 | cibil_score_between_650_800 = Expression(NumericToken("cibil_score"), Between(floor=650, ceiling=800))
54 | marital_status_in_married_unspecified = Expression(StringToken("marital_status"), In("Married", "Unspecified"))
55 | business_owned_by_self_family = Expression(
56 | StringToken("business_ownership"),
57 | In(OWNED_BY_SELF, OWNED_BY_FAMILY)
58 | )
59 |
60 | rule_row_decision_go = RuleRowDecision(
61 | WhenAll(
62 | cibil_score_between_650_800,
63 | marital_status_in_married_unspecified,
64 | business_owned_by_self_family
65 | ),
66 | "GO"
67 | )
68 | rule_set_decision = RuleSetDecision(rule_row_decision_go)
69 |
70 | fact = dict(cibil_score=700, marital_status="Married", business_ownership=OWNED_BY_SELF)
71 | assert rule_set_decision.evaluate(fact) == "GO"
72 |
73 | def test_evaluate_complex_decision(self):
74 | applicant_age_gte_35 = Expression(NumericToken("applicant_age"), Gte(35))
75 | business_owned_by_self_family = Expression(
76 | StringToken("business_ownership"),
77 | In(OWNED_BY_SELF, OWNED_BY_FAMILY)
78 | )
79 | applicant_owned_by_self_family = Expression(
80 | StringToken("applicant_ownership"),
81 | In(OWNED_BY_SELF, OWNED_BY_FAMILY)
82 | )
83 |
84 | rule_row_decision_go = RuleRowDecision(
85 | WhenAll(
86 | applicant_age_gte_35,
87 | WhenAny(
88 | business_owned_by_self_family,
89 | applicant_owned_by_self_family
90 | )
91 | ),
92 | "GO"
93 | )
94 | rule_set_decision = RuleSetDecision(rule_row_decision_go)
95 |
96 | fact_go = dict(
97 | applicant_age=42,
98 | applicant_ownership=OWNED,
99 | business_ownership=OWNED_BY_SELF
100 | )
101 | assert rule_set_decision.evaluate(fact_go) == "GO"
102 |
103 | fact_no_go_1 = dict(
104 | applicant_age=42,
105 | applicant_ownership=OWNED,
106 | business_ownership=OWNED
107 | )
108 | assert rule_set_decision.evaluate(fact_no_go_1) != "GO"
109 |
110 | fact_no_go_2 = dict(
111 | applicant_age=25,
112 | applicant_ownership=OWNED_BY_SELF,
113 | business_ownership=OWNED_BY_SELF
114 | )
115 | assert rule_set_decision.evaluate(fact_no_go_2) != "GO"
116 |
--------------------------------------------------------------------------------
/simpleruleengine/test_rule_set_score.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | import pytest
4 |
5 | from simpleruleengine.conditional.when_all import WhenAll
6 | from simpleruleengine.conditional.when_any import WhenAny
7 | from simpleruleengine.expression.expression import Expression
8 | from simpleruleengine.operator.equal import Eq
9 | from simpleruleengine.operator.greater_than import Gt
10 | from simpleruleengine.operator.greater_than_equal import Gte
11 | from simpleruleengine.operator.string_in import In
12 | from simpleruleengine.operator.less_than import Lt
13 | from simpleruleengine.operator.less_than_equal import Lte
14 | from simpleruleengine.rule.rule_score import RuleScore
15 | from simpleruleengine.rulerow.rule_row_decision import RuleRowDecision
16 | from simpleruleengine.rulerow.rule_row_score import RuleRowScore
17 | from simpleruleengine.ruleset.rule_set_decision import RuleSetDecision
18 | from simpleruleengine.ruleset.rule_set_score import RuleSetScore
19 | from simpleruleengine.token.numeric_token import NumericToken
20 | from simpleruleengine.token.rule_token import RuleToken
21 | from simpleruleengine.token.string_token import StringToken
22 |
23 |
24 | class TestRuleSetScore(TestCase):
25 | def test_evaluate_exception(self):
26 | with pytest.raises(TypeError):
27 | RuleSetScore(["test_1", "test_2"])
28 |
29 | def test_evaluate(self):
30 | no_of_bl_paid_gt_2 = Expression(NumericToken("no_of_bl_paid_off_successfully"), Gt(2))
31 | score_row = RuleRowScore(antecedent=WhenAll(no_of_bl_paid_gt_2), consequent=70)
32 |
33 | score_set = RuleSetScore(score_row, weight=0.6)
34 | token_dict = {"no_of_bl_paid_off_successfully": 3}
35 |
36 | assert score_set.evaluate(token_dict) == 42
37 |
38 | def test_evaluate_2(self):
39 | _and = WhenAll(Expression(NumericToken("no_of_bl_paid_off_successfully"), Gt(2)))
40 |
41 | _score_row = RuleRowScore(antecedent=_and, consequent=70)
42 |
43 | _score_set = RuleSetScore(_score_row, weight=0.6)
44 | _token_dict = {"no_of_bl_paid_off_successfully": 1}
45 |
46 | assert _score_set.evaluate(_token_dict) == 0.0
47 |
48 | def test_nested_rule(self):
49 | _and = WhenAll(Expression(NumericToken("no_of_bl_paid_off_successfully"), Gt(2)))
50 |
51 | _score_row = RuleRowScore(antecedent=_and, consequent=70)
52 |
53 | _score_set = RuleSetScore(_score_row, weight=0.6)
54 | rule_no_bl_paid_off = RuleScore(_score_set)
55 |
56 | _token_dict = {"no_of_bl_paid_off_successfully": 3}
57 |
58 | assert rule_no_bl_paid_off.execute(_token_dict) == 42
59 |
60 | token_bl_pl_paid_off_gt_40 = Expression(RuleToken("rule_no_bl_paid_off", rule_no_bl_paid_off), Gt(40))
61 | applicant_age_gte_35 = Expression(NumericToken("applicant_age"), Gte(35))
62 | business_owned_by_self_family = Expression(
63 | StringToken("business_ownership"), In("Owned by Self", "Owned by Family")
64 | )
65 | rule_row_decision_go = RuleRowDecision(
66 | WhenAll(
67 | applicant_age_gte_35,
68 | business_owned_by_self_family,
69 | token_bl_pl_paid_off_gt_40
70 | ),
71 | "GO"
72 | )
73 | rule_set_decision = RuleSetDecision(rule_row_decision_go)
74 | fact_go = dict(
75 | no_of_bl_paid_off_successfully=3,
76 | applicant_age=42,
77 | business_ownership="Owned by Self"
78 | )
79 | assert rule_set_decision.evaluate(fact_go) == "GO"
80 |
81 | def test_evaluate_complex_score(self):
82 | no_run_bl_pl_gte_7_score_minus_100 = RuleRowScore(
83 | WhenAll(Expression(NumericToken("no_of_running_bl_pl"), Gte(7))),
84 | -100
85 | )
86 | no_run_bl_pl_gte_4_score_minus_40 = RuleRowScore(
87 | WhenAll(Expression(NumericToken("no_of_running_bl_pl"), Gte(4))),
88 | -40
89 | )
90 | no_run_bl_pl_gte_2_score_30 = RuleRowScore(
91 | WhenAll(Expression(NumericToken("no_of_running_bl_pl"), Gte(2))),
92 | 30
93 | )
94 | no_run_bl_pl_gte_0_score_100 = RuleRowScore(
95 | WhenAll(Expression(NumericToken("no_of_running_bl_pl"), Gte(0))),
96 | 100
97 | )
98 |
99 | no_of_run_bl_pl_rule_set = RuleSetScore(
100 | no_run_bl_pl_gte_7_score_minus_100,
101 | no_run_bl_pl_gte_4_score_minus_40,
102 | no_run_bl_pl_gte_2_score_30,
103 | no_run_bl_pl_gte_0_score_100,
104 | weight=0.5
105 | )
106 |
107 | fact_no_run_bl_pl_2 = dict(no_of_running_bl_pl=2)
108 | assert no_of_run_bl_pl_rule_set.evaluate(fact_no_run_bl_pl_2) == 15.0
109 |
110 | last_loan_drawn_in_months_eq_0_score_30 = RuleRowScore(
111 | WhenAll(Expression(NumericToken("last_loan_drawn_in_months"), Eq(0))),
112 | 30
113 | )
114 | last_loan_drawn_in_months_lt_3_score_minus_30 = RuleRowScore(
115 | WhenAll(Expression(NumericToken("last_loan_drawn_in_months"), Lt(3))),
116 | -30
117 | )
118 | last_loan_drawn_in_months_lte_12_score_40 = RuleRowScore(
119 | WhenAll(Expression(NumericToken("last_loan_drawn_in_months"), Lte(12))),
120 | 40
121 | )
122 | last_loan_drawn_in_months_gt_12_score_100 = RuleRowScore(
123 | WhenAll(Expression(NumericToken("last_loan_drawn_in_months"), Gt(12))),
124 | 100
125 | )
126 |
127 | last_loan_drawn_in_months_rule_set = RuleSetScore(
128 | last_loan_drawn_in_months_eq_0_score_30,
129 | last_loan_drawn_in_months_lt_3_score_minus_30,
130 | last_loan_drawn_in_months_lte_12_score_40,
131 | last_loan_drawn_in_months_gt_12_score_100,
132 | weight=0.5
133 | )
134 |
135 | fact_last_loan_drawn_in_months_lte_12 = dict(last_loan_drawn_in_months=6)
136 | assert last_loan_drawn_in_months_rule_set.evaluate(fact_last_loan_drawn_in_months_lte_12) == 20.0
137 |
--------------------------------------------------------------------------------
/simpleruleengine/test_when_all.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | import pytest
4 |
5 | from simpleruleengine.conditional.when_all import WhenAll
6 | from simpleruleengine.conditional.when_any import WhenAny
7 | from simpleruleengine.operator.greater_than import Gt
8 | from simpleruleengine.operator.string_in import In
9 | from simpleruleengine.operator.less_than import Lt
10 | from simpleruleengine.token.numeric_token import NumericToken
11 | from simpleruleengine.token.string_token import StringToken
12 | from simpleruleengine.expression.expression import Expression
13 |
14 |
15 | class TestWhenAll(TestCase):
16 | def test_evaluate_true(self):
17 | numeric_token_age = NumericToken(name="age")
18 | age_gt_35 = Expression(numeric_token_age, operator=Gt(35))
19 |
20 | string_token_pet = StringToken(name="pet")
21 | pet_in_dog_cat = Expression(string_token_pet, In("dog", "cat"))
22 |
23 | when_all_age_and_pet = WhenAll(age_gt_35, pet_in_dog_cat)
24 |
25 | token_dict = {"age": 40, "pet": "dog"}
26 | assert when_all_age_and_pet.evaluate(token_dict) is True
27 |
28 | def test_evaluate_false(self):
29 | numeric_token_age = NumericToken(name="age")
30 | age_gt_35 = Expression(numeric_token_age, operator=Gt(35))
31 |
32 | string_token_pet = StringToken(name="pet")
33 | pet_in_dog_cat = Expression(string_token_pet, In("dog", "cat"))
34 |
35 | when_all_age_and_pet = WhenAll(age_gt_35, pet_in_dog_cat)
36 |
37 | token_dict = {"age": 40, "pet": "parrot"}
38 | assert when_all_age_and_pet.evaluate(token_dict) is False
39 |
40 | token_dict = {"age": 25, "pet": "parrot"}
41 | assert when_all_age_and_pet.evaluate(token_dict) is False
42 |
43 | def test_insufficient_values(self):
44 | with pytest.raises(ValueError):
45 | numeric_token_age = NumericToken(name="age")
46 | age_gt_35 = Expression(numeric_token_age, operator=Gt(35))
47 |
48 | string_token_pet = StringToken(name="pet")
49 | pet_in_dog_cat = Expression(string_token_pet, In("dog", "cat"))
50 |
51 | when_all_age_and_pet = WhenAll(age_gt_35, pet_in_dog_cat)
52 |
53 | token_dict = {"age": 40}
54 | when_all_age_and_pet.evaluate(token_dict)
55 |
--------------------------------------------------------------------------------
/simpleruleengine/test_when_any.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from simpleruleengine.conditional.when_all import WhenAll
4 | from simpleruleengine.conditional.when_any import WhenAny
5 | from simpleruleengine.expression.expression import Expression
6 | from simpleruleengine.operator.greater_than import Gt
7 | from simpleruleengine.operator.string_in import In
8 | from simpleruleengine.token.numeric_token import NumericToken
9 | from simpleruleengine.token.string_token import StringToken
10 |
11 |
12 | class TestWhenAny(TestCase):
13 | def test_evaluate_true(self):
14 | numeric_token_age = NumericToken(name="age")
15 | age_gt_35 = Expression(numeric_token_age, Gt(35))
16 |
17 | string_token_pet = StringToken(name="pet")
18 | pet_in_dog_cat = Expression(string_token_pet, In("dog", "cat"))
19 |
20 | when_any_age_or_pet = WhenAny(age_gt_35, pet_in_dog_cat)
21 |
22 | token_dict = {"age": 25, "pet": "dog"}
23 | assert when_any_age_or_pet.evaluate(token_dict) is True
24 |
25 | def test_evaluate_false(self):
26 | numeric_token_age = NumericToken(name="age")
27 | age_gt_35 = Expression(numeric_token_age, Gt(35))
28 |
29 | string_token_pet = StringToken(name="pet")
30 | pet_in_dog_cat = Expression(string_token_pet, In("dog", "cat"))
31 |
32 | when_any_age_or_pet = WhenAny(age_gt_35, pet_in_dog_cat)
33 |
34 | token_dict = {"age": 25, "pet": "parrot"}
35 | assert when_any_age_or_pet.evaluate(token_dict) is False
36 |
37 | def test_recursive(self):
38 | numeric_token_age = NumericToken(name="age")
39 | age_gt_35 = Expression(numeric_token_age, operator=Gt(35))
40 |
41 | string_token_pet = StringToken(name="pet")
42 | pet_in_dog_cat = Expression(string_token_pet, In("dog", "cat"))
43 |
44 | string_token_ownership = StringToken(name="ownership")
45 | ownership_in_owned_leased = Expression(string_token_ownership, In("owned", "leased"))
46 |
47 | age_or_pet_condition = WhenAny(age_gt_35, pet_in_dog_cat)
48 |
49 | age_or_pet_and_ownership = WhenAll(
50 | age_or_pet_condition,
51 | ownership_in_owned_leased
52 | )
53 |
54 | token_dict = {"age": 40, "pet": "parrot", "ownership": "owned"}
55 | assert age_or_pet_and_ownership.evaluate(token_dict) is True
56 |
57 | token_dict = {"age": 10, "pet": "dog", "ownership": "rented"}
58 | assert age_or_pet_and_ownership.evaluate(token_dict) is False
59 |
60 | token_dict = {"age": 25, "pet": "parrot", "ownership": "owned"}
61 | assert age_or_pet_and_ownership.evaluate(token_dict) is False
62 |
--------------------------------------------------------------------------------
/simpleruleengine/token/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine/token/__init__.py
--------------------------------------------------------------------------------
/simpleruleengine/token/__pycache__/__init__.cpython-37.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine/token/__pycache__/__init__.cpython-37.pyc
--------------------------------------------------------------------------------
/simpleruleengine/token/__pycache__/test_NumericToken.cpython-37-pytest-6.1.1.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine/token/__pycache__/test_NumericToken.cpython-37-pytest-6.1.1.pyc
--------------------------------------------------------------------------------
/simpleruleengine/token/boolean_token.py:
--------------------------------------------------------------------------------
1 | from simpleruleengine.token.token import Token
2 |
3 |
4 | class BooleanToken(Token):
5 | def __init__(self, name: str):
6 | super().__init__(name)
7 |
8 | def get_token_dict_structure(self) -> dict:
9 | return super(BooleanToken, self).get_token_dict_structure()
10 |
11 | def get_token_value(self, token_dict: dict):
12 | return super(BooleanToken, self).get_token_value(token_dict)
13 |
--------------------------------------------------------------------------------
/simpleruleengine/token/numeric_token.py:
--------------------------------------------------------------------------------
1 | from simpleruleengine.token.token import Token
2 |
3 |
4 | class NumericToken(Token):
5 | def __init__(self, name: str):
6 | super().__init__(name)
7 |
8 | def get_token_dict_structure(self) -> dict:
9 | return super(NumericToken, self).get_token_dict_structure()
10 |
11 | def get_token_value(self, token_dict: dict):
12 | return super(NumericToken, self).get_token_value(token_dict)
13 |
--------------------------------------------------------------------------------
/simpleruleengine/token/rule_token.py:
--------------------------------------------------------------------------------
1 | from simpleruleengine.token.token import Token
2 | from simpleruleengine.rule.rule import Rule
3 |
4 |
5 | class RuleToken(Token):
6 | def __init__(self, name: str, rule: Rule):
7 | super().__init__(name)
8 | self.rule = rule
9 |
10 | def get_token_dict_structure(self) -> dict:
11 | return self.rule.get_token_dict_structure()
12 |
13 | def get_token_value(self, token_dict: dict):
14 | return self.rule.execute(token_dict)
15 |
--------------------------------------------------------------------------------
/simpleruleengine/token/string_token.py:
--------------------------------------------------------------------------------
1 | from simpleruleengine.token.token import Token
2 |
3 |
4 | class StringToken(Token):
5 | def __init__(self, name: str):
6 | super().__init__(name)
7 |
8 | def get_token_dict_structure(self) -> dict:
9 | return super(StringToken, self).get_token_dict_structure()
10 |
11 | def get_token_value(self, token_dict: dict):
12 | return super(StringToken, self).get_token_value(token_dict)
13 |
--------------------------------------------------------------------------------
/simpleruleengine/token/token.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 |
3 |
4 | class Token(ABC):
5 | def __init__(self, name: str):
6 | self.name = name
7 |
8 | @abstractmethod
9 | def get_token_dict_structure(self) -> dict:
10 | return dict(name=self.name, type=type(self).__name__)
11 |
12 | @abstractmethod
13 | def get_token_value(self, token_dict: dict):
14 | if self.name not in token_dict:
15 | raise ValueError("{} not in token_dict".format(self.name))
16 | return token_dict.get(self.name)
17 |
--------------------------------------------------------------------------------
/simpleruleengine/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine/utils/__init__.py
--------------------------------------------------------------------------------
/simpleruleengine/utils/__pycache__/__init__.cpython-37.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine/utils/__pycache__/__init__.cpython-37.pyc
--------------------------------------------------------------------------------
/simpleruleengine/utils/__pycache__/type_util.cpython-37.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeyabalajis/simple-rule-engine/f86f282db8bb47f539f3b512ca45b389cf27eda3/simpleruleengine/utils/__pycache__/type_util.cpython-37.pyc
--------------------------------------------------------------------------------
/simpleruleengine/utils/type_util.py:
--------------------------------------------------------------------------------
1 | def numeric(val) -> bool:
2 | """ Validate whether the value sent is a numeric (integer or float
3 | :returns bool"""
4 | if type(val).__name__ in ('int', 'float'):
5 | return True
6 |
7 | return False
8 |
9 |
10 | def string(val) -> bool:
11 | """ string validates whether the value sent is a string
12 | :returns bool"""
13 | if type(val).__name__ == 'str':
14 | return True
15 |
16 | return False
17 |
18 |
19 | def string_list(val) -> bool:
20 | """ string validates whether the value sent is a string
21 | :returns bool"""
22 | if type(val).__name__ in ('list', 'tuple'):
23 | for ind_val in val:
24 | if not string(ind_val):
25 | return False
26 | return True
27 | return False
28 |
29 |
30 | def is_dict(val) -> bool:
31 | """ is_dict validates whether the value sent is a dict"""
32 | if type(val).__name__ == 'dict':
33 | return True
34 | return False
35 |
--------------------------------------------------------------------------------