3

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 )

octopusgrabbus
  • 10,555
  • 15
  • 68
  • 131
Angela
  • 1,671
  • 3
  • 19
  • 29
  • 1
    Why do you need a rules engine or a separate language? What's wrong with just writing rules in Python? You just import the Python "rule" module as first-class code. It's the most secure way to do business. What's wrong with it? – S.Lott Jan 12 '12 at 00:32
  • Maybe a duplicate of http://stackoverflow.com/questions/467738/implementing-a-rules-engine-in-python – Felix Yan Jan 12 '12 at 00:35
  • i have not looked into the rule module. i will take a look at it and see if it fits my requirements. if you could provide a basic snippet of code, it would be great. thanks. – Angela Jan 12 '12 at 03:53
  • When S.Lott says "import the 'rule' module" s/he means a module that you create containing your rules, there isn't a built in module called `rule` I guess because a "rule" is conceptually just a predicate: i.e. a function which returns `True` or `False`. – snim2 Jan 12 '12 at 11:45
  • There is not "rule" module. S.Lott just suggest that you write your rules in Python instead of storing them in a database which is a wise advice. Usually you want to listen to what's S.Lott has to say, despite his lack of diplomacy. – Bite code Jan 12 '12 at 11:46
  • (I updated the question with some extra information the O.P. posted as a comment bellow) – jsbueno Jan 12 '12 at 13:00

8 Answers8

3

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.

snim2
  • 4,004
  • 27
  • 44
  • "some of which may be neater". Doubtful. This is very, very neat. I'd strike the "some of which may be neater" part of the answer. This is quite nice otherwise. – S.Lott Jan 12 '12 at 00:59
  • That's kind of you. I was trying to think of a sensible way to remove the `lambda`s, but without putting a lot more logic in the rules I don't think it's sensibly possible. The other (quite possibly neater) way to do this would be to create a DSL out of a nice hierarchy of classes, like this: http://www.ibm.com/developerworks/library/l-cpdec/index.html – snim2 Jan 12 '12 at 01:01
  • Actually, the best way to remove the lambdas is to make them simple, first-class function `def` s. Or -- as you suggest -- callable objects. – S.Lott Jan 12 '12 at 01:22
  • Well, they could be `def`'d functions, but to my taste that's a bit long winded. YMMV of course. – snim2 Jan 12 '12 at 01:24
  • A sensible way to remove the `lambda` s remains `def`. Long-winded or not. It's still sensible. – S.Lott Jan 12 '12 at 01:28
  • Of course, perfectly sensible, just a matter of preferred style. – snim2 Jan 12 '12 at 01:30
  • thanks for your detailed answer, however i am not very clear on how it would fit my scenario, i have a database which stores the rules and yes the action could be email or sms or create a helpdesk ticket etc. so it is not limited to the email module. I am not very clear on how to map the operators to actual functions, like when i retrieve "<" or ">" how can I make it actually perform the operation without checking the value of the variable?? Hope I am making sense. – Angela Jan 12 '12 at 03:51
  • @Angela: Don't "store" the rules in a database. Store the the rules as functions. In a separate module that contains just the functions and nothing more. This separate module then becomes your database of rules. Don't invent a **new** language. Just use Python and be done with it. – S.Lott Jan 12 '12 at 10:56
  • @Angela it's a little more complicated if each rule can generate a different sort of action. I've modified the answer accordingly and incorporated @S.Lott's suggested about using functions rather than `lambda`s. – snim2 Jan 12 '12 at 12:00
  • @ S.Lott I have a web interface with which the end-user can add a new rule ( eg - if pressure > 50 send sms..etc/etc//) if required and which gets stored in the database. This rule shoudl then be dynamically picked up by the script which executes the current rules and the necesary action taken. Hence the requirement of storing the rules in the database. – Angela Jan 15 '12 at 12:46
  • @sinm2 Thanks for this piece of code.. I am going to try this out as well in addition to the other snippets and see which fits best – Angela Jan 15 '12 at 12:48
  • @sinm2 This is a very neat way of doing it. Had I not had the requirement of adding/editing rules via the front-end, I would have def. gone for this approach. – Angela Jan 16 '12 at 23:39
2

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
Bite code
  • 578,959
  • 113
  • 301
  • 329
  • Functions cannot be pickled - the rules could be stored as python functions in SQL, but they would have to be in plain text, and one would have to use exec/compile to make then "live" code anyway. I like the idea, however. – jsbueno Jan 12 '12 at 01:15
  • @e-stais could you provide a code example of how to go about this – Angela Jan 12 '12 at 04:19
  • @jsbueno 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 – Angela Jan 12 '12 at 04:22
  • i could redesign the database if required – Angela Jan 12 '12 at 04:26
  • @jsbuneo: functions can be pickle, they just need to be at the top level of the module: http://docs.python.org/library/pickle.html#what-can-be-pickled-and-unpickled – Bite code Jan 12 '12 at 11:44
  • @e-satis: ok, I stand corrected abotu pickling functions :-) . – jsbueno Jan 12 '12 at 12:40
  • @e-satis i have not used pickle before..still not an expert in python..would see if this fits the requirement – Angela Jan 15 '12 at 12:54
