def execute(self, logic_row: LogicRow): # logic_row.log(f'Constraint BEGIN {str(self)} on {str(logic_row)}') if self._function is not None: value = self._function(row=logic_row.row, old_row=logic_row.old_row, logic_row=logic_row) else: value = self._as_condition(row=logic_row.row) if value: pass elif not value: row = logic_row.row msg = eval(f'f"""{self._error_msg}"""') from sqlalchemy import exc # exception = exc.DBAPIError(msg, None, None) # 'statement', 'params', and 'orig' logic_row.log(f'Constraint Failure: {msg}') ll = RuleBank() if ll.constraint_event: ll.constraint_event(message=msg, logic_row=logic_row, constraint=self) raise ConstraintException(msg) else: raise RuntimeError( f'Constraint did not return boolean: {str(self)}') logic_row.log_engine(f'Constraint END {str(self)} on {str(logic_row)}')
def __init__(self, derive: InstrumentedAttribute, from_parent: any): super(Copy, self).__init__(derive) if isinstance(from_parent, str): names = from_parent.split('.') self._from_parent_role = names[0] self._from_column = names[1] elif isinstance(from_parent, InstrumentedAttribute): self._from_column = from_parent.key table_class = from_parent.class_ parent_class_name = self.get_class_name(table_class) pass attrs = self._derive.parent.attrs found_attr = None for each_attr in attrs: if isinstance(each_attr, RelationshipProperty): each_parent_class_nodal_name = each_attr.entity.class_ each_parent_class_name = self.get_class_name( each_parent_class_nodal_name) if each_parent_class_name == parent_class_name: if found_attr is not None: raise Exception( "TODO / copy - disambiguate relationship") found_attr = each_attr if found_attr is None: raise Exception("Invalid 'as_sum_of' - not a reference to: " + self.table + " in " + self.__str__()) else: self._from_parent_role = found_attr.key else: pass rb = RuleBank() rb.deposit_rule(self)
def __init__(self, validate: object, error_msg: str = "Missing Parent", enable: bool = True): super(ParentCheck, self).__init__(validate) self._error_msg = error_msg self._enable = enable ll = RuleBank() ll.deposit_rule(self)
def __init__(self, validate: object, error_msg: str = "Unable to delete - existing Child rows", relationship: str = "*", action: str = 'nullify'): super(ParentCascade, self).__init__(validate) self._error_msg = error_msg if not isinstance(action, ParentCascadeAction): raise Exception("Invalid Action: " + str(action)) self._action = action self._relationship = relationship ll = RuleBank() ll.deposit_rule(self)
def validate(a_session: session, engine: Engine): """ Determine formula execution order based on "row.xx" references, (or raise exception if cycles detected). """ list_rules = "\n\nValidate Rule Bank" rules_bank = RuleBank() for each_key in rules_bank.orm_objects: validate_formula_dependencies(class_name=each_key) list_rules += rules_bank.__str__() print(list_rules) return True
def early_row_event_all_classes(self, verb_reason: str): """ if exists: rules_bank._early_row_event_all_classes(self) Args: verb_reason: debug string (not used) Returns: """ rules_bank = RuleBank() if rules_bank._early_row_event_all_classes is not None: # self.log("early_row_event_all_classes - " + verb_reason) rules_bank._early_row_event_all_classes(self)
def setup(a_session: session): """ Create the RuleBank Register before_flush listeners """ rules_bank = RuleBank() event.listen(a_session, "before_flush", before_flush) event.listen(a_session, "before_commit", before_commit) rules_bank.orm_objects = {} rules_bank._at = datetime.now() return rules_bank
def update_referenced_parent_attributes(self, dependencies: list): """ Used by Formulas and constraints log their dependence on parent attributes This sets RuleBank.TableRules[mapped_class].referring_children dependencies is a list But, can't do this now, because meta_contains_role_name = False So, do it on the fly in logic_row (which is an ugh) """ meta_contains_role_name = False if meta_contains_role_name is False: return else: meta_data = rule_bank_withdraw.get_meta_data() child_meta = meta_data.tables[self.table] parent_role_name = dependencies[1] foreign_keys = child_meta.foreign_keys for each_foreign_key in foreign_keys: # eg, OrderDetail has OrderHeader, Product each_parent_class_name = each_foreign_key.name each_parent_role_name = each_foreign_key.key if parent_role_name == each_parent_role_name: # eg, OrderHeader rule_bank = RuleBank() if each_parent_class_name not in rule_bank.orm_objects: self._tables[rule_bank] = TableRules() table_rules = self._tables[rule_bank] if table_rules.referring_children is None: table_rules.referring_children = {} if parent_role_name not in table_rules.referring_children: table_rules.referring_children[parent_role_name] = [] table_rules.referring_children.append(dependencies[2]) engine_logger.debug( prt("child parent dependency: " + dependencies[1])) break
def __init__(self, row: base, old_row: base, ins_upd_dlt: str, nest_level: int, a_session: session, row_sets: object): """ Note adds self to row_sets (if supplied), for later commit-phase logic """ self.session = a_session self.row = row # type(base) """ mapped row """ self.old_row = old_row """ old mapped row """ self.ins_upd_dlt = ins_upd_dlt self.ins_upd_dlt_initial = ins_upd_dlt # order inserted, then adjusted self.nest_level = nest_level self.reason = "?" # set by insert, update and delete """ if starts with cascade, triggers cascade processing """ self.row_sets = row_sets if row_sets is not None: # eg, for debug as in upd_order_shipped test row_sets.add_processed(logic_row=self) rb = RuleBank() self.rb = rb self.session = rb._session self.engine = rb._engine self.some_base = declarative_base() self.name = type(self.row).__name__ self.table_meta = None if self.row is not None: self.table_meta = row.metadata.tables[type(self.row).__name__] if self.engine is not None: # e.g, for testing legacy logic (no RuleBank) self.inspector = Inspector.from_engine(self.engine)
def aggregate_rules(child_logic_row: LogicRow) -> dict: """returns dict(<parent_role_name>, sum/count_rules[] for given child_table_name This requires we **invert** the RuleBank, to find sums that reference child_table_name, grouped by parent_role e.g., for child_logic_row "Order", we return ["Order", (Customer.balance, Customer.order_count...) ["Employee, (Employee.order_count)] """ result_role_rules_list = {} # dict of RoleRules child_mapper = object_mapper(child_logic_row.row) rule_bank = RuleBank() relationships = child_mapper.relationships for each_relationship in relationships: # eg, order has parents cust & emp, child orderdetail if each_relationship.direction == sqlalchemy.orm.interfaces.MANYTOONE: # cust, emp child_role_name = each_relationship.back_populates # eg, OrderList if child_role_name is None: child_role_name = child_mapper.class_.__name__ # default TODO design review parent_role_name = each_relationship.key # eg, Customer TODO design review parent_class_name = each_relationship.entity.class_.__name__ if parent_class_name in rule_bank._tables: parent_rules = rule_bank._tables[parent_class_name].rules for each_parent_rule in parent_rules: # (.. bal = sum(OrderList.amount) ) if isinstance(each_parent_rule, (Sum, Count)): if each_parent_rule._child_role_name == child_role_name: if parent_role_name not in result_role_rules_list: result_role_rules_list[parent_role_name] = [] result_role_rules_list[parent_role_name].append( each_parent_rule) each_parent_rule._parent_role_name = parent_role_name return result_role_rules_list
def check_parents_on_update(self): """ per ParentCheck rule, verify parents exist. If disabled, ignore (with warning). """ list_ref_integ_rules = rule_bank_withdraw.rules_of_class( self, ParentCheck) if list_ref_integ_rules: ref_integ_rule = list_ref_integ_rules[0] if ref_integ_rule._enable: child_mapper = object_mapper(self.row) my_relationships = child_mapper.relationships for each_relationship in my_relationships: # eg, order has parents cust & emp, child orderdetail if each_relationship.direction == sqlalchemy.orm.interfaces.MANYTOONE: # cust, emp parent_role_name = each_relationship.key # eg, OrderList if not self.is_foreign_key_null(each_relationship): # continue reason = "Cascading PK change to: " + \ each_relationship.key + "->" + \ each_relationship.back_populates if self.reason == reason: """ The parent doing the cascade obviously exists, and note: try to getattr it will fail (FIXME design review - perhaps SQLAlchemy is not checking cache?) """ pass else: self.get_parent_logic_row( parent_role_name) # sets the accessor does_parent_exist = getattr( self.row, parent_role_name) if does_parent_exist is None and ref_integ_rule._enable == True: msg = "Missing Parent: " + parent_role_name self.log(msg) ll = RuleBank() if ll.constraint_event: ll.constraint_event(message=msg, logic_row=self, constraint=None) raise ConstraintException(msg) else: self.log("Warning: Missing Parent: " + parent_role_name) pass # if you don't care, I don't care return self
def __init__(self, derive: InstrumentedAttribute, as_sum_of: any, where: any): super(Sum, self).__init__(derive=derive, where=where) self._as_sum_of = as_sum_of # could probably super-ize parent accessor if isinstance(as_sum_of, str): self._child_role_name = self._as_sum_of.split(".")[ 0] # child role retrieves children self._child_summed_field = self._as_sum_of.split(".")[1] elif isinstance(as_sum_of, InstrumentedAttribute): self._child_summed_field = as_sum_of.key child_attrs = as_sum_of.parent.attrs self._child_role_name = self.get_child_role_name( child_attrs=child_attrs) else: raise Exception( "as_sum_of must be either string, or <mapped-class.column>: " + str(as_sum_of)) rb = RuleBank() rb.deposit_rule(self)
def get_formula_rules(class_name: str) -> list: """withdraw rules of designated a_class """ rule_bank = RuleBank() rules_list = [] role_rules_list = {} # dict of RoleRules for each_rule in rule_bank._tables[class_name].rules: if isinstance(each_rule, Formula): rules_list.append(each_rule) return rules_list
def __init__(self, derive: InstrumentedAttribute, as_count_of: object, where: any): super(Count, self).__init__(derive=derive, where=where) if not isinstance(as_count_of, sqlalchemy.orm.DeclarativeMeta): raise Exception("rule definition error, not mapped class: " + str(as_count_of)) self._as_count_of = as_count_of self._as_count_of_class_name = self.get_class_name(as_count_of) local_attrs = as_count_of._sa_class_manager.local_attrs # FIXME design for each_local_attr in local_attrs: random_attr = local_attrs[each_local_attr] child_attrs = random_attr.parent.attrs break self._child_role_name = self.get_child_role_name( child_attrs=child_attrs) rb = RuleBank() rb.deposit_rule(self)
def compute_formula_execution_order() -> bool: """ Determine formula execution order based on "row.xx" references (dependencies), (or raise exception if cycles detected). """ global version rules_bank = RuleBank() for each_key in rules_bank.orm_objects: compute_formula_execution_order_for_class(class_name=each_key) logic_logger = logging.getLogger("logic_logger") rule_count = 0 logic_logger.debug(f'\nThe following rules have been activated\n') list_rules = rules_bank.__str__() loaded_rules = list(list_rules.split("\n")) for each_rule in loaded_rules: logic_logger.debug(each_rule) rule_count += 1 logic_logger.info(f'Logic Bank {__version__} - {rule_count} rules loaded') return True
def generic_rules_of_class(a_class: (Formula, Constraint, EarlyRowEvent)) -> list: """withdraw rules of the "*" (any) class """ rule_bank = RuleBank() rules_list = [] role_rules_list = {} # dict of RoleRules if "*" in rule_bank._tables: for each_rule in rule_bank._tables["*"].rules: if isinstance(each_rule, a_class): rules_list.append(each_rule) return rules_list
def load_parents_on_insert(self): """ sqlalchemy lazy does not work for inserts... do it here because... 1. RI would require the sql anyway 2. Provide a consistent model - your parents are always there for you - eg, see add_order event rule - references {sales_rep.Manager.FirstName} """ ref_integ_enabled = True list_ref_integ_rules = rule_bank_withdraw.rules_of_class( self, ParentCheck) if list_ref_integ_rules: ref_integ_rule = list_ref_integ_rules[0] child_mapper = object_mapper(self.row) my_relationships = child_mapper.relationships for each_relationship in my_relationships: # eg, order has parents cust & emp, child orderdetail if each_relationship.direction == sqlalchemy.orm.interfaces.MANYTOONE: # cust, emp parent_role_name = each_relationship.key # eg, OrderList if self.is_foreign_key_null(each_relationship) is False: # continue - foreign key not null - parent *should* exist self.get_parent_logic_row( parent_role_name) # sets the accessor does_parent_exist = getattr(self.row, parent_role_name) if does_parent_exist: pass # yes, parent exists... it's all fine elif ref_integ_enabled: msg = "Missing Parent: " + parent_role_name self.log(msg) ll = RuleBank() if ll.constraint_event: ll.constraint_event(message=msg, logic_row=self, constraint=None) raise ConstraintException(msg) else: self.log("Warning: Missing Parent: " + parent_role_name) pass # if you don't care, I don't care return self
def copy_rules(logic_row: LogicRow) -> CopyRulesForTable: """dict(<role_name>, copy_rules[] """ rule_bank = RuleBank() role_rules_list = {} # dict of RoleRules if logic_row.name in rule_bank._tables: for each_rule in rule_bank._tables[logic_row.name].rules: if isinstance(each_rule, Copy): role_name = each_rule._from_parent_role if role_name not in role_rules_list: role_rules_list[role_name] = [] role_rules_list[role_name].append(each_rule) return role_rules_list
def rules_of_class( logic_row: LogicRow, a_class: (Formula, Constraint, EarlyRowEvent)) -> list: """withdraw rules of designated a_class """ rule_bank = RuleBank() rules_list = [] role_rules_list = {} # dict of RoleRules if logic_row.name in rule_bank._tables: for each_rule in rule_bank._tables[logic_row.name].rules: if isinstance(each_rule, a_class): rules_list.append(each_rule) return rules_list
def __init__( self, validate: object, error_msg: str, calling: Callable = None, as_condition: object = None): # str or lambda boolean expression super(Constraint, self).__init__(validate) # self.table = validate # setter finds object self._error_msg = error_msg self._as_condition = as_condition self._calling = calling if calling is None and as_condition is None: raise Exception( f'Constraint {str} requires calling or as_expression') if calling is not None and as_condition is not None: raise Exception( f'Constraint {str} either calling or as_expression') if calling is not None: self._function = calling elif isinstance(as_condition, str): self._as_condition = lambda row: eval(as_condition) ll = RuleBank() ll.deposit_rule(self)
def __init__(self, derive: InstrumentedAttribute, as_exp: str = None, # for very short expressions as_expression: Callable = None, # short, with type checking calling: Callable = None # complex formula ): """ Specify rep * as_exp - string (for very short expressions - price * quantity) * ex_expression - lambda (for type checking) * calling - function (for more complex formula, with old_row) """ super(Formula, self).__init__(derive) self._as_exp = as_exp self._as_expression = as_expression self._function = calling self._as_exp_lambda = None # we exec this, or _function valid_count = 0 if as_exp is not None: self._as_exp_lambda = lambda row: eval(as_exp) valid_count += 1 if as_expression is not None: self._as_exp_lambda = as_expression valid_count += 1 if calling is not None: valid_count += 1 if valid_count != 1: raise Exception(f'Formula requires one of as_exp, as_expression or calling') self._dependencies = [] text = self.get_rule_text() self.parse_dependencies(rule_text=text) self._exec_order = -1 # will be computed in rule_bank_setup (all rules loaded) rb = RuleBank() rb.deposit_rule(self)
def setup(a_session: session, an_engine: Engine): """ Initialize the RuleBank """ rules_bank = RuleBank() rules_bank._session = a_session event.listen(a_session, "before_flush", before_flush) event.listen(a_session, "before_commit", before_commit) rules_bank.orm_objects = {} rules_bank._at = datetime.now() rules_bank._engine = an_engine rules_bank._metadata = MetaData(bind=an_engine, reflect=True) from sqlalchemy.ext.declarative import declarative_base rules_bank._base = declarative_base() return
def get_referring_children(parent_logic_row: LogicRow) -> dict: """ return RulesBank[class_name].referring_children (create if None) referring_children is <parent_role_name>, parent_attribute_list() """ rule_bank = RuleBank() if parent_logic_row.name not in rule_bank._tables: return {} else: # sigh, best to have built this in rule_bank_setup, but unable to get mapper # FIXME design is this threadsafe? table_rules = rule_bank._tables[parent_logic_row.name] result = table_rules.referring_children table_rules.referring_children = {} parent_mapper = object_mapper(parent_logic_row.row) parent_relationships = parent_mapper.relationships for each_parent_relationship in parent_relationships: # eg, order has parents cust & emp, child orderdetail if each_parent_relationship.direction == sqlalchemy.orm.interfaces.ONETOMANY: # cust, emp parent_role_name = each_parent_relationship.back_populates # eg, OrderList table_rules.referring_children[parent_role_name] = [] child_role_name = each_parent_relationship.key child_class_name = get_child_class_name( each_parent_relationship) # eg, OrderDetail if child_class_name not in rule_bank._tables: pass # eg, banking - ALERT is child of customer, has no rules, that's ok else: child_table_rules = rule_bank._tables[ child_class_name].rules search_for_rew_parent = "row." + parent_role_name for each_rule in child_table_rules: if isinstance( each_rule, (Formula, Constraint)): # eg, OrderDetail.ShippedDate rule_text = each_rule.get_rule_text( ) # eg, row.OrderHeader.ShippedDate rule_words = rule_text.split() for each_word in rule_words: if each_word.startswith(search_for_rew_parent): rule_terms = each_word.split(".") # if parent_role_name not in table_rules.referring_children: # table_rules.referring_children[parent_role_name] = () table_rules.referring_children[ parent_role_name].append(rule_terms[2]) return table_rules.referring_children
def setup_early_row_event_all_classes(early_row_event_all_classes: callable): ll = RuleBank() ll._early_row_event_all_classes = early_row_event_all_classes
def __init__( self, validate: object, error_msg: str, calling: Callable = None, as_condition: object = None, # str or lambda boolean expression error_attributes: Sequence[InstrumentedAttribute] = None): super(Constraint, self).__init__(validate) # self.table = validate # setter finds object self._error_msg = error_msg self._as_condition = as_condition self._calling = calling self.error_attributes = error_attributes if calling is None and as_condition is None: msg = "Constraint " + str + " requires calling or as_expression" ll = RuleBank() if ll.constraint_event: ll.constraint_event(message=msg, logic_row=None, constraint=None) raise ConstraintException(msg) if calling is not None and as_condition is not None: msg = "Constraint " + str + " either calling or as_expression" ll = RuleBank() if ll.constraint_event: ll.constraint_event(message=msg, logic_row=None, constraint=None) raise ConstraintException(msg) if calling is not None: self._function = calling elif isinstance(as_condition, str): self._as_condition = lambda row: eval(as_condition) ll = RuleBank() ll.deposit_rule(self)
def __init__(self, on_class: object, calling: Callable = None): super(AbstractRowEvent, self).__init__(on_class) self._function = calling ll = RuleBank() ll.deposit_rule(self)
def cascade_delete_children(self): """ This recursive descent is required to adjust dependent sums/counts on passive_deletes; ie, when (and only when) the DBMS - and *not* SQLAlchemy - does the deletes. (When SQLAlchemy does deletes, these are queued through the normal delete logic.) @see nw/tests/test_dlt_order.py """ parent_mapper = object_mapper(self.row) my_relationships = parent_mapper.relationships for each_relationship in my_relationships: # eg, cust has child OrderDetail if each_relationship.direction == sqlalchemy.orm.interfaces.ONETOMANY: # eg, OrderDetail child_role_name = each_relationship.key # eg, OrderList if each_relationship.cascade.delete and each_relationship.passive_deletes: child_rows = getattr(self.row, child_role_name) for each_child_row in child_rows: old_child = self.make_copy(each_child_row) each_child_logic_row = LogicRow(row=each_child_row, old_row=old_child, ins_upd_dlt="dlt", nest_level=1 + self.nest_level, a_session=self.session, row_sets=self.row_sets) each_child_logic_row.delete( reason="Cascade Delete to run rules on - " + child_role_name) self.session.delete( each_child_row ) # deletes in beforeFlush are not re-queued enforce_cascade = False if enforce_cascade: # disabled - SQLAlchemy DOES enforce cascade delete/nullify; prevent way less important """ per parent_cascade rule(s), nullify (child FKs), delete (children), prevent (if children exist) Default is ParentCascadeAction.PREVENT. This recursive descent is required to adjust dependent sums/counts. """ list_parent_cascade_rules = rule_bank_withdraw.rules_of_class( self, ParentCascade) defined_relns = {} for each_parent_cascade_rule in list_parent_cascade_rules: defined_relns[each_parent_cascade_rule. _relationship] = each_parent_cascade_rule for each_relationship in my_relationships: # eg, Order has child OrderDetail if each_relationship.direction == sqlalchemy.orm.interfaces.ONETOMANY: # eg, OrderDetail each_child_role_name = each_relationship.key # eg, OrderDetailList refinteg_action = ParentCascadeAction.PREVENT if each_child_role_name in defined_relns: refinteg_action = defined_relns[ each_child_role_name]._action child_rows = getattr(self.row, each_child_role_name) for each_child_row in child_rows: old_child = self.make_copy(each_child_row) each_child_logic_row = LogicRow(row=each_child_row, old_row=old_child, ins_upd_dlt="dlt", nest_level=1 + self.nest_level, a_session=self.session, row_sets=self.row_sets) if refinteg_action == ParentCascadeAction.DELETE: # each_relationship.cascade.delete: each_child_logic_row.delete( reason="Cascade Delete - " + each_child_role_name) elif refinteg_action == ParentCascadeAction.NULLIFY: for p, c in each_relationship.local_remote_pairs: setattr(each_child_row, c.name, None) each_child_logic_row.update( reason="Cascade Nullify - " + each_child_role_name) elif refinteg_action == ParentCascadeAction.PREVENT: msg = "Delete rejected - " + each_child_role_name + " has rows" ll = RuleBank() if ll.constraint_event: ll.constraint_event(message=msg, logic_row=self, constraint=None) raise ConstraintException(msg) else: raise Exception("Invalid parent_cascade action: " + refinteg_action)
def get_session(): rule_bank = RuleBank() return rule_bank._session
def get_meta_data(): rule_bank = RuleBank() return rule_bank._metadata