From 0704216070256387c1f1658e7484aaf2409b4595 Mon Sep 17 00:00:00 2001 From: Hugh Dickinson Date: Fri, 20 May 2022 10:29:22 +0100 Subject: [PATCH 1/3] Added Python to Caesar rule generator classes. --- panoptes_client/python_rule_generator.py | 149 +++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 panoptes_client/python_rule_generator.py diff --git a/panoptes_client/python_rule_generator.py b/panoptes_client/python_rule_generator.py new file mode 100644 index 00000000..2a2d4c4b --- /dev/null +++ b/panoptes_client/python_rule_generator.py @@ -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(node.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 From 2c4be90a430314840e111c4535a637d7c7bb08d3 Mon Sep 17 00:00:00 2001 From: Hugh Dickinson Date: Fri, 20 May 2022 10:43:04 +0100 Subject: [PATCH 2/3] Enabled specification of rules in Python syntax. --- panoptes_client/caesar.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/panoptes_client/caesar.py b/panoptes_client/caesar.py index 77af720c..f419a18e 100644 --- a/panoptes_client/caesar.py +++ b/panoptes_client/caesar.py @@ -1,4 +1,5 @@ from panoptes_client.panoptes import Panoptes +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, @@ -136,7 +138,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). @@ -152,8 +154,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 @@ -205,3 +211,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) From 63cab47dd1faca4108b5a18973d169b865b8993b Mon Sep 17 00:00:00 2001 From: Hugh Dickinson Date: Fri, 20 May 2022 10:54:31 +0100 Subject: [PATCH 3/3] Bug fix. --- panoptes_client/python_rule_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/panoptes_client/python_rule_generator.py b/panoptes_client/python_rule_generator.py index 2a2d4c4b..df302df9 100644 --- a/panoptes_client/python_rule_generator.py +++ b/panoptes_client/python_rule_generator.py @@ -41,7 +41,7 @@ def compare_impl(self, node): op = node.ops.pop(0) left = node.left node.left = node.comparators.pop(0) - return [self.visit(op), self.visit(node.left)] + [self.visit(node)] + return [self.visit(op), self.visit(left)] + [self.visit(node)] return [ self.visit(node.ops[0]), self.visit(node.left),