2

Since the "variable", "value" and comparison operator for eahc rule are on the database, you could write a Rule class that will take the apropriate parameters (operator, action, value, etc...), and yield a callable object that will receive all the relevant variables in the form of a dictionary, and take the proper registered action.

It would look like this, though you have to adapt it to proper get the parameters for your actions:

import operator

class Rule(object):
    def __init__(self, variable_name, op, value, action):
        op_dict = {"=": operator.eq,
                   ">": operator.gt,
                   "<": operator.lt,
                   #(...)
                  }
        action_dict = {"email": email_function,
                       "log": log_function,
                       # ...
                      }
        self.variable = variable_name
        self.op = op_dict[op]
        self.value = value
        self.action = action_dict[action]
    def __call__(self, value_dict, action_parameters, k_action_parameters):
        if self.op(value_dict[self.variable], self.value):
            return self.action(*action_parameters, **k_action_parameters)
        return False

rule = Rule("temp", ">", "email")
for result in query():
     rule(result, ())
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • thnks for this ..looks v much akin to what i need. will try it out and get back to u – Angela Jan 15 '12 at 12:53
  • I used your `Rule` class to build an interface for @Angela front-end template in my answer, I hope it's okey with you. – Rik Poggi Jan 15 '12 at 18:36
  • @jsbueno I have tried your code with a few edits, I am not sure if you can see my edits as I dont have enuf privileges to edit code. Basically I have added another dict to map the unit_of_measure for a rule to a calculate function and changed the actual callable for the Rule class accordingly. so to support diff. rules like if time > 24 hrs send email OR if temp > 40 celsius send email etc.. Is this a good design approach?? – Angela Jan 17 '12 at 02:52
  • @Angela: I think it sounds reasonable - if you want, paste the code somewhere else and I will take a look. – jsbueno Jan 17 '12 at 13:48
  • @jsbueno I have added another answer to explain the code. Could you please have a look at it? – Angela Jan 17 '12 at 23:43
0

You will want to take a look at NebriOS. Rules are written in pure Python rather than storing them in the DB. For example:

class hello(NebriOS):
    listens_to = ['temp']

    def check(self):
        return self.temp > 40

    def action(self):
        send_email ("angela@example.com","""
            Alert: Temp is now > 40! """)

I think there is a tun of value in using a rules engine for this application. To quote Martin Fowler in describing one:

A rules engine is all about providing an alternative computational model. Instead of the usual imperative model, commands in sequence with conditionals and loops, it provides a list of production rules. Each rule has a condition and an action – simplistically you can think of it as a bunch of if-then statements.

Taking a nonlinear approach to certain software projects helps make it more robust, accurate, and easy to understand. A small rule like "temp > 40 then do x" is much easier to write as a stand alone rule than it is to build a full application that uses the same rule. It doesn't require a linear chain to become evaluated. Once written, always enforced.

Another benefit is that if one rule breaks down, the rest of the rules operate as normal. This would generally cause much more commotion with traditional software (imperative models) techniques.

I released this app to the wild after building it for my current company. I think rules rule. Just my two cents.

Adam
  • 3,311
  • 1
  • 18
  • 9
0

Write a parser. See pyparsing.

Alternatively, make a table driven approach.

Nickle
  • 367
  • 3
  • 5
0

Why do you need to store rules into a database?
Can't you just store the data in the database and put the rules into a python module?

For example in a file rules.py you could:

  • Write a set of rules
  • Write a parser for your data that will apply the right rule for each paramenter

Then in your main you just have to pass the data to your rules.parser() and everything will be taken care of.

