Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I create a rules engine without using eval() or exec()?

I have a simple rules/conditions table in my database which is used to generate alerts for one of our systems. I want to create a rules engine or a domain specific language.

A simple rule stored in this table would be..(omitting the relationships here)

if temp > 40 send email

Please note there would be many more such rules. A script runs once daily to evaluate these rules and perform the necessary actions. At the beginning, there was only one rule, so we had the script in place to only support that rule. However we now need to make it more scalable to support different conditions/rules. I have looked into rules engines , but I hope to achieve this in some simple pythonic way. At the moment, I have only come up with eval/exec and I know that is not the most recommended approach. So, what would be the best way to accomplish this??

( The rules are stored as data in database so each object like "temperature", condition like ">/=..etc" , value like "40,50..etc" and action like "email, sms, etc.." are stored in the database, i retrieve this to form the condition...if temp > 50 send email, that was my idea to then use exec or eval on them to make it live code..but not sure if this is the right approach )

like image 879
Angela Avatar asked Jan 20 '26 22:01

Angela


2 Answers

Well, if what you want to do is send emails then use the email module.

If I were you, I would write a simple Python script which processes a bunch of rules, probably just written as simple Python statements in a separate file, then send the emails / sms / ... for those rules that require an action to be performed.

You can make that run once a day (or whatever) using a service such as cron

For example, if your rules look like this:

# Rule file: rules.py

def rule1():
    if db.getAllUsers().contains("admin"): 
        return ('email', 'no admin user in db')
    else:
        return None, None

def rule2():
    if temp > 100.0: 
        return ('sms', 'too hot in greenhouse')
    else:
        return (None, None)

...

rules = [rule1, rule2, ....]

then your processing script might look like this:

# Script file: engine.py

import rules
import email
...

def send_email(message, receiver):
    # function that sends an email...

def send_sms(message, receiver):
    # function that sends an sms...

actions = {'email':send_email, 'sms':send_sms, ...}    

if __name__ == '__main__':

    # Declare receiver here...

    for rule in rules.rules:
        # Does the rule return a do-able action?
        # To be really paranoid we might wrap this in a try/finally
        # in case the rules themselves have any side effects,
        # or they don't all return 2-tuples.
        act, message = rule()
        if act in actions:
            # perform the action
            actions[rule()](message, receiver) 

Undoubtedly there are other ways to do this, such as creating a Pythonic DSL with which to write the rules.

like image 85
snim2 Avatar answered Jan 22 '26 10:01

snim2


There are several ways to achieve this. The other answers are valuable, and I'd like to add two techniques.

  • Provided you can rewrite the table juste create each rules as a pickled function that you can deserialize when needed
  • Write a big dictionary with rules as a key, and a function a a value. If you got 100 max rules, this is manageable. Just make sure you make very flexible functions using *args and **kwargs.

Example with pickle:

First, make a function that is flexible with its input.

def greater_than(value, *args, **kwargs):
    return all(value > i for i in args)

Then pickle it:

>>> import pickle
>>> rule = pickle.dumps(greater_than)
>>> rule # store this in DB
'ctest\ngreater_than\np0\n.'

Then when you need to get you business rule back:

>>> func = pickle.loads(rule) # rule is the sring from DB
>>> func(5, 4, 3, 1)
True
>>> func(5, 6) 
False

The purpose of having flexible input is that you can get an arbitrary number of parameters :

>>> args = [1, 2, 3]
>>> func(5, *args)
True 

Example with a dictionary

Store all functions in one big mapping:

def greater_than(value, *args, **kwargs):
    return all(value > i for i in args)

RULES = {
    'if x > y': greater_than
    'other rule': other_func,
    etc
}

Then when you need it:

   >>> func = RULES['if x > y']
   >>> func(5, 1)
   True
like image 27
e-satis Avatar answered Jan 22 '26 11:01

e-satis