Python DSL for setting up business intelligence rules
business-rule-engine ====================
As a software system grows in complexity and usage, it can become burdensome if every change to the logic/behavior of the system also requires you to write and deploy new code. The goal of this business rules engine is to provide a simple interface allowing anyone to capture new rules and logic defining the behavior of a system, and a way to then process those rules on the backend.
You might, for example, find this is a useful way for analysts to define marketing logic around when certain customers or items are eligible for a discount, or to automate emails after users enter a certain state or go through a particular sequence of events.
Usage
1. Define your variables
Variables represent values in your system, usually the value of some particular object. You create rules by setting threshold conditions such that when a variable is computed that triggers the condition, some action is taken.
params = {
'productsinstock': 10
}
2. Define custom functions
def ordermore(itemsto_order):
print("you ordered {} new items".format(itemstoorder))
return itemstoorder
3. Write the rules
Rules use standard Python expression syntax. The when block supports multi-line expressions with and/or โ lines are joined and evaluated as a single Python expression. Each then line is an action executed in order.
rules = """
rule "order new items"
when
productsinstock < 20
then
order_more(50)
end
"""
4. Create the parser and execute
from businessruleengine import RuleParser
parser = RuleParser() parser.registerfunction(ordermore) parser.parsestr(rules)
result = parser.execute(params) if result: print("A rule was triggered")
Multiple conditions and multiple actions
The when block uses standard Python boolean syntax. Multiple lines are joined into a single expression, so and/or work exactly as in Python. Each line in the then block is a separate action executed in order.
rules = """
rule "standard reorder"
when
productsinstock < 20
and margin > 0.3
then
order_more(50)
notify_purchasing()
end
rule "urgent reorder" when productsinstock < 5 or products_reserved > 100 then order_more(200) notify_manager() end """
Custom functions
You can register your own functions to use in conditions and actions:
from businessruleengine import RuleParser
def is_even(num): return (num % 2) == 0
params = { 'number': 10 }
rules = """ rule "check even number" when is_even(number) == True then print("is even") end """
parser = RuleParser() parser.registerfunction(iseven) parser.register_function(print) parser.parsestr(rules) parser.execute(params)
You can also register a function under a different name for use in rules:
parser.registerfunction(iseven, "even") # use as even(number) in rules
## Rule options
Priority
Rules with a higher priority are evaluated first. The default priority is 0. Rules with equal priority are evaluated in the order they were added.
Priority can be set on the rule line or as a separate keyword:
rule "urgent reorder" priority 10 when productsinstock < 5 then order_more(200) end
rule "standard reorder" priority 5 when productsinstock < 20 then order_more(50) end
Priority can also be set when using add_rule():python parser.addrule("urgent reorder", "productsinstock < 5", "ordermore(200)", priority=10) ### Description
Rules can carry a human-readable description:
rule "standard reorder" description "Triggers a standard reorder when stock falls below 20 units" when productsinstock < 20 then order_more(50) end ### Allowing non-boolean conditions
By default, the parser raises ConditionReturnValueError if a when expression does not evaluate to a bool. Set conditionrequiresbool=False to accept any truthy/falsy value instead:
python parser = RuleParser(conditionrequiresbool=False) This can also be set per rule:python rule = Rule("my rule", conditionrequiresbool=False) ### Enabling and disabling rules
Rules can be disabled at runtime without removing them from the parser:
python parser.rules["standard reorder"].enabled = False Disabled rules are skipped during execute() and do not appear in the execution results.
Removing and counting rules
Check whether a rule exists and how many rules are loaded:
python "standard reorder" in parser # True / False len(parser) # number of registered rules Remove a single rule or all rules at once:python parser.remove_rule("standard reorder") # raises KeyError if not found parser.clear_rules() # removes all rules ### Managing custom functions
Remove a previously registered function or clear all of them:
python RuleParser.unregisterfunction("ordermore") # raises KeyError if not found RuleParser.clear_functions() # removes all custom functions Note: CUSTOMFUNCTIONS is shared across all parser instances, so clearfunctions() affects every instance.
Processing all matching rules
By default, execute() stops after the first rule whose condition is satisfied (stoponfirst_trigger=True). Set it to False to evaluate every enabled rule regardless:
python result = parser.execute(params, stoponfirst_trigger=False)
for r in result.results: if r.triggered: print(f"{r.rulename}: {r.actionresult}")
This is useful when multiple independent rules may apply to the same input.
Loading rules from a file
Use parsefile() to load rules directly from a file:
python parser = RuleParser() parser.registerfunction(ordermore) parser.parsefile("rules/reorder.rules") parser.execute(params) ## Accessing execution results
execute() returns an ExecutionResult object that behaves like a bool but also gives you access to the result of each rule:
python from businessruleengine import RuleParser
def ordermore(itemsto_order): return "you ordered {} new items".format(itemstoorder)
def notify_purchasing(): return "purchasing notified"
rules = """ rule "standard reorder" when productsinstock < 20 then order_more(50) notify_purchasing() end """
parser = RuleParser() parser.registerfunction(ordermore) parser.registerfunction(notifypurchasing) parser.parsestr(rules)
result = parser.execute({'productsinstock': 10})
for r in result.results: if r.triggered: print(f"{r.rulename}: {r.actionresult}") # โ standard reorder: ['you ordered 50 new items', 'purchasing notified']
Each entry in result.results is a RuleResult with these fields:
| Field | Type | Description | |---|---|---| | rule_name | str | Name of the rule | | triggered | bool | Whether all conditions evaluated to True | | condition_result | bool | Result of the condition evaluation | | action_result | list[object] | Return values of each action, or [] if not triggered |
Handle missing rule parameters
If a required argument is missing, the rule engine raises a MissingArgumentError.
For cases where you work with incomplete data, you can provide a default value:
python params = {}
parser = RuleParser() parser.registerfunction(ordermore) parser.parsestr(rules) parser.execute(params, setdefaultarg=True, default_arg=0)
## More control of the RuleParser
If you need full control over rule execution, you can iterate over the parser and execute each rule individually:
python from businessruleengine import RuleParser from businessruleengine.exceptions import MissingArgumentError
def ordermore(itemsto_order): return "you ordered {} new items".format(itemstoorder)
rules = """ rule "order new items" when productsinstock < 20 then order_more(50) end """
params = {'productsinstock': 10}
parser = RuleParser() parser.registerfunction(ordermore) parser.parsestr(rules)
for rule in parser: try: conditionresult, actionresults = rule.execute(params) if rule.status: print(action_results[0]) break except MissingArgumentError: pass
## Error Handlingpython from businessruleengine import RuleParser from businessruleengine.exceptions import MissingArgumentError
def ordermore(itemsto_order): return "you ordered {} new items".format(itemstoorder)
intentional typo
params = { 'produtcsinstock': 30 }rules = """ rule "order new items" when productsinstock < 20 then order_more(50) end """
parser = RuleParser() parser.registerfunction(ordermore) parser.parsestr(rules)
try: result = parser.execute(params) if not result: print("No conditions matched") except MissingArgumentError as e: print(e)
## Debug
To debug the rules processing, use the logging module:
python import sys import logging logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) ---
Migration Guide
From 0.x to 1.x
Version 1.0 is a major rewrite with several breaking changes.
Rule expression syntax
Rules no longer use Excel-style syntax. All expressions are plain Python:
| 0.x (Excel syntax) | 1.x (Python syntax) | |---|---| | AND(a, b) | a and b | | OR(a, b) | a or b | | NOT(a) | not a | | a = b | a == b |
Multiple when lines
Multiple when lines are no longer treated as independent conditions joined with AND. They are concatenated into a single Python expression, so you must write the operators explicitly:
0.x โ implicit AND between lines
when productsinstock < 20 margin > 0.31.x โ explicit operator required
when productsinstock < 20 and margin > 0.3#### action_result is now a list
Each then line is a separate action. action_result is now list[object] instead of a single value:
python 0.x
conditionresult, actionresult = rule.execute(params) print(action_result) # single value1.x
conditionresult, actionresults = rule.execute(params) print(action_results[0]) # first action result print(action_results) # all action results#### Renamed and restructured exceptions
The exception base class and one exception were renamed for consistency with Python naming conventions:
| 0.x | 1.x | |---|---| | RuleParserException | RuleParserError | | DuplicateRuleName | DuplicateRuleNameError |
A new exception DuplicateThenError (subclass of RuleParserSyntaxError) is raised when a rule block contains more than one then section.
Boolean parameters are now keyword-only
All boolean and optional parameters must be passed as keyword arguments:
python 0.x
parser = RuleParser(False) rule = Rule("name", False, 10, False) parser.execute(params, False)1.x
parser = RuleParser(conditionrequiresbool=False) rule = Rule("name", conditionrequiresbool=False, priority=10, enabled=False) parser.execute(params, stoponfirst_trigger=False)This also applies to add_rule():python
0.x
parser.add_rule("name", "cond", "action", 10, False, "desc")1.x
parser.add_rule("name", "cond", "action", priority=10, enabled=False, description="desc") ``
CUSTOM_FUNCTIONS is now a class-level dict
In 0.x, functions were registered as a list of strings. In 1.x,
CUSTOM_FUNCTIONS is a dict[str, Callable] (name โ callable) and is shared across all parser instances. Use registerfunction() to add functions โ do not modify CUSTOMFUNCTIONS` directly.