Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python syntax specification of caesar rules #275

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions panoptes_client/caesar.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from panoptes_client.panoptes import Panoptes, PanoptesAPIException
from panoptes_client.python_rule_generator import CaesarRuleGenerator


class Caesar(object):
@@ -11,6 +12,7 @@ class Caesar(object):
'subject': ['retire_subject', 'add_subject_to_set', 'add_to_collection', 'external'],
'user': ['promote_user']
}
CONDITION_STRING_FORMATS = ["caesar", "python"]

def __init__(
self,
@@ -162,7 +164,7 @@ def create_workflow_reducer(self, workflow_id, reducer_type, key, other_reducer_

return self.http_post(f'workflows/{workflow_id}/reducers', json=payload)[0]

def create_workflow_rule(self, workflow_id, rule_type, condition_string='[]'):
def create_workflow_rule(self, workflow_id, rule_type, condition_string='[]', condition_string_format="caesar"):
"""
Adds a Caesar rule for given workflow. Will return rule as a dict with 'id' if successful.
- **condition_string** is a string that represents a single operation (sometimes nested).
@@ -178,8 +180,12 @@ def create_workflow_rule(self, workflow_id, rule_type, condition_string='[]'):
caesar.create_workflow_rule(workflow.id, 'subject','["gte", ["lookup", "complete.0", 0], ["const", 3]]')

"""

self.validate_condition_string_format(condition_string_format)
self.validate_rule_type(rule_type)

if condition_string_format == "python":
condition_string = self.convert_python_rule(condition_string, workflow_id)

payload = {
f'{rule_type}_rule': {
'condition_string': condition_string
@@ -231,3 +237,12 @@ def validate_extractor_type(self, extractor_type):
def validate_action(self, rule_type, action):
if action not in self.RULE_TO_ACTION_TYPES[rule_type]:
raise ValueError('Invalid action for rule type')

def validate_condition_string_format(self, condition_string_format):
if condition_string_format not in self.CONDITION_STRING_FORMATS:
raise ValueError('Invalid condition string format')

def convert_python_rule(self, rule, workflow_id):
rule_generator = CaesarRuleGenerator(rule)
valid_reducers = self.get_workflow_reducers(workflow_id=workflow_id)
return rule_generator(valid_reducers)
149 changes: 149 additions & 0 deletions panoptes_client/python_rule_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import ast


class CaesarRuleGenerator:
def __init__(self, rule):
self.tree = ast.parse(rule)

def __call__(self, reducer_names):
visitor = CaesarRuleGenParser(reducer_names=reducer_names)
try:
visitor.visit(self.tree)
except ValueError as e:
print("Error!", e)
return
return visitor.report()[0]


class CaesarRuleGenParser(ast.NodeVisitor):
def __init__(self, reducer_names=None):
super().__init__()
self.rule_components = []
self.sub_expr_level = 0
self.reducers = reducer_names

def binop_impl(self, node):
# left, op, right
return [self.visit(node.op), self.visit(node.left), self.visit(node.right)]

def visit_BinOp(self, node):
if not self.sub_expr_level:
# only append top level operations
self.sub_expr_level += 1
self.rule_components.append(self.binop_impl(node))
self.sub_expr_level -= 1
else:
return self.binop_impl(node)

def compare_impl(self, node):
# left, ops, comparators
if len(node.ops) > 1:
op = node.ops.pop(0)
left = node.left
node.left = node.comparators.pop(0)
return [self.visit(op), self.visit(left)] + [self.visit(node)]
return [
self.visit(node.ops[0]),
self.visit(node.left),
self.visit(node.comparators[0]),
]

def visit_Compare(self, node):
if not self.sub_expr_level:
# only append top level operations
self.sub_expr_level += 1
self.rule_components.append(self.compare_impl(node))
self.sub_expr_level -= 1
else:
return self.compare_impl(node)

def boolop_impl(self, node):
# op, values
return [self.visit(node.op)] + [self.visit(value) for value in node.values]

def visit_BoolOp(self, node):
if not self.sub_expr_level:
# only append top level operations
self.sub_expr_level += 1
self.rule_components.append(self.boolop_impl(node))
self.sub_expr_level -= 1
return self.boolop_impl(node)

def bad_name_error(self):
return (
"Lookup names must start with 'subject'"
+ " or the name of a registered reducer.\n"
+ " Registered reducers are:\n"
+ "\n".join(self.reducers)
)

def visit_Subscript(self, node):
# Lookups without a default
lookup_category = node.value.id
lookup_attribute = node.slice.value
if lookup_category == "subject":
return ["lookup", f"{lookup_category}.{lookup_attribute}"]
if self.reducers is None or lookup_category not in self.reducers:
raise ValueError(self.bad_name_error())
return ["lookup", f"{lookup_category}.{lookup_attribute}"]

def bad_lookup_def_error(self):
return "Bad lookup definition."

def visit_List(self, node):
# Lookups with a default
criterion = (
len(node.elts) == 2
and type(node.elts[0]) == ast.Subscript
and type(node.elts[1]) == ast.Constant
)
if not criterion:
raise ValueError(self.bad_lookup_def_error())

lookup = self.visit(node.elts[0]) + [self.visit(node.elts[1])]
return lookup

def const_type_handler(self, value):
if type(value) == bool:
return "true" if value else "false"
return value

def visit_Constant(self, node):
# value, kind
return ["const", self.const_type_handler(node.value)]

def visit_Add(self, node):
return "+"

def visit_Sub(self, node):
return "-"

def visit_Mult(self, node):
return "*"

def visit_Div(self, node):
return "/"

def visit_And(self, node):
return "and"

def visit_Or(self, node):
return "or"

def visit_Eq(self, node):
return "eq"

def visit_GtE(self, node):
return "gte"

def visit_LtE(self, node):
return "lte"

def visit_Gt(self, node):
return "gt"

def visit_Lt(self, node):
return "lt"

def report(self):
return self.rule_components