├── .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 | 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 | [![CodeFactor](https://www.codefactor.io/repository/github/jeyabalajis/simple-rule-engine/badge)](https://www.codefactor.io/repository/github/jeyabalajis/simple-rule-engine) 9 | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/jeyabalajis/simple-rule-engine/tree/main.svg?style=shield)](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 | --------------------------------------------------------------------------------