Edit: Seen your comment I've made a new answer.

Community
  • 1
  • 1
Rik Poggi
  • 28,332
  • 6
  • 65
  • 82
0

I think you mainly need two things:

  • a Rule class to interface your front-end template
  • pickle to store your rules in the db.

This is how your main could look like:

import pickle

# some data loaded from your DB
data = {'temp': 60, 'wind': 150}

# entry should be provided by your front-end template
entry = {'param_name': 'temp', 'test': Test(gt, 50), 'action': send_email}

rule = Rule(**entry)
to_store = pickle.dumps(rule)
# store 'to_store' into your DB

# Let's pretend to load the previously stored rule
stored = to_store
rule = pickle.loads(stored)
rule(data)

The idea is to get every information that you need to build a Rule from your template and then you just store that rule with pickle.

This may be a Rule implementation:

# =======
# Actions
# =======
#
# Any callable with no arguments is an Action
# (you may need to implement this)
#

def send_email():
    print('email sent')

# ==========
# Test class
# ==========
#
# Test class is a way to call your test function.
# The real test is in self.function
#

class Test:

    def __init__(self, function, *args):
        self.function = function
        self.args = args

    def __call__(self, parameter):
        return self.function(parameter, *self.args)

# ==============
# Test functions
# ==============
#
# These are the functions that are going to be executed
#

import operator

gt = operator.gt

def more_complex_test(*args):
    pass

# ==========
# Rule class
# ==========
#
# A Rule needs to know:
#  - the parameter to test
#  - the test to perform
#      - the action to execute
#

class Rule:

    def __init__(self, param_name, test, action):
        self.param_name = param_name
        self.test = test
        self.action = action

    def __call__(self, data):   # data is a dictionary {'temp': 60, ...}
        param_value = data[self.param_name]
        if self.test(param_value):
            return self.action()
        return False

Note: The two pieces of code above (if put togheter) can be executed. Give it a try!

Rik Poggi
  • 28,332
  • 6
  • 65
  • 82
0

I have used @jsbuenos code snippet and done a few changes to form this. Basicaly, I also need support to check the "unit of measure" for a rule to evaluate the condition. So to support diff. rules like if time > 24 hrs send email OR if temp > 40 celsius send email etc.(I would probably have other units later on as well..) I have included a new dict to map the unit of measure to a calculate function and accordingly changed the callable function for the class. Would this be the right approach to do it?

import operator

class Rule(object):
    def __init__(self, variable_name, op, value, action):
      op_dict = {"=": operator.eq,
               ">": operator.gt,
               "<": operator.lt,
               #(...)
              }
      action_dict = {"email": email_function,
                   "log": log_function,
                   # ...
                  }

      eval_condition = {"hrs" :  self.raise_timeexceeded_alert,
                    "celsius" : self.raise_tempexceeded_alert,
                    #}  

      self.variable = variable_name
      self.op = op_dict[op]
      self.value = value
      self.action = action_dict[action]
      self.uom = measure      
      self.raise_alert = eval_condition[measure]

   def __call__(self, actual_value, *action_parameters):
     if self.raise_alert(actual_value,self.op,self.uom,self.threshold):
        return self.action(*action_parameters)
    return False

   def raise_timeexceeded_alert(self,timevalue, op, uom, threshold):
    #calculate time difference with respect to local timezone and return true
    # if diff is 'operator' threshold
    localtime=pytz.timezone(TIMEZONE)
    ....
    ...
    return False


   def raise_tempexceeded_alert(self,timevalue, op, uom, threshold):
     #return True if temp. is 'operator' threshold
     ....
     .....
     return False


rule = Rule("time", ">=", "24" , "hrs", "email")
args = [contact_email,message]
rule("2011-12-11 12:06:03",*args)
Angela
  • 1,671
  • 3
  • 19
  • 29
  • It looks ok to have an extra dictionary - but let's check some issues: Where does the "measure" variable come from on the code above? Second - I think it be better to have methods called to simply do the unit conversion - even if some rules are more complicated and can't be tested in a simple `if`, certainly it is worth to have unit conversions in separate functions for that. – jsbueno Jan 18 '12 at 02:36
  • @jsbueno thnks for ur response, sorry forgot to include the measure variable as a param to init. I m not clear on your suggestion of methods for unit conversion could you pls edit your code to explain this ? – Angela Jan 18 '12 at 10:04