# py-rules-engine **Repository Path**: itopen/py-rules-engine ## Basic Information - **Project Name**: py-rules-engine - **Description**: fork from https://github.com/saurabh0719/py-rules-engine - **Primary Language**: Unknown - **License**: BSD-3-Clause - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-12-09 - **Last Updated**: 2025-12-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README

`py-rules-engine` is a robust, Python-based rules engine that enables the creation of intricate logical conditions and actions. ```bash pip install py-rules-engine ``` Key features: - **Complex Logical Conditions**: Define intricate conditions using logical operators. - **Pythonic Rule Builder**: Utilize a Pythonic interface for easy rule definition. - **Flexible Rule Management**: Store, configure, and share rules in JSON/YAML format, with seamless shifting between Python rule builder and other formats. - **Nested Rules**: Create multi-level rule structures. - **Rule Evaluation**: Evaluate rules in a given context with a built-in rule engine. - **Zero Dependencies**: Pure Python implementation for easy installation and use. Example usage - ```python from py_rules.components import Condition, Result, Rule from py_rules.engine import RuleEngine # Create a condition condition = Condition('temperature', '>=', 40) | Condition('wind_speed', '>', 50) # Create a result result = Result('message', 'str', 'Unfavourable weather conditions for work!') # Create a rule rule = Rule('Temperature Rule').If(condition).Then(result) # initialise a new instance of RuleEngine with context context = {'temperature': 45, 'wind_speed': 30} engine = RuleEngine(context) print(engine.evaluate(rule)) # 'Unfavourable weather conditions for work!' # if a Rule is used without a Result, it simply returns True/False rule = Rule('Bool Temperature Rule').If(condition) print(engine.evaluate(rule)) # True ``` ## Table of Contents : * [Installation](#installation) * [Rule structure](#structure) * [Basics](#basics) * [Representation](#repr) * [Rule builder & parser](#builder) * [Components](#components) * [Nested rules](#nested) * [Parser](#parser) * [Rule engine and evaluation](#engine) * [Rule storage and parsing](#storage) * [Tests](#tests) * [To do](#todo) ## Installation You can install `py-rules-engine` using pip: ```bash pip install py-rules-engine ``` **NOTE** - this library is currently in Beta, and things may break between version bumps pre-1.0.0. ## Rule structure This section covers the basics of this library, including the structure of the `Rule` object. ### Basics The `If-Then-Else` structure is a fundamental part of the rule engine. It allows you to define a condition and specify the results based on whether the condition is met or not. Here's a breakdown of each part: - `If`: This is the condition that will be evaluated. It can be a simple condition (like "temperature > 30") or a complex condition involving multiple variables and logical operators (like "temperature > 30 AND humidity < 50"). The condition is evaluated against a given `context`, which is a dictionary of variables and their values. - `Then`: This part specifies the result or action that should be returned or performed if the IF condition is met (i.e., if it evaluates to True). It can be a simple value (like "It's hot") or a complex object. It can also be another `Rule`, allowing for `nested rules`. - `Else`: This part specifies the result or action that should be returned or performed if the IF condition is not met (i.e., if it evaluates to False). Like the THEN part, it can be a simple value, a complex object, or another `Rule`. The ELSE part is optional; if it's not provided and the IF condition is not met, the rule engine will return False. Every `Rule` object's `dict` representation (rule.to_dict()) contains the following - `metadata`: This section contains metadata about the rule. - `version`: The version of the rule. - `id`: A unique identifier for the rule. - `parent_id`: The id of the parent rule if this rule is nested, otherwise null. - `created`: The UTC timestamp when the rule was created. - `name`: The name of the rule. - `required_context_parameters`: A list of context parameters that are required for this rule. These variables/parameters are passed to the `RuleEngine` via a `context` dictionary - `if`: This section contains the condition to be evaluated. - `condition` or `and` or `or`: The condition to be evaluated. It can be a single condition or a logical combination (`and`, `or`) of multiple conditions. Each condition consists of a `variable`, an `operator`, and a `value`. - `then`: This section contains the result to be returned if the `if` condition evaluates to `True`. - `else`: This section contains the result to be returned if the `if` condition evaluates to `False`, or a nested rule with its own `if`, `then`, and `else` sections. All the `Rule`(s) are evaluated against a `context` dictionary. The `context` is key-value dictionary of `facts` we use to evaluate all conditions and prepare return statements. See the `examples/` directory for more. [Go back to top](#table-of-contents)
### Representation Every `Rule` object and it's conditions is represented as a `dictionary` structure. Ev For example- ```python from py_rules.components import Condition, Result, Rule from py_rules.engine import RuleEngine # Create a condition condition = Condition('temperature', '>', 40) | Condition('wind_speed', '>', 50) no_work = Result('message', 'str', 'Unfavourable weather conditions for work!') work = Result('message', 'str', 'Favourable weather conditions for work!') rule = Rule('Temperature Rule').If(condition).Then(no_work).Else(work) print(rule.to_dict()) """ { "metadata": { "version": "0.3.0", "type": "Rule", "id": "a605f337-7d60-4a65-a361-96c282e3fc74", "created": "2023-12-15 12:53:48.492187", "required_context_parameters": ["wind_speed", "temperature"], "name": "Temperature Rule", "parent_id": None, }, "if": { "or": [ { "condition": { "metadata": { "version": "0.3.0", "type": "Condition", "id": "10346a0c-3934-4ed9-b393-053b871ab17d", "created": "2023-12-15 12:53:48.492090", "required_context_parameters": ["temperature"], }, "variable": "temperature", "operator": ">", "value": {"type": "int", "value": 40}, } }, { "condition": { "metadata": { "version": "0.3.0", "type": "Condition", "id": "0f3e660e-01a6-41d1-849c-e5b27088ccb4", "created": "2023-12-15 12:53:48.492140", "required_context_parameters": ["wind_speed"], }, "variable": "wind_speed", "operator": ">", "value": {"type": "int", "value": 50}, } }, ] }, "then": { "result": { "message": { "type": "str", "value": "Unfavourable weather conditions for work!", } } }, "else": { "result": { "message": { "type": "str", "value": "Favourable weather conditions for work!", } } }, } """ ``` This `dictionary` is what is used for evaluation. [Go back to top](#table-of-contents)
## Rule builder & parser ### Rule components The following components can be used to build complex Rules. - `Condition`: This class represents a condition in a rule. It can be initialized with a variable, operator, and value, or with a condition dictionary. It supports logical `and` and `or` operations to combine conditions. The `to_dict` method returns a dictionary representation of the condition. - `Result`: This class represents a result in a Rule. It can be initialized with a key, type, and value, or with a result dictionary. It supports the `and` operation to combine results. The `to_dict` method returns a dictionary representation of the result. - `Rule`: This class represents a rule. It can be initialized with a name and optional keyword arguments. It supports the `If`, `Then`, and `Else` methods to set the condition and results of the rule. The `to_dict` method returns a dictionary representation of the rule. ```python from py_rules.components import Condition, Result, Rule # Create a condition condition = Condition('temperature', '>', 30) # Create a result result = Result('message', 'str', 'It is hot!') # Create a rule rule = Rule('Temperature Rule').If(condition).Then(result) # Print the rule print(rule.to_dict()) ``` Output - ```python { "metadata": { "version": "0.3.0", "type": "Rule", "id": "5dbca846-5e59-4b6c-bdbf-9602d68c79ff", "created": "2023-12-15 04:49:45.183531", "required_context_parameters": ["temperature"], "name": "Temperature Rule", "parent_id": None, }, "if": { "condition": { "metadata": { "version": "0.3.0", "type": "Condition", "id": "14950c65-3741-40da-ac13-e5cf1a0c49ce", "created": "2023-12-15 04:49:45.183438", "required_context_parameters": ["temperature"], }, "variable": "temperature", "operator": ">", "value": {"type": "int", "value": 30}, } }, "then": {"result": {"message": {"type": "str", "value": "It is hot!"}}}, } ``` This will create a rule that checks if the temperature is greater than 30 and returns the message "It is hot!" if the condition is met. ### Nested rules - ```python from py_rules.components import Condition, Result, Rule # Create conditions condition1 = Condition('temperature', '>', 30) condition2 = Condition('humidity', '<', 50) # Create results result1 = Result('message', 'str', 'It is hot!') result2 = Result('message', 'str', 'It is dry!') # Create rules rule1 = Rule('Temperature Rule').If(condition1).Then(result1) rule2 = Rule('Humidity Rule').If(condition2).Then(result2) # Create a nested rule nested_rule = Rule('Nested Rule').If(condition1).Then(rule2).Else(result1) # Print the nested rule print(nested_rule.to_dict()) ``` This will create a nested rule that checks if the temperature is greater than 30. If the condition is met, it evaluates another rule that checks if the humidity is less than 50. If the nested condition is met, it returns the message "It is dry!". If the nested condition is not met, it returns the message "It is hot!". Here's the equivalent rule in JSON format: ```json { "metadata": { "version": "0.3.0", "type": "Rule", "id": "7f2a4893-322c-4973-8e41-c3930e37648b", "created": "2023-12-15 04:57:33.071026", "required_context_parameters": ["temperature", "humidity"], "name": "Nested Rule", "parent_id": null, }, "if": { "condition": { "metadata": { "version": "0.3.0", "type": "Condition", "id": "515a3a0e-6a45-4848-9ea9-8202e66372cf", "created": "2023-12-15 04:57:33.070916", "required_context_parameters": ["temperature"], }, "variable": "temperature", "operator": ">", "value": {"type": "int", "value": 30}, } }, "then": { "metadata": { "version": "0.3.0", "type": "Rule", "id": "7c045ec9-c262-4c6c-8b0f-34cc773d4f41", "created": "2023-12-15 04:57:33.071017", "required_context_parameters": ["humidity"], "name": "Humidity Rule", "parent_id": null, }, "if": { "condition": { "metadata": { "version": "0.3.0", "type": "Condition", "id": "ddc1e0f2-54a5-4c4f-aedd-2f6db69741dc", "created": "2023-12-15 04:57:33.070964", "required_context_parameters": ["humidity"], }, "variable": "humidity", "operator": "<", "value": {"type": "int", "value": 50}, } }, "then": {"result": {"message": {"type": "str", "value": "It is dry!"}}}, }, "else": {"result": {"message": {"type": "str", "value": "It is hot!"}}}, } ``` [Go back to top](#table-of-contents)
### Rule Parser The `RuleParser` class is a utility class that is used to parse a rule from a `python dictionary` into a `Rule` object. This is useful when you want to define rules in a another format (JSON, YAML, etc.) and then load them into your Python code. ```python from py_rules.components import Rule from py_rules.parser import RuleParser # Define a rule as a dictionary rule_dict = { "metadata": { "version": "0.3.0", "type": "Rule", "id": "9911bf90-b6a1-490f-ae5a-3fa9409529f8", "created": "2023-12-15 13:09:59.411292", "required_context_parameters": ["temperature"], "name": "Unnamed Rule 1", "parent_id": None, }, "if": { "condition": { "metadata": { "version": "0.3.0", "type": "Condition", "id": "a4b678f7-44f3-4818-8a72-298880263703", "created": "2023-12-15 13:09:59.411350", "required_context_parameters": ["temperature"], }, "variable": "temperature", "operator": ">", "value": {"type": "int", "value": 30}, } }, "then": {"result": {"message": {"type": "str", "value": "It's hot"}}}, "else": {"result": {"message": {"type": "str", "value": "It's not hot"}}}, } # Parse the rule rule = RuleParser().parse(rule_dict) print(type(rule)) # assert isinstance(rule, Rule) print(rule.metadata) # {'version': '0.3.0', 'type': 'Rule', 'id': '9911bf90-b6a1-490f-ae5a-3fa9409529f8', 'created': '2023-12-15 13:09:59.411292', 'required_context_parameters': ['temperature'], 'name': 'Unnamed Rule 1', 'parent_id': None} ``` [Go back to top](#table-of-contents)
## Rule engine and evaluation The `RuleEngine` class is used to evaluate a rules. It takes a `Rule` object and a context (a dictionary) as input. The `Rule` is then evaluated against the `context` Here's a breakdown of the methods in the RuleEngine class: - `__init__`: Initializes the RuleEngine with a `context: dict`; a dictionary of `facts`. - The `context` dictionary MUST contain all the `variable`(s) being used, either in results or in conditions. The `required_context_parameters` set() of the `Rule` maintains a list of all unique context parameters required to evaluate the rule. - `evaluate(rule: Rule)`: Evaluates the rule. It checks the 'if' condition and returns the result of the 'then' action if the condition is met, or the result of the 'else' action otherwise. Here's an example of how to use the RuleEngine class: ```python from py_rules.builder import Condition, Result, Rule from py_rules.engine import RuleEngine # Create a condition condition = Condition('temperature', '>', 30) # Create a result result = Result('message', 'str', 'It is hot!') # Create a rule rule = Rule('Temperature Rule').If(condition).Then(result) # Create a context context = {'temperature': 35} # Create a rule engine engine = RuleEngine(context) # Evaluate the rule print(engine.evaluate(rule)) # prints: {'message': 'It is hot!'} ``` In this example, the rule checks if the temperature is greater than 30. The context provides the actual temperature. The rule engine evaluates the rule in the given context and returns the result of the rule. You can also use the `RuleEngine` class with complex rules that have nested conditions and multiple results. Here's an example: ```python # Create conditions condition1 = Condition('temperature', '>', 30) condition2 = Condition('humidity', '<', 50) # Create results result1 = Result('message', 'str', 'It is hot!') result2 = Result('message', 'str', 'It is dry!') # Create rules rule1 = Rule('Temperature Rule').If(condition1).Then(result1) rule2 = Rule('Humidity Rule').If(condition2).Then(result2) # Create a context context = {'temperature': 35, 'humidity': 45} # Create a rule engine engine = RuleEngine(context) # Evaluate the rules print(engine.evaluate(rule1)) # prints: {'message': 'It is hot!'} print(engine.evaluate(rule2)) # prints: {'message': 'It is dry!'} ``` In this example, the first rule checks if the temperature is greater than 30, and the second rule checks if the humidity is less than 50. The context provides the actual temperature and humidity. The rule engines evaluate the rules in the given context and return the results of the rules. [Go back to top](#table-of-contents)
## Rule parsing and storage Storing & loading rules from persistent storage enable reuse of rules across different sessions and sharing of rules between different systems. The `RuleStorage` class is an abstract base class that defines the common interface for all storage classes. It has two abstract methods: `load` and `store`. Any class that inherits from `RuleStorage` must implement these methods. The `RuleStorage` class also has a RuleParser object that is used to parse a rule **after it is loaded into a `dictionary`** format. The `JSONRuleStorage` and `PickledRuleStorage` classes are concrete classes that inherit from RuleStorage and implement the load and store methods. Each class is designed to work with a specific file format. Each class also validates the file type in its constructor to ensure that it matches the expected file type. If the file type is not valid, it raises an `InvalidRuleError`. You can also create your own `RuleStorage` class, as shown in the example below for `yaml` files - ```python import yaml from py_rules.components import Condition, Result, Rule from py_rules.storages import RuleStorage, JSONRuleStorage, PickledRuleStorage yaml.Dumper.ignore_aliases = lambda *args: True # CUSTOM Yaml Storage class YAMLRuleStorage(RuleStorage): """ RuleStorage class for YAML files. """ format = 'yaml' def __init__(self, file_path): """ Initialize the loader with a file_path. """ super().__init__() self.file_path = file_path # validate that the file_path is valid and is a yaml file if not self.file_path.endswith('.yaml'): raise InvalidRuleError('Invalid file type. Only YAML files are supported.') def load(self): """ Load a rule from a YAML file. """ data = {} with open(self.file_path) as f: data = yaml.load(f, Loader=yaml.FullLoader) return self.parser.parse(data) def store(self, rule): """ Store a rule in a YAML file. """ data = rule.to_dict() with open(self.file_path, 'w') as f: yaml.dump(data, f, default_flow_style=False, indent=4, sort_keys=False) # Define conditions condition1 = Condition('number', 'in', [1, 2, 3]) condition2 = Condition('number', '=', 1) # Combine conditions using logical 'and' combined_condition = condition1 & condition2 # Define results result1 = Result('xyz', 'str', 'Condition met') result2 = Result('result', 'variable', 'xyz') # Combine results using logical 'and' combined_result = result1 & result2 # Define a nested rule nested_rule = Rule('Nested rule').If(condition1).Then(result1) # Define a complex rule with nested conditions and rules complex_rule = Rule('Complex rule').If(combined_condition).Then(combined_result).Else(nested_rule) # Store the complex rule in different formats JSONRuleStorage('rule.json').store(complex_rule) YAMLRuleStorage('rule.yaml').store(complex_rule) # Load the complex rule from each format json_rule = JSONRuleStorage('rule.json').load() yaml_rule = YAMLRuleStorage('rule.yaml').load() assert complex_rule == json_rule == yaml_rule ``` `rule.json` - ```json { "metadata": { "version": "0.3.0", "type": "Rule", "id": "8330dd39-a0a4-4f21-aab4-0f8e35924c74", "created": "2023-12-15 04:54:00.349206", "required_context_parameters": [ "xyz", "number" ], "name": "Complex rule", "parent_id": null }, "if": { "and": [ { "condition": { "metadata": { "version": "0.3.0", "type": "Condition", "id": "16a74acf-3dfd-4c8f-a280-50dc3970455c", "created": "2023-12-15 04:54:00.349063", "required_context_parameters": [ "number" ] }, "variable": "number", "operator": "in", "value": { "type": "list", "value": [ { "type": "int", "value": 1 }, { "type": "int", "value": 2 }, { "type": "int", "value": 3 } ] } } }, { "condition": { "metadata": { "version": "0.3.0", "type": "Condition", "id": "137af61f-16af-44b9-8c49-a1c61912704b", "created": "2023-12-15 04:54:00.349134", "required_context_parameters": [ "number" ] }, "variable": "number", "operator": "=", "value": { "type": "int", "value": 1 } } } ] }, "then": { "result": { "xyz": { "type": "str", "value": "Condition met" }, "result": { "type": "variable", "value": "xyz" } } }, "else": { "metadata": { "version": "0.3.0", "type": "Rule", "id": "41ad9aa7-d072-49d1-9336-4186a3a6b69c", "created": "2023-12-15 04:54:00.349189", "required_context_parameters": [ "number" ], "name": "Nested rule", "parent_id": null }, "if": { "condition": { "metadata": { "version": "0.3.0", "type": "Condition", "id": "16a74acf-3dfd-4c8f-a280-50dc3970455c", "created": "2023-12-15 04:54:00.349063", "required_context_parameters": [ "number" ] }, "variable": "number", "operator": "in", "value": { "type": "list", "value": [ { "type": "int", "value": 1 }, { "type": "int", "value": 2 }, { "type": "int", "value": 3 } ] } } }, "then": { "result": { "xyz": { "type": "str", "value": "Condition met" } } } } } ``` `rule.yaml` ```yaml metadata: version: 0.3.0 type: Rule id: 8330dd39-a0a4-4f21-aab4-0f8e35924c74 created: '2023-12-15 04:54:00.349206' required_context_parameters: - xyz - number name: Complex rule parent_id: null if: and: - condition: metadata: version: 0.3.0 type: Condition id: 16a74acf-3dfd-4c8f-a280-50dc3970455c created: '2023-12-15 04:54:00.349063' required_context_parameters: - number variable: number operator: in value: type: list value: - type: int value: 1 - type: int value: 2 - type: int value: 3 - condition: metadata: version: 0.3.0 type: Condition id: 137af61f-16af-44b9-8c49-a1c61912704b created: '2023-12-15 04:54:00.349134' required_context_parameters: - number variable: number operator: '=' value: type: int value: 1 then: result: xyz: type: str value: Condition met result: type: variable value: xyz else: metadata: version: 0.3.0 type: Rule id: 41ad9aa7-d072-49d1-9336-4186a3a6b69c created: '2023-12-15 04:54:00.349189' required_context_parameters: - number name: Nested rule parent_id: null if: condition: metadata: version: 0.3.0 type: Condition id: 16a74acf-3dfd-4c8f-a280-50dc3970455c created: '2023-12-15 04:54:00.349063' required_context_parameters: - number variable: number operator: in value: type: list value: - type: int value: 1 - type: int value: 2 - type: int value: 3 then: result: xyz: type: str value: Condition met ``` [Go back to top](#table-of-contents)
## Tests Install all dev dependencies ```sh $ pip install dev-requirements.txt ``` Run tests - ```sh python -m unittest -v ``` ## To do - Pass a global config dictionary from `RuleEngine` to control the following - - Date parsing functions - Rule metadata creation [Go back to top](#table-of-contents)