rule-engine-core 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rule_engine_core/__init__.py +0 -0
- rule_engine_core/entity_base.py +94 -0
- rule_engine_core/entity_dao.py +179 -0
- rule_engine_core/entity_types.py +10 -0
- rule_engine_core/filter.py +145 -0
- rule_engine_core/metadata_store.py +49 -0
- rule_engine_core/parser.py +222 -0
- rule_engine_core/rule.py +137 -0
- rule_engine_core-0.0.1.dist-info/METADATA +36 -0
- rule_engine_core-0.0.1.dist-info/RECORD +12 -0
- rule_engine_core-0.0.1.dist-info/WHEEL +5 -0
- rule_engine_core-0.0.1.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This is the base class for filter and rule entities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from rule_engine_core.parser import Node, Parser
|
|
7
|
+
|
|
8
|
+
from rule_engine_core.entity_types import EntityEnum, EntityNode
|
|
9
|
+
|
|
10
|
+
class EntityBase(ABC):
|
|
11
|
+
"""
|
|
12
|
+
Base class for entities like Filter and Rule.
|
|
13
|
+
Provides a common implementation for evaluate and enforces validate implementation.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, expression: str, node_type: EntityEnum, name: str):
|
|
17
|
+
"""
|
|
18
|
+
Initializes the entity with an expression string and parses it into an expression tree.
|
|
19
|
+
"""
|
|
20
|
+
self.expression = expression
|
|
21
|
+
self._expression_tree: Node = self._parse_expression(expression)
|
|
22
|
+
self.node_type = node_type
|
|
23
|
+
self.name = name
|
|
24
|
+
|
|
25
|
+
def validate(self) -> bool:
|
|
26
|
+
"""
|
|
27
|
+
Validates whether the operands of the entity are valid.
|
|
28
|
+
_validate_node must be implemented by subclasses
|
|
29
|
+
"""
|
|
30
|
+
return self._validate_node(self._expression_tree)
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def _validate_node(self, node: Node) -> bool:
|
|
34
|
+
"""
|
|
35
|
+
Validates the operands of the entity.
|
|
36
|
+
Must be implemented by subclasses.
|
|
37
|
+
"""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def _evaluate_node(self, node: Node, data: dict, entity_cache: dict, entity_eval_cache) -> float|int|bool:
|
|
42
|
+
"""
|
|
43
|
+
Evaluates a single node of the expression tree.
|
|
44
|
+
Must be implemented by subclasses.
|
|
45
|
+
"""
|
|
46
|
+
pass
|
|
47
|
+
"""
|
|
48
|
+
During eval, entity_cache should not be modified, if any lock is to be acquired, it should be acquired
|
|
49
|
+
before evaluation. Similarly, entity_eval_cache should not be modified during eval.
|
|
50
|
+
"""
|
|
51
|
+
def evaluate(self, pos_data: dict, entity_eval_cache: dict, entity_cache: dict[EntityEnum, dict[str, Node]]) -> float|int|bool:
|
|
52
|
+
"""
|
|
53
|
+
Evaluates the entity against the given data.
|
|
54
|
+
The evaluation is done by traversing the expression tree and evaluating each node.
|
|
55
|
+
Arguments:
|
|
56
|
+
pos_data: Dictionary of data where keys are position id and the value is a dictionary of fields and their resepective values for the position.
|
|
57
|
+
entity_eval_cache: Dictionary of cached evaluation results for entities. We do a dfs sort of thing and keep on populating the cache with the results.
|
|
58
|
+
entity_cache: Dictionary of cached entities. This is used to get entity object by entity id.
|
|
59
|
+
"""
|
|
60
|
+
return self._evaluate_node(EntityNode(self._expression_tree, self.node_type), pos_data, entity_eval_cache, entity_cache)
|
|
61
|
+
|
|
62
|
+
def _parse_expression(self, expression: str) -> Node:
|
|
63
|
+
"""
|
|
64
|
+
Parses the expression string into an expression tree.
|
|
65
|
+
"""
|
|
66
|
+
return Parser.parse(expression)
|
|
67
|
+
|
|
68
|
+
def _check_if_node_is_number(self, node_ident: str) -> bool:
|
|
69
|
+
"""
|
|
70
|
+
Check if the node is a number (int/float)
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
int(node_ident)
|
|
74
|
+
return True
|
|
75
|
+
except ValueError:
|
|
76
|
+
try:
|
|
77
|
+
float(node_ident)
|
|
78
|
+
return True
|
|
79
|
+
except ValueError:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
def __getstate__(self):
|
|
83
|
+
"""Control what gets pickled, excluding connection objects and other unpicklable items"""
|
|
84
|
+
# only copy expression tree, expression, name and node_type
|
|
85
|
+
# rest everything is not needed
|
|
86
|
+
state = {
|
|
87
|
+
'expression': self.expression,
|
|
88
|
+
'node_type': self.node_type,
|
|
89
|
+
'name': self.name,
|
|
90
|
+
'expression_tree': self._expression_tree,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
return state
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the EntityDao class.
|
|
3
|
+
This is responsible for storing and managing filter and rule entities.
|
|
4
|
+
We will store these in a postgres database.
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
from psycopg2.extensions import connection as PgConnection
|
|
8
|
+
import psycopg2
|
|
9
|
+
from rule_engine_core.parser import Node
|
|
10
|
+
import os, logging
|
|
11
|
+
from typing import Optional
|
|
12
|
+
import pickle
|
|
13
|
+
import sys
|
|
14
|
+
from rule_engine_core.entity_types import EntityEnum, EntityNode
|
|
15
|
+
|
|
16
|
+
# Add an alias for backwards compatibility with pickled data
|
|
17
|
+
# This allows pickle to find the module at the old path
|
|
18
|
+
sys.modules['entity_types'] = sys.modules['rule_engine_core.entity_types']
|
|
19
|
+
sys.modules['parser'] = sys.modules['rule_engine_core.parser']
|
|
20
|
+
|
|
21
|
+
from rule_engine_core.entity_base import EntityBase
|
|
22
|
+
|
|
23
|
+
_logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
def create_connection(config: Optional[dict] = None) -> PgConnection:
|
|
26
|
+
if config is None:
|
|
27
|
+
# Load the config from the environment variables
|
|
28
|
+
# and assume db is on the same host as the application
|
|
29
|
+
# using the default port: 5432
|
|
30
|
+
config = {
|
|
31
|
+
"database": os.getenv("POSTGRES_DB"),
|
|
32
|
+
"user": os.getenv("POSTGRES_USER"),
|
|
33
|
+
"password": os.getenv("POSTGRES_PASSWORD"),
|
|
34
|
+
"host": os.getenv("POSTGRES_HOST", "localhost"),
|
|
35
|
+
# "port": os.getenv("POSTGRES_PORT", 5432),
|
|
36
|
+
}
|
|
37
|
+
connection = psycopg2.connect(
|
|
38
|
+
dbname=config["database"],
|
|
39
|
+
user=config["user"],
|
|
40
|
+
password=config["password"],
|
|
41
|
+
host=config["host"],
|
|
42
|
+
)
|
|
43
|
+
# check and log if the connection is successful
|
|
44
|
+
if connection.closed == 0:
|
|
45
|
+
_logger.info("Connection to the database was successful")
|
|
46
|
+
return connection
|
|
47
|
+
else:
|
|
48
|
+
_logger.error("Connection to the database failed")
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
class EntityDao:
|
|
52
|
+
"""
|
|
53
|
+
This class is a dao for the postgres database which stores the filters and rules.
|
|
54
|
+
"""
|
|
55
|
+
def __init__(self, config_file_path: Optional[str] = None):
|
|
56
|
+
"""
|
|
57
|
+
Initializes the MetadataStore object with an empty dictionary to store metadata.
|
|
58
|
+
"""
|
|
59
|
+
if config_file_path is not None:
|
|
60
|
+
with open(config_file_path, 'r') as f:
|
|
61
|
+
config = json.load(f)
|
|
62
|
+
self._connection = create_connection(config if config_file_path is not None else None)
|
|
63
|
+
if self._connection is None:
|
|
64
|
+
raise RuntimeError("Failed to create a connection to the database")
|
|
65
|
+
|
|
66
|
+
def get_all_entities(self, entity_type: EntityEnum) -> dict[str, EntityBase]:
|
|
67
|
+
"""
|
|
68
|
+
Retrieves all entities of the specified type from the store.
|
|
69
|
+
"""
|
|
70
|
+
# if entity_type is filter, read from the filter table
|
|
71
|
+
# if entity_type is rule, read from the rule table
|
|
72
|
+
# and then return the result as a dictionary. Where the key maps to rule_name/filter_name and the value maps to rule_data/filter_data
|
|
73
|
+
if entity_type == EntityEnum.FILTER:
|
|
74
|
+
query = "SELECT * FROM filters"
|
|
75
|
+
elif entity_type == EntityEnum.RULE:
|
|
76
|
+
query = "SELECT * FROM rules"
|
|
77
|
+
else:
|
|
78
|
+
raise ValueError(f"Invalid entity type: {entity_type}")
|
|
79
|
+
with self._connection.cursor() as cursor:
|
|
80
|
+
cursor.execute(query)
|
|
81
|
+
rows = cursor.fetchall()
|
|
82
|
+
entities = {}
|
|
83
|
+
for row in rows:
|
|
84
|
+
entity_id = row[0]
|
|
85
|
+
entity_data = row[1]
|
|
86
|
+
try:
|
|
87
|
+
# convert the entity data to a Node object
|
|
88
|
+
node = pickle.loads(entity_data)
|
|
89
|
+
entities[entity_id] = node
|
|
90
|
+
except Exception as e:
|
|
91
|
+
_logger.error(f"Failed to unpickle entity {entity_id}: {e}")
|
|
92
|
+
# Skip this entity and continue
|
|
93
|
+
continue
|
|
94
|
+
return entities
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def store_entity(self, entity_enum: EntityEnum, entity: EntityBase) -> bool:
|
|
98
|
+
"""
|
|
99
|
+
Stores the entity and the expression in the store.
|
|
100
|
+
"""
|
|
101
|
+
print(f"Storing entity: {entity.name} of type: {entity_enum}")
|
|
102
|
+
# if entity_enum is filter, store in the filter table
|
|
103
|
+
# if entity_enum is rule, store in the rule table
|
|
104
|
+
# and then return True if the operation was successful, False otherwise
|
|
105
|
+
|
|
106
|
+
# Compare by value instead of direct equality
|
|
107
|
+
if entity_enum.value == EntityEnum.FILTER.value:
|
|
108
|
+
query = "INSERT INTO filters (filter_name, filter_data) VALUES (%s, %s)"
|
|
109
|
+
elif entity_enum.value == EntityEnum.RULE.value:
|
|
110
|
+
query = "INSERT INTO rules (rule_name, rule_data) VALUES (%s, %s)"
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError(f"Invalid entity type: {entity_enum}")
|
|
113
|
+
|
|
114
|
+
with self._connection.cursor() as cursor:
|
|
115
|
+
try:
|
|
116
|
+
# there shouldnt be a need to store the expression tree in the database
|
|
117
|
+
# todo: change it later
|
|
118
|
+
cursor.execute(query, (entity.name, pickle.dumps(entity)))
|
|
119
|
+
self._connection.commit()
|
|
120
|
+
return True
|
|
121
|
+
except Exception as e:
|
|
122
|
+
_logger.error(f"Failed to store entity: {e}")
|
|
123
|
+
self._connection.rollback()
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def entity_exists(self, entity_enum: EntityEnum, entity_id: str) -> bool:
|
|
128
|
+
"""
|
|
129
|
+
Checks if the entity exists in the store.
|
|
130
|
+
"""
|
|
131
|
+
# if entity_enum is filter, check in the filter table
|
|
132
|
+
# if entity_enum is rule, check in the rule table
|
|
133
|
+
# and then return True if the entity exists, False otherwise
|
|
134
|
+
if entity_enum.value == EntityEnum.FILTER.value:
|
|
135
|
+
query = "SELECT * FROM filters WHERE filter_name = %s"
|
|
136
|
+
elif entity_enum.value == EntityEnum.RULE.value:
|
|
137
|
+
query = "SELECT * FROM rules WHERE rule_name = %s"
|
|
138
|
+
else:
|
|
139
|
+
raise ValueError(f"Invalid entity type: {entity_enum}")
|
|
140
|
+
with self._connection.cursor() as cursor:
|
|
141
|
+
cursor.execute(query, (entity_id,))
|
|
142
|
+
rows = cursor.fetchall()
|
|
143
|
+
if len(rows) > 0:
|
|
144
|
+
return True
|
|
145
|
+
else:
|
|
146
|
+
return False
|
|
147
|
+
# close the connection
|
|
148
|
+
|
|
149
|
+
def delete_entity(self, entity_enum: EntityEnum, entity_id: str) -> bool:
|
|
150
|
+
"""
|
|
151
|
+
Deletes the entity from the store.
|
|
152
|
+
"""
|
|
153
|
+
# if entity_enum is filter, delete from the filter table
|
|
154
|
+
# if entity_enum is rule, delete from the rule table
|
|
155
|
+
# and then return True if the operation was successful, False otherwise
|
|
156
|
+
if entity_enum == EntityEnum.FILTER:
|
|
157
|
+
query = "DELETE FROM filters WHERE filter_name = %s"
|
|
158
|
+
elif entity_enum == EntityEnum.RULE:
|
|
159
|
+
query = "DELETE FROM rules WHERE rule_name = %s"
|
|
160
|
+
else:
|
|
161
|
+
raise ValueError(f"Invalid entity type: {entity_enum}")
|
|
162
|
+
|
|
163
|
+
with self._connection.cursor() as cursor:
|
|
164
|
+
try:
|
|
165
|
+
cursor.execute(query, (entity_id,))
|
|
166
|
+
self._connection.commit()
|
|
167
|
+
return True
|
|
168
|
+
except Exception as e:
|
|
169
|
+
_logger.error(f"Failed to delete entity: {e}")
|
|
170
|
+
self._connection.rollback()
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
# create destructor for EntityDao class to close the connection
|
|
174
|
+
def __del__(self):
|
|
175
|
+
if self._connection is not None:
|
|
176
|
+
self._connection.close()
|
|
177
|
+
_logger.info("Connection to the database closed")
|
|
178
|
+
else:
|
|
179
|
+
_logger.error("Connection to the database was not established")
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the Filter class, which represents a filter in the rule engine.
|
|
3
|
+
Each filter is a single condition that can be evaluated against a given data.
|
|
4
|
+
Filters can be combined with binary operators to create complex rules.
|
|
5
|
+
Example of unary filter: !x: which means !(bool(x))
|
|
6
|
+
Example of binary filter: x > 5 and y < 10: which means (x > 5) and (y < 10)
|
|
7
|
+
The filter operands are a set of variables from financial metadata, which are used to evaluate the filter. The various operands from the metadata are used to define various aspects of a financial transaction.
|
|
8
|
+
The Filter class has following methods:
|
|
9
|
+
-> validate: validates whether the operands of the filter are valid. The operands of filter should be either present in our finance metadata or filters database if the filer is composed of more leaf level filters . It queries the underlying store to check if filter operands exist either in the financial metadata or the filters database.
|
|
10
|
+
-> evaluate: evaluates the filter against the given data. The evaluation is done by evaluating each operand and combining the results using the operator.
|
|
11
|
+
-> __init__: initializes the Filter object with a string representing the expression string of the filter. The expression string is parsed to create a expression tree. The nodes of the tree are created using the Filter class. The tree is then used to evaluate the filter.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from rule_engine_core.parser import Node, OPERATOR_SET
|
|
15
|
+
from rule_engine_core.metadata_store import MetadataStore
|
|
16
|
+
from rule_engine_core.entity_dao import EntityDao
|
|
17
|
+
from rule_engine_core.entity_types import EntityEnum, EntityNode
|
|
18
|
+
from rule_engine_core.entity_base import EntityBase
|
|
19
|
+
import logging
|
|
20
|
+
|
|
21
|
+
_logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
class Filter(EntityBase):
|
|
24
|
+
"""
|
|
25
|
+
This class represents a filter in the rule engine.
|
|
26
|
+
Each filter is a single condition that can be evaluated against a given data.
|
|
27
|
+
Filters can be combined with binary operators to create complex rules.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, filter_name: str, expression: str, metadata_store: MetadataStore, entity_dao: EntityDao):
|
|
31
|
+
"""
|
|
32
|
+
Initializes the Filter object with a string representing the expression string of the filter.
|
|
33
|
+
The expression string is parsed to create a expression tree. The nodes of the tree are created using the Filter class. The tree is then used to evaluate the filter.
|
|
34
|
+
"""
|
|
35
|
+
super().__init__(expression, EntityEnum.FILTER, filter_name)
|
|
36
|
+
self.filter_name = filter_name
|
|
37
|
+
self._metadata_store = metadata_store
|
|
38
|
+
self._entity_dao = entity_dao
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _validate_node(self, node: Node) -> bool:
|
|
42
|
+
"""
|
|
43
|
+
Recursively validates the operands of the filter.
|
|
44
|
+
"""
|
|
45
|
+
if node is None:
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
if node.value in OPERATOR_SET:
|
|
49
|
+
return self._validate_node(node.left) and self._validate_node(node.right)
|
|
50
|
+
|
|
51
|
+
# make a debug log to check if the node is a number or a field in the metadata store or an existing filter in the database
|
|
52
|
+
_logger.debug(f"Node: {node.value} is node a number {self._check_if_node_is_number(node.value)} or a field in the metadata store {self._metadata_store.does_field_exist(node.value)} or an existing filter in the database {self._entity_dao.entity_exists(EntityEnum.FILTER, node.value)}")
|
|
53
|
+
# check if node value is a number or a field in the metadata store or an existing filter in the database
|
|
54
|
+
return self._check_if_node_is_number(node.value) or \
|
|
55
|
+
self._metadata_store.does_field_exist(node.value) or \
|
|
56
|
+
self._entity_dao.entity_exists(EntityEnum.FILTER, node.value)
|
|
57
|
+
|
|
58
|
+
def _evaluate_node(self, node: EntityNode, pos_data: dict[str, str|int|float], entity_eval_cache: dict[EntityEnum, dict[str, int|float|bool]], entity_cache: dict[EntityEnum, dict[str, EntityBase]]) -> float|int|bool:
|
|
59
|
+
"""
|
|
60
|
+
Evaluates the node against the given data. And updates the cache with the result.
|
|
61
|
+
"""
|
|
62
|
+
_logger.info(f"Evaluating filter {self.filter_name} with value {node.node.value} and node type {node.entity_type}")
|
|
63
|
+
# check is filter_name is cached in entity_eval_cache
|
|
64
|
+
if self.filter_name in entity_eval_cache[EntityEnum.FILTER]:
|
|
65
|
+
return entity_eval_cache[EntityEnum.FILTER][self.filter_name]
|
|
66
|
+
if node.entity_type != EntityEnum.FILTER or node.node is None:
|
|
67
|
+
raise ValueError(f"Invalid node type: {node.entity_type} or invalide node: {node.node}")
|
|
68
|
+
|
|
69
|
+
if node.node.value not in OPERATOR_SET:
|
|
70
|
+
raise ValueError(f"Invalid operator: {node.node.value}")
|
|
71
|
+
|
|
72
|
+
def evaluate_node_or_leaf(node_ident: str) -> float|int|bool:
|
|
73
|
+
# First check if the node_ident is a number (int/float)
|
|
74
|
+
# then check is it is a metadata field and evaluate the value
|
|
75
|
+
# from pos_data
|
|
76
|
+
# then finally check if it is a filter in the database
|
|
77
|
+
try:
|
|
78
|
+
return int(node_ident)
|
|
79
|
+
except ValueError:
|
|
80
|
+
try:
|
|
81
|
+
return float(node_ident)
|
|
82
|
+
except ValueError:
|
|
83
|
+
if self._metadata_store.does_field_exist(node_ident):
|
|
84
|
+
return pos_data[node_ident]
|
|
85
|
+
else:
|
|
86
|
+
# if not found in metadata store, evaluate the filter from struct
|
|
87
|
+
# since the filter has not been evaluated yet
|
|
88
|
+
if entity_cache[EntityEnum.FILTER].get(node_ident) is None:
|
|
89
|
+
err_msg = f"Filter {node_ident} is not a valid entity for this rule evaluation cycle"
|
|
90
|
+
_logger.error(err_msg)
|
|
91
|
+
# propagata the error to the rule evaluation
|
|
92
|
+
raise ValueError(err_msg)
|
|
93
|
+
if entity_cache[EntityEnum.FILTER].get(node_ident).node_type is not EntityEnum.FILTER:
|
|
94
|
+
err_msg = f"Filter {node_ident}: has bad expression tree"
|
|
95
|
+
_logger.error(err_msg)
|
|
96
|
+
# propagata the error to the rule evaluation
|
|
97
|
+
raise ValueError(err_msg)
|
|
98
|
+
return entity_cache[EntityEnum.FILTER][node_ident].evaluate(pos_data, entity_eval_cache, entity_cache)
|
|
99
|
+
|
|
100
|
+
match node.node.value:
|
|
101
|
+
case "!":
|
|
102
|
+
# Unary not operator
|
|
103
|
+
result = not evaluate_node_or_leaf(node.node.left.value)
|
|
104
|
+
case "&":
|
|
105
|
+
# Binary and operator
|
|
106
|
+
result = evaluate_node_or_leaf(node.node.left.value) and \
|
|
107
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
108
|
+
case "|":
|
|
109
|
+
# Binary or operator
|
|
110
|
+
result = evaluate_node_or_leaf(node.node.left.value) or \
|
|
111
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
112
|
+
case ">":
|
|
113
|
+
# bonary greater than operator
|
|
114
|
+
result = evaluate_node_or_leaf(node.node.left.value) > \
|
|
115
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
116
|
+
case "<":
|
|
117
|
+
# Binary less than operator
|
|
118
|
+
result = evaluate_node_or_leaf(node.node.left.value) < \
|
|
119
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
120
|
+
case ">=":
|
|
121
|
+
# Binary greater than or equal to operator
|
|
122
|
+
result = evaluate_node_or_leaf(node.node.left.value) >= \
|
|
123
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
124
|
+
case "<=":
|
|
125
|
+
# Binary less than or equal to operator
|
|
126
|
+
result = evaluate_node_or_leaf(node.node.left.value) <= \
|
|
127
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
128
|
+
case "+":
|
|
129
|
+
# Binary addition operator
|
|
130
|
+
result = evaluate_node_or_leaf(node.node.left.value) + \
|
|
131
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
132
|
+
case "-":
|
|
133
|
+
# Binary subtraction operator
|
|
134
|
+
result = evaluate_node_or_leaf(node.node.left.value) - \
|
|
135
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
136
|
+
case "*":
|
|
137
|
+
# Binary multiplication operator
|
|
138
|
+
result = evaluate_node_or_leaf(node.node.left.value) * \
|
|
139
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
140
|
+
case "/":
|
|
141
|
+
# Binary division operator
|
|
142
|
+
result = evaluate_node_or_leaf(node.node.left.value) / \
|
|
143
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
144
|
+
entity_eval_cache[EntityEnum.FILTER][self.filter_name] = result
|
|
145
|
+
return result
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the MetadataStore class, which is responsible for retrieval of financial metadata.
|
|
3
|
+
The MetadataStore class provides methods for retrieval of metadata.
|
|
4
|
+
There should be no writes to this and this is expected to be a read-only store.
|
|
5
|
+
The MetadataStore class has the following methods:
|
|
6
|
+
-> get_attributes: Get the list of attributes available for the metadata.
|
|
7
|
+
-> get_all_metadata: retrieves all metadata from the store. The metadata is a list of dictionaries each defining a key value pair.
|
|
8
|
+
-> attribute_exist_for_field: checks if field exists in the store and if the attribute exists for the field.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
class MetadataStore:
|
|
14
|
+
def __init__(self, metadata_file_path: str):
|
|
15
|
+
"""
|
|
16
|
+
Initializes the MetadataStore object with an empty dictionary to store metadata.
|
|
17
|
+
"""
|
|
18
|
+
with open(metadata_file_path, 'r') as f:
|
|
19
|
+
self._metadata_store: dict[str, dict[str, str|int|float]] = json.load(f)
|
|
20
|
+
self._attributes = self._populate_attributes()
|
|
21
|
+
|
|
22
|
+
def _populate_attributes(self):
|
|
23
|
+
"""
|
|
24
|
+
Populates the attributes from the metadata store.
|
|
25
|
+
"""
|
|
26
|
+
attributes_set = set()
|
|
27
|
+
for _, entity_data in self._metadata_store.items():
|
|
28
|
+
for field in entity_data:
|
|
29
|
+
if field not in attributes_set:
|
|
30
|
+
attributes_set.add(field)
|
|
31
|
+
return attributes_set
|
|
32
|
+
|
|
33
|
+
def get_all_metadata(self) -> dict[str, dict[str, str|int|float]]:
|
|
34
|
+
"""
|
|
35
|
+
Retrieves all metadata from the store. The metadata is a list of dictionaries each defining a key value pair.
|
|
36
|
+
"""
|
|
37
|
+
return self._metadata_store
|
|
38
|
+
|
|
39
|
+
def does_field_exist(self, field: str) -> bool:
|
|
40
|
+
"""
|
|
41
|
+
Checks if field exists in the store.
|
|
42
|
+
"""
|
|
43
|
+
return field in self._metadata_store
|
|
44
|
+
|
|
45
|
+
def attribute_exist_for_field(self, field: str, attribute: str) -> bool:
|
|
46
|
+
"""
|
|
47
|
+
Checks if field exists in the store and if the attribute exists for the field.
|
|
48
|
+
"""
|
|
49
|
+
return field in self._metadata_store and attribute in self._metadata_store[field] and self._metadata_store[field][attribute] is not None
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the Parser class, which is responsible for parsing the expression string of a rule or filter.
|
|
3
|
+
The expression string is a string of operands and operators.
|
|
4
|
+
The Parser class has the staticmethod:
|
|
5
|
+
-> parse: parses the expression string and creates a prefix expression tree. It returns the root of the tree and raises RuntimeError if the expression is invalid.
|
|
6
|
+
"""
|
|
7
|
+
import logging
|
|
8
|
+
_logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
def _replace_words_with_symbols(expression_str):
|
|
11
|
+
"""
|
|
12
|
+
Replaces NOT by ! and AND by & and OR by | in the expression string.
|
|
13
|
+
"""
|
|
14
|
+
expression_str = expression_str.replace("NOT", "!")
|
|
15
|
+
expression_str = expression_str.replace("AND", "&")
|
|
16
|
+
expression_str = expression_str.replace("OR", "|")
|
|
17
|
+
return expression_str
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
|
|
21
|
+
class Node:
|
|
22
|
+
def __init__(self, value, left=None, right=None):
|
|
23
|
+
"""
|
|
24
|
+
Represents a node in the expression tree.
|
|
25
|
+
- value: a string representing an operator or operand.
|
|
26
|
+
- left: the left child (None if not applicable).
|
|
27
|
+
- right: the right child (None for leaves or unary operators).
|
|
28
|
+
"""
|
|
29
|
+
self.value: str = value
|
|
30
|
+
self.left: Node = left
|
|
31
|
+
self.right: Node = right
|
|
32
|
+
|
|
33
|
+
def __repr__(self):
|
|
34
|
+
if self.left is None and self.right is None:
|
|
35
|
+
return f"Node({self.value})"
|
|
36
|
+
if self.right is None:
|
|
37
|
+
return f"Node({self.value}, {self.left})"
|
|
38
|
+
return f"Node({self.value}, {self.left}, {self.right})"
|
|
39
|
+
|
|
40
|
+
OPERATOR_SET = {'+', '-', '*', '/', '&', '|', '!', '<', '<=', '>', '>='}
|
|
41
|
+
|
|
42
|
+
class Parser:
|
|
43
|
+
"""
|
|
44
|
+
A recursive descent parser for infix expressions that supports:
|
|
45
|
+
- Parentheses: ( and )
|
|
46
|
+
- Operands as numbers or identifiers (like abc)
|
|
47
|
+
- Operators:
|
|
48
|
+
* Unary: ! (logical not)
|
|
49
|
+
* Binary arithmetic: +, -, *, /
|
|
50
|
+
* Relational: <, <=, >, >=
|
|
51
|
+
* Bitwise: & (and), | (or)
|
|
52
|
+
|
|
53
|
+
Operator precedence (from highest to lowest):
|
|
54
|
+
1. Unary: !
|
|
55
|
+
2. Multiplicative: *, /
|
|
56
|
+
3. Additive: +, -
|
|
57
|
+
4. Comparison: <, <=, >, >=
|
|
58
|
+
5. Bitwise AND: &
|
|
59
|
+
6. Bitwise OR: |
|
|
60
|
+
"""
|
|
61
|
+
def __init__(self, text: str):
|
|
62
|
+
self.text = text
|
|
63
|
+
self.tokens = self.tokenize(text)
|
|
64
|
+
self.pos = 0
|
|
65
|
+
|
|
66
|
+
def tokenize(self, text: str):
|
|
67
|
+
# Define token specifications.
|
|
68
|
+
token_spec = [
|
|
69
|
+
('NUMBER', r'\d+(\.\d+)?'), # Integer or decimal number
|
|
70
|
+
('RELOP', r'(<=|>=|<|>)'), # Relational operators
|
|
71
|
+
('IDENT', r'[A-Za-z_]\w*'), # Identifiers (operands)
|
|
72
|
+
('OP', r'[+\-*/&|!]'), # Other operators
|
|
73
|
+
('LPAREN', r'\('), # Left Parenthesis
|
|
74
|
+
('RPAREN', r'\)'), # Right Parenthesis
|
|
75
|
+
('SKIP', r'\s+'), # Skip whitespace
|
|
76
|
+
('MISMATCH', r'.'), # Any other character
|
|
77
|
+
]
|
|
78
|
+
token_regex = '|'.join(f'(?P<{name}>{pattern})' for name, pattern in token_spec)
|
|
79
|
+
tokens = []
|
|
80
|
+
for mo in re.finditer(token_regex, text):
|
|
81
|
+
kind = mo.lastgroup
|
|
82
|
+
value = mo.group()
|
|
83
|
+
if kind == 'NUMBER':
|
|
84
|
+
tokens.append(('NUMBER', value))
|
|
85
|
+
elif kind == 'RELOP':
|
|
86
|
+
tokens.append(('RELOP', value))
|
|
87
|
+
elif kind == 'IDENT':
|
|
88
|
+
tokens.append(('IDENT', value))
|
|
89
|
+
elif kind == 'OP':
|
|
90
|
+
tokens.append(('OP', value))
|
|
91
|
+
elif kind == 'LPAREN':
|
|
92
|
+
tokens.append(('LPAREN', value))
|
|
93
|
+
elif kind == 'RPAREN':
|
|
94
|
+
tokens.append(('RPAREN', value))
|
|
95
|
+
elif kind == 'SKIP':
|
|
96
|
+
continue
|
|
97
|
+
else:
|
|
98
|
+
raise RuntimeError(f"Unexpected token: {value}")
|
|
99
|
+
return tokens
|
|
100
|
+
|
|
101
|
+
def peek(self):
|
|
102
|
+
if self.pos < len(self.tokens):
|
|
103
|
+
return self.tokens[self.pos]
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
def consume(self, expected_type=None, expected_value=None):
|
|
107
|
+
token = self.peek()
|
|
108
|
+
if token is None:
|
|
109
|
+
return None
|
|
110
|
+
if expected_type and token[0] != expected_type:
|
|
111
|
+
raise RuntimeError(f"Expected token type '{expected_type}', got '{token[0]}' with value '{token[1]}'")
|
|
112
|
+
if expected_value and token[1] != expected_value:
|
|
113
|
+
raise RuntimeError(f"Expected token value '{expected_value}', got '{token[1]}'")
|
|
114
|
+
self.pos += 1
|
|
115
|
+
return token
|
|
116
|
+
|
|
117
|
+
def parse_expression(self):
|
|
118
|
+
# Top-level rule: start with the lowest precedence, bitwise OR.
|
|
119
|
+
return self.parse_bitwise_or()
|
|
120
|
+
|
|
121
|
+
def parse_bitwise_or(self):
|
|
122
|
+
node = self.parse_bitwise_and()
|
|
123
|
+
while True:
|
|
124
|
+
token = self.peek()
|
|
125
|
+
if token and token[0] == 'OP' and token[1] == '|':
|
|
126
|
+
self.consume('OP', '|')
|
|
127
|
+
right = self.parse_bitwise_and()
|
|
128
|
+
node = Node('|', node, right)
|
|
129
|
+
else:
|
|
130
|
+
break
|
|
131
|
+
return node
|
|
132
|
+
|
|
133
|
+
def parse_bitwise_and(self):
|
|
134
|
+
node = self.parse_comparison()
|
|
135
|
+
while True:
|
|
136
|
+
token = self.peek()
|
|
137
|
+
if token and token[0] == 'OP' and token[1] == '&':
|
|
138
|
+
self.consume('OP', '&')
|
|
139
|
+
right = self.parse_comparison()
|
|
140
|
+
node = Node('&', node, right)
|
|
141
|
+
else:
|
|
142
|
+
break
|
|
143
|
+
return node
|
|
144
|
+
|
|
145
|
+
def parse_comparison(self):
|
|
146
|
+
node = self.parse_additive()
|
|
147
|
+
token = self.peek()
|
|
148
|
+
# Allow at most one comparison operator (non-associative)
|
|
149
|
+
if token and token[0] == 'RELOP':
|
|
150
|
+
op = token[1]
|
|
151
|
+
self.consume('RELOP', op)
|
|
152
|
+
right = self.parse_additive()
|
|
153
|
+
node = Node(op, node, right)
|
|
154
|
+
return node
|
|
155
|
+
|
|
156
|
+
def parse_additive(self):
|
|
157
|
+
node = self.parse_multiplicative()
|
|
158
|
+
while True:
|
|
159
|
+
token = self.peek()
|
|
160
|
+
if token and token[0] == 'OP' and token[1] in ('+', '-'):
|
|
161
|
+
op = token[1]
|
|
162
|
+
self.consume('OP', op)
|
|
163
|
+
right = self.parse_multiplicative()
|
|
164
|
+
node = Node(op, node, right)
|
|
165
|
+
else:
|
|
166
|
+
break
|
|
167
|
+
return node
|
|
168
|
+
|
|
169
|
+
def parse_multiplicative(self):
|
|
170
|
+
node = self.parse_unary()
|
|
171
|
+
while True:
|
|
172
|
+
token = self.peek()
|
|
173
|
+
if token and token[0] == 'OP' and token[1] in ('*', '/'):
|
|
174
|
+
op = token[1]
|
|
175
|
+
self.consume('OP', op)
|
|
176
|
+
right = self.parse_unary()
|
|
177
|
+
node = Node(op, node, right)
|
|
178
|
+
else:
|
|
179
|
+
break
|
|
180
|
+
return node
|
|
181
|
+
|
|
182
|
+
def parse_unary(self):
|
|
183
|
+
token = self.peek()
|
|
184
|
+
if token and token[0] == 'OP' and token[1] == '!':
|
|
185
|
+
self.consume('OP', '!')
|
|
186
|
+
operand = self.parse_unary()
|
|
187
|
+
return Node('!', operand)
|
|
188
|
+
return self.parse_primary()
|
|
189
|
+
|
|
190
|
+
def parse_primary(self):
|
|
191
|
+
token = self.peek()
|
|
192
|
+
if token is None:
|
|
193
|
+
raise RuntimeError("Unexpected end of expression")
|
|
194
|
+
if token[0] == 'NUMBER' or token[0] == 'IDENT':
|
|
195
|
+
self.consume(token[0])
|
|
196
|
+
return Node(token[1])
|
|
197
|
+
elif token[0] == 'LPAREN':
|
|
198
|
+
self.consume('LPAREN')
|
|
199
|
+
node = self.parse_expression()
|
|
200
|
+
if self.peek() is None or self.peek()[0] != 'RPAREN':
|
|
201
|
+
raise RuntimeError("Missing closing parenthesis")
|
|
202
|
+
self.consume('RPAREN')
|
|
203
|
+
return node
|
|
204
|
+
else:
|
|
205
|
+
raise RuntimeError(f"Unexpected token: {token}")
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def parse(expr: str) -> Node:
|
|
209
|
+
"""
|
|
210
|
+
Parses the given infix expression string and returns the root
|
|
211
|
+
of the generated expression tree.
|
|
212
|
+
Raises RuntimeError if the expression is invalid.
|
|
213
|
+
"""
|
|
214
|
+
expr = _replace_words_with_symbols(expr)
|
|
215
|
+
parser = Parser(expr)
|
|
216
|
+
node = parser.parse_expression()
|
|
217
|
+
if parser.pos != len(parser.tokens):
|
|
218
|
+
errMsg = f"Invalid expression: Extra tokens present. Parser pos{parser.pos} and tokens {parser.tokens}"
|
|
219
|
+
_logger.error(errMsg)
|
|
220
|
+
raise RuntimeError(errMsg)
|
|
221
|
+
return node
|
|
222
|
+
|
rule_engine_core/rule.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the Rule class, which represents a rule in the rule engine.
|
|
3
|
+
Each rule contains a combination of filters or existing rule.
|
|
4
|
+
Rules can be made in the following ways:
|
|
5
|
+
-> Rule with filters: A rule can be created with a combination of filters. Filters can be either combined with binary operators or can be used as a single filter optionally with a unary operators.
|
|
6
|
+
-> Rule with filters and existing rules: A rule can be created with a combination of filters and existing rules using binary operators.
|
|
7
|
+
-> Rule with existing rules: A rule can be created with a combination of existing rules using binary operators.
|
|
8
|
+
The Rule class has following methods:
|
|
9
|
+
-> validate: validates whether the operands of the rule are valid. The operands of rule being rules and filters. It queries the underlying store to check if filter and rule exist.
|
|
10
|
+
-> evaluate: evaluates the rule against the given data. The evaluation is done by evaluating each operand and combining the results using the operator.
|
|
11
|
+
-> __init__: initializes the Rule object with a string representing the expression string of the rule. The expression string is parsed to create a expression tree. The nodes of the tree are created using the Filter and Rule classes. The tree is then used to evaluate the rule.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from rule_engine_core.metadata_store import MetadataStore
|
|
15
|
+
from rule_engine_core.entity_dao import EntityDao
|
|
16
|
+
from rule_engine_core.entity_types import EntityEnum, EntityNode
|
|
17
|
+
from rule_engine_core.entity_base import EntityBase
|
|
18
|
+
from rule_engine_core.parser import Node, OPERATOR_SET
|
|
19
|
+
import logging
|
|
20
|
+
from rule_engine_core.filter import Filter
|
|
21
|
+
|
|
22
|
+
_logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
class Rule(EntityBase):
|
|
25
|
+
def __init__(self, rule_name: str, expression: str, metadata_store: MetadataStore, entity_dao: EntityDao):
|
|
26
|
+
"""
|
|
27
|
+
Initializes the Rule object with a string representing the expression string of the rule.
|
|
28
|
+
The expression string is parsed to create a expression tree. The nodes of the tree are created using the Filter and Rule classes. The tree is then used to evaluate the rule.
|
|
29
|
+
"""
|
|
30
|
+
super().__init__(expression, EntityEnum.RULE, rule_name)
|
|
31
|
+
self.rule_name = rule_name
|
|
32
|
+
self._metadata_store = metadata_store
|
|
33
|
+
self._entity_dao = entity_dao
|
|
34
|
+
|
|
35
|
+
def _validate_node(self, node: Node) -> bool:
|
|
36
|
+
# Recursively validates the operands of the rule.
|
|
37
|
+
if node is None:
|
|
38
|
+
return True
|
|
39
|
+
if node.value in OPERATOR_SET:
|
|
40
|
+
return self._validate_node(node.left) and self._validate_node(node.right)
|
|
41
|
+
|
|
42
|
+
# debug log if rule is a number or a field in the metadata store or an existing rule/filter in the database
|
|
43
|
+
_logger.debug(f"Node: {node.value} is node a number {self._check_if_node_is_number(node.value)} or a field in the metadata store {self._metadata_store.does_field_exist(node.value)} or an existing filter in the database {self._entity_dao.entity_exists(EntityEnum.FILTER, node.value)} or existing rule in the database {self._entity_dao.entity_exists(EntityEnum.RULE, node.value)}")
|
|
44
|
+
# todo: do we want the rules to have primary operands or just composed of rules and filters ?
|
|
45
|
+
# check if node value is a number or a field in the metadata store or an existing filter in the database
|
|
46
|
+
return self._check_if_node_is_number(node.value) or \
|
|
47
|
+
self._metadata_store.does_field_exist(node.value) or \
|
|
48
|
+
self._entity_dao.entity_exists(EntityEnum.FILTER, node.value) or \
|
|
49
|
+
self._entity_dao.entity_exists(EntityEnum.RULE, node.value)
|
|
50
|
+
|
|
51
|
+
def _evaluate_node(self, node: EntityNode, pos_data: dict[str, str|int|float], entity_eval_cache: dict[EntityEnum, dict[str, int|float|bool]], entity_cache: dict[EntityEnum, dict[str, EntityBase]]) -> float|int|bool:
|
|
52
|
+
_logger.info(f"Evaluating rule {self.rule_name} with value {node.node.value} and node type {node.entity_type}")
|
|
53
|
+
# Evaluates the node against the given data. And updates the cache with the result.
|
|
54
|
+
# check is rule_name is cached in entity_eval_cache
|
|
55
|
+
if self.rule_name in entity_eval_cache[EntityEnum.RULE]:
|
|
56
|
+
return entity_eval_cache[EntityEnum.RULE][self.rule_name]
|
|
57
|
+
if node.entity_type != EntityEnum.RULE or node.node is None:
|
|
58
|
+
raise ValueError(f"Invalid node type: {node.entity_type} or invalide node: {node.node}")
|
|
59
|
+
|
|
60
|
+
if node.node.value not in OPERATOR_SET:
|
|
61
|
+
raise ValueError(f"Invalid operator: {node.node.value}")
|
|
62
|
+
|
|
63
|
+
def evaluate_node_or_leaf(node_ident: str) -> float|int|bool:
|
|
64
|
+
try:
|
|
65
|
+
return int(node_ident)
|
|
66
|
+
except ValueError:
|
|
67
|
+
try:
|
|
68
|
+
return float(node_ident)
|
|
69
|
+
except ValueError:
|
|
70
|
+
if self._metadata_store.does_field_exist(node_ident):
|
|
71
|
+
return pos_data[node_ident]
|
|
72
|
+
else:
|
|
73
|
+
# if not found in metadata store, evaluate the filter from struct
|
|
74
|
+
# since the filter has not been evaluated yet
|
|
75
|
+
if entity_cache[EntityEnum.FILTER].get(node_ident, None) is None and \
|
|
76
|
+
entity_cache[EntityEnum.RULE].get(node_ident, None) is None:
|
|
77
|
+
err_msg = f"Filter {node_ident} is not a valid entity for this rule evaluation cycle. FIlter keys {entity_cache[EntityEnum.FILTER].keys()} Rule keys {entity_cache[EntityEnum.RULE].keys()}"
|
|
78
|
+
_logger.error(err_msg)
|
|
79
|
+
# propagata the error to the rule evaluation
|
|
80
|
+
raise ValueError(err_msg)
|
|
81
|
+
if (node_ident in entity_cache[EntityEnum.FILTER] and entity_cache[EntityEnum.FILTER].get(node_ident).node_type is not EntityEnum.FILTER) or (node_ident in entity_cache[EntityEnum.RULE] and entity_cache[EntityEnum.RULE].get(node_ident).node_type is not EntityEnum.RULE):
|
|
82
|
+
err_msg = f"Filter {node_ident}: has bad expression tree"
|
|
83
|
+
_logger.error(err_msg)
|
|
84
|
+
# propagata the error to the rule evaluation
|
|
85
|
+
raise ValueError(err_msg)
|
|
86
|
+
_logger.debug(f"Evaluating child node {node_ident} of type {EntityEnum.FILTER.value if node_ident in entity_cache[EntityEnum.FILTER] else EntityEnum.RULE.value}")
|
|
87
|
+
# import pdb; pdb.set_trace()
|
|
88
|
+
return entity_cache[EntityEnum.FILTER][node_ident].evaluate(pos_data, entity_eval_cache, entity_cache) if entity_cache[EntityEnum.FILTER][node_ident].node_type == EntityEnum.FILTER else \
|
|
89
|
+
entity_cache[EntityEnum.RULE][node_ident].evaluate(pos_data, entity_eval_cache, entity_cache)
|
|
90
|
+
|
|
91
|
+
match node.node.value:
|
|
92
|
+
case "!":
|
|
93
|
+
# Unary not operator
|
|
94
|
+
result = not evaluate_node_or_leaf(node.node.left.value)
|
|
95
|
+
case "&":
|
|
96
|
+
# Binary and operator
|
|
97
|
+
result = evaluate_node_or_leaf(node.node.left.value) and \
|
|
98
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
99
|
+
case "|":
|
|
100
|
+
# Binary or operator
|
|
101
|
+
result = evaluate_node_or_leaf(node.node.left.value) or \
|
|
102
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
103
|
+
case ">":
|
|
104
|
+
# bonary greater than operator
|
|
105
|
+
result = evaluate_node_or_leaf(node.node.left.value) > \
|
|
106
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
107
|
+
case "<":
|
|
108
|
+
# Binary less than operator
|
|
109
|
+
result = evaluate_node_or_leaf(node.node.left.value) < \
|
|
110
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
111
|
+
case ">=":
|
|
112
|
+
# Binary greater than or equal to operator
|
|
113
|
+
result = evaluate_node_or_leaf(node.node.left.value) >= \
|
|
114
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
115
|
+
case "<=":
|
|
116
|
+
# Binary less than or equal to operator
|
|
117
|
+
result = evaluate_node_or_leaf(node.node.left.value) <= \
|
|
118
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
119
|
+
case "+":
|
|
120
|
+
# Binary addition operator
|
|
121
|
+
result = evaluate_node_or_leaf(node.node.left.value) + \
|
|
122
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
123
|
+
case "-":
|
|
124
|
+
# Binary subtraction operator
|
|
125
|
+
result = evaluate_node_or_leaf(node.node.left.value) - \
|
|
126
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
127
|
+
case "*":
|
|
128
|
+
# Binary multiplication operator
|
|
129
|
+
result = evaluate_node_or_leaf(node.node.left.value) * \
|
|
130
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
131
|
+
case "/":
|
|
132
|
+
# Binary division operator
|
|
133
|
+
result = evaluate_node_or_leaf(node.node.left.value) / \
|
|
134
|
+
evaluate_node_or_leaf(node.node.right.value)
|
|
135
|
+
entity_eval_cache[EntityEnum.RULE][self.rule_name] = result
|
|
136
|
+
return result
|
|
137
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rule-engine-core
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A rule engine core library for Python.
|
|
5
|
+
Home-page: https://github.com/temp-noob/rule-engine
|
|
6
|
+
Author: Mohit Tripathi
|
|
7
|
+
Author-email: tripathimohit051@gmail.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: psycopg2-binary
|
|
14
|
+
Dynamic: author
|
|
15
|
+
Dynamic: author-email
|
|
16
|
+
Dynamic: classifier
|
|
17
|
+
Dynamic: description
|
|
18
|
+
Dynamic: description-content-type
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
Dynamic: requires-dist
|
|
21
|
+
Dynamic: requires-python
|
|
22
|
+
Dynamic: summary
|
|
23
|
+
|
|
24
|
+
## Build
|
|
25
|
+
-> create venv and activate
|
|
26
|
+
(install meta)
|
|
27
|
+
-> pip install steuptools wheel twine
|
|
28
|
+
(build package)
|
|
29
|
+
-> python setup.py sdist bdist_wheel
|
|
30
|
+
-> pip install dist/rule-engine-core-0.1.0-py3-none-any.whl
|
|
31
|
+
(Publish to pypi)
|
|
32
|
+
-> pip install twine
|
|
33
|
+
-> twine upload dist/*
|
|
34
|
+
|
|
35
|
+
### Docker
|
|
36
|
+
-> `psql --username=$POSTGRES_USER -d $POSTGRES_DB`
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
rule_engine_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
rule_engine_core/entity_base.py,sha256=fBxO3o5I6lVfAmhABEUtrnNw4OQyMK0r3lLDFLP-qY4,3569
|
|
3
|
+
rule_engine_core/entity_dao.py,sha256=luDAqOUfk_A9tt5M9vRxAgNX78tqO8YYsiLWAcopGk4,7457
|
|
4
|
+
rule_engine_core/entity_types.py,sha256=KQ5x3NsVy1HYukWJvPiSsMe-BgDgAUwz76U3IqEAXPs,271
|
|
5
|
+
rule_engine_core/filter.py,sha256=4bcL2W2UWby_L_5zN-jcsYD3g590mL74ksfFR1O9MfI,8647
|
|
6
|
+
rule_engine_core/metadata_store.py,sha256=d-i-7RAN-DOCSpwkARDWoM4OPgJDNDCqH1TXz8bSWj8,2148
|
|
7
|
+
rule_engine_core/parser.py,sha256=FLBXlE_72bmMCrjlrQnOgqDxI5HO4E0aeIP8p-rpQS8,8069
|
|
8
|
+
rule_engine_core/rule.py,sha256=pnKRm3GxLrumm0byKCQcXH3YwT6OAGtbSDBjm6uCbjQ,9160
|
|
9
|
+
rule_engine_core-0.0.1.dist-info/METADATA,sha256=HttQ90_OcKDZ60QRfaN96sgLWxz4Ilg16_56D3M3YTA,992
|
|
10
|
+
rule_engine_core-0.0.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
11
|
+
rule_engine_core-0.0.1.dist-info/top_level.txt,sha256=cAKgKEaQs3beemyeE5dLxLJ37qvZTVXc5gz0yuP2KrA,17
|
|
12
|
+
rule_engine_core-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rule_engine_core
|