Skip to content

Commit

Permalink
01.20.15 # missing attrs excp with all excps, fail-save rules, excp c…
Browse files Browse the repository at this point in the history
…ontent
  • Loading branch information
valhuber committed Dec 27, 2024
1 parent eea820a commit bd612ed
Show file tree
Hide file tree
Showing 13 changed files with 187 additions and 65 deletions.
8 changes: 8 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@
"program": "${file}",
"console": "integratedTerminal"
},
{
"name": "nw_test test_missing_attrs",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/examples/nw/tests/test_missing_attrs.py",
"redirectOutput": true,
"console": "integratedTerminal"
},
{
"name": "nw_test add emp",
"type": "debugpy",
Expand Down
6 changes: 3 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
"python.testing.pytestEnabled": false,
"debug.console.wordWrap": false,
"python.testing.nittestEnabled": true,
"editor.fontSize": 13,
"terminal.integrated.fontSize": 13,
"window.zoomLevel": 1.0,
// "editor.fontSize": 13,
// "terminal.integrated.fontSize": 13,
// "window.zoomLevel": 1.0,
"python.testing.pytestArgs": [
"examples"
]
Expand Down
Binary file modified examples/nw/db/database.db
Binary file not shown.
13 changes: 11 additions & 2 deletions examples/nw/logic/logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,20 @@ def declare_logic():
if test_bad_rules:
print('loading bad rules')
if use_strings := True:
'''
Rule.constraint(validate='CustomerBadConstraint',
as_condition=lambda row: row.Balance <= row.CreditLimitConstraintBadAttr,
error_msg="balance ({row.Balance}) exceeds credit ({row.CreditLimit})")
Rule.sum(derive='Customer.CreditLimitYY', as_sum_of='Order.AmountTotalTT', where=lambda row: row.BalWhereWorseAttr is None)
Rule.count(derive='Customer.IdNoCount', as_count_of='OrderNoCount', where=lambda row: row.WorstAttr is None)
'''
# Rule.sum(derive='Customer.CreditLimitYY', as_sum_of='Order.AmountTotalTT', where=lambda row: row.BalWhereWorseAttr is None)
# Rule.count(derive='Customer.IdNoCount', as_count_of='OrderNoCount', where=lambda row: row.WorstAttr is None)

# missing attr tests
Rule.sum(derive='Customer.CreditLimit', as_sum_of='Order.AmountTotal', where=lambda row: row.BalWhereWorseAttr is None)
Rule.count(derive='Customer.Count', as_count_of='Order', where=lambda row: row.WorstAttr is None)
Rule.constraint(validate='Customer',
as_condition=lambda row: row.Balance <= row.CreditLimitConstraintBadAttr,
error_msg="balance ({row.Balance}) exceeds credit ({row.CreditLimit})")
else:
Rule.constraint(validate=CustomerYY,
as_condition=lambda row: row.Balance <= row.CreditLimitConstraintBadAttr,
Expand Down
6 changes: 5 additions & 1 deletion logic_bank/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ class LBActivateException(Exception):

message = "LogicBank Activation error"
def __init__(self, invalid_rules = [], missing_attributes = []):
self.invalid_rules = invalid_rules
LBActivateException = invalid_rules
self.missing_attributes = missing_attributes
super().__init__(self.message)

'''
def __str__(self):
return f'LBActivateException: \n{LBActivateException}\n{self.missing_attributes}\n{self.message}'
'''
64 changes: 49 additions & 15 deletions logic_bank/logic_bank.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
from logic_bank.rule_type.parent_check import ParentCheck
from logic_bank.rule_type.row_event import EarlyRowEvent, RowEvent, CommitRowEvent, AfterFlushRowEvent
from logic_bank.rule_type.sum import Sum
from logic_bank import engine_logger
import functools
import logging
import traceback
import os
from .exceptions import LBActivateException
from .rule_bank.rule_bank import RuleBank

logic_logger = logging.getLogger("logic_logger")

Expand Down Expand Up @@ -73,13 +75,28 @@ class LogicBank:
@staticmethod
def activate(session: session, activator: callable, constraint_event: callable = None):
"""
Call LogicBank.activate(activator) (this file: logic_bank.py), after opening database to activate logic :
- registers SQLAlchemy listeners
#### Usage (e.g., als - highly recommended)
Ooccurs in `api_logic_server_run -> Config/server_setup`, after opening database to activate logic:
`LogicBank.activate(session=session, activator=declare_logic.declare_logic, constraint_event=constraint_handler)`
#### Operation (file: `logic_bank.py`):
- Calls `rule_bank_setup.setup(session)` to set up the `RuleBank` and register SQLAlchemy listeners
- invokes your activator to load rules to create `RuleBank` (dict of `TableRules`); later executed on commit
- Calls your activator to load rules into `RuleBank` (not executed yet)
- raises exception if cycles detected, or invalid rules per rule references
- Calls `rule_bank_setup.compute_formula_execution_order()` for dependencies
- Raises exception if cycles detected, or invalid rules per missing attr references
#### Subsequent rule execution starts in `exec_row_logic/LogicRow.py on session.commit()`
- `exec_trans_logic/listeners.py` handles the SQLAlchemy events (after_flush etc.) to get changed rows; for each, it calls....
- `exec_row_logic/LogicRow.py#update()`, which executes the rule_type objects (in TableRules)
Use constraint_event to log / change class of constraints, for example
'''
Expand All @@ -94,20 +111,10 @@ def constraint_handler(message: str, constraint: Constraint, logic_row: LogicRow
logic_row.log(exception_message)
raise MyConstraintException(exception_message)
'''
In API Logic Server (highly recommended): setup occurs in `api_logic_server_run -> Config/server_setup`:
- `LogicBank.activate(session=session, activator=declare_logic.declare_logic, constraint_event=constraint_handler)`
- Rule Execution occurs in exec_row_logic/LogicRow.py on session.commit()
- exec_trans_logic handles the SQLAlchemy events (after_flush etc) to get changed rows; for each, it calls....
- `exec_row_logic/LogicRow.py#update()`, which executes the rule_type objects (in TableRules)
Arguments:
session: SQLAlchemy session
activator: user function that declares rules (e.g., Rule.sum...)
activator: user function that declares rules (e.g., Rule.sum... in als `logic/declare_logic.py`)
constraint_event: optional user function called on constraint exceptions
"""
rule_bank = rule_bank_setup.setup(session)
Expand All @@ -117,10 +124,37 @@ def constraint_handler(message: str, constraint: Constraint, logic_row: LogicRow
activator() # in als, called from server_setup - this is logic/declare_logic.py#declare_logic()
except Exception as e:
rule_bank.invalid_rules.append(e)

if debug_show_attributes :=True:
rules_bank = RuleBank()
rule_count = 0
logic_logger.debug(f'\nThe following rules have been loaded')
list_rules = rules_bank.__str__()
loaded_rules = list(list_rules.split("\n"))
for each_rule in loaded_rules: # rules with bad derive= etc not loaded - no TableRule to own them
logic_logger.debug(str(each_rule))
rule_count += 1

missing_attributes = rule_bank_setup.compute_formula_execution_order()
if len(rule_bank.invalid_rules) > 0 or len(missing_attributes) > 0:
#raise Exception(rule_bank.invalid_rules, missing_attributes) # compare - this logs the errors
for each_invalid_rule in rule_bank.invalid_rules:
logic_logger.info(f'Invalid Rule: {each_invalid_rule}')
for each_missing_attribute in missing_attributes:
logic_logger.info(f'Missing Attribute: {each_missing_attribute}')
raise LBActivateException(rule_bank.invalid_rules, missing_attributes)

rules_bank = RuleBank()
rule_count = 0
logic_logger.debug(f'\nThe following rules have been activated')
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 {rule_bank_setup.__version__} - {rule_count} rules loaded')


class Rule:
"""Invoke these functions to declare rules.
Expand Down
45 changes: 44 additions & 1 deletion logic_bank/rule_bank/rule_bank.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ def __init__(self):
self._session = None
self.constraint_event = None
self.invalid_rules : list[str] = [] # rule-load failures during activation
self.map_name_to_mapper = None # type: None | Dict[str, mapper]
""" mappers for each orm_object, key is class name """


def deposit_rule(self, a_rule: 'AbstractRule'):
if a_rule._load_error is not None:
Expand All @@ -60,7 +63,18 @@ def deposit_rule(self, a_rule: 'AbstractRule'):
if a_rule.table not in self.orm_objects:
self.orm_objects[a_rule.table] = TableRules()
table_rules = self.orm_objects[a_rule.table]
table_rules._decl_meta = a_rule._decl_meta
# a_rule._decl_meta = None # huh??
if hasattr(a_rule, '_decl_meta'):
if a_rule._decl_meta is not None:
if self.map_name_to_mapper is None: # setup mappers for each orm_object
self.map_name_to_mapper = {}
table_rules._decl_meta = a_rule._decl_meta
decl_meta = table_rules._decl_meta
mappers = decl_meta.registry.mappers
for each_mapper in mappers:
each_class_name = each_mapper.class_.__name__
if each_class_name not in self.map_name_to_mapper:
self.map_name_to_mapper[each_class_name] = each_mapper
table_rules.rules.append(a_rule)
engine_logger.debug(prt(str(a_rule)))
else:
Expand All @@ -74,6 +88,35 @@ def get_all_rules(self):
all_rules.append(each_rule)
return all_rules

def get_mapper_for_class_name(self, class_name: str):
""" return sqlalchemy mapper for class_name
beware - very subtle case
referenced attrs may not have rules, so no entry in rules_bank.orm_objects
so, we have to a use SQLAlchemy meta... to get the mapper
from first each_attribute
key assumption is 1st each_attribute (rule) will be for a class *with rules*
Args:
class_name (str): class name
"""

if self.map_name_to_mapper is None: # setup mappers for each orm_object
self.map_name_to_mapper = {}
if class_name not in self.orm_objects:
engine_logger.info(f"uh oh, no rules for first referenced attribute: {class_name}")
else:
table_rules = self.orm_objects[class_name] # key assumption, above
decl_meta = table_rules._decl_meta
mappers = decl_meta.registry.mappers
for each_mapper in mappers:
each_class_name = each_mapper.class_.__name__
if each_class_name not in self.map_name_to_mapper:
self.map_name_to_mapper[each_class_name] = each_mapper

if class_name in self.map_name_to_mapper:
return self.map_name_to_mapper[class_name]
return None

def __str__(self):
result = f"Rule Bank[{str(hex(id(self)))}] (loaded {self._at})"
for each_key in self.orm_objects:
Expand Down
68 changes: 33 additions & 35 deletions logic_bank/rule_bank/rule_bank_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from sqlalchemy.orm import mapper
import logging

__version__ = "01.20.14" # missing attrs excp with all excps, fail-save rules
__version__ = "01.20.15" # missing attrs excp with all excps, fail-save rules, excp content


def setup(a_session: session):
Expand Down Expand Up @@ -64,31 +64,38 @@ def find_missing_attributes(all_attributes: list[str], rules_bank: RuleBank) ->
class_and_attr = each_attribute.split(':')[0]
if len(class_and_attr.split('.')) > 2:
pass # FIXME - parent reference, need to decode the role name --> table name
if len(class_and_attr.split('.')) < 2:
missing_attributes.append(each_attribute)
continue
class_name = class_and_attr.split('.')[0]
attr_name = class_and_attr.split('.')[1]
if attr_name == 'unit_price':
good_breakpoint = True
if mapper_dict is None:
''' beware - very subtle case
referenced attrs may not have rules, so no entry in rules_bank.orm_objects
so, we have a use SQLAlchemy meta... to get the mapper
from first each_attribute
key assumption is 1st each_attribute (rule) will be for a class *with rules*
'''
mapper_dict = {}
table_rules = rules_bank.orm_objects[class_name] # key assumption, above
decl_meta = table_rules._decl_meta
mappers = decl_meta.registry.mappers
for each_mapper in mappers:
each_class_name = each_mapper.class_.__name__
if each_class_name not in mapper_dict:
mapper_dict[each_class_name] = each_mapper
if class_name not in mapper_dict:
each_mapper = rules_bank.get_mapper_for_class_name(class_name)
if each_mapper is None:
missing_attributes.append(each_attribute)
continue
if old_code := False:
if mapper_dict is None:
''' beware - very subtle case
referenced attrs may not have rules, so no entry in rules_bank.orm_objects
so, we have a use SQLAlchemy meta... to get the mapper
from first each_attribute
key assumption is 1st each_attribute (rule) will be for a class *with rules*
'''
mapper_dict = {}
table_rules = rules_bank.orm_objects[class_name] # key assumption, above
decl_meta = table_rules._decl_meta
mappers = decl_meta.registry.mappers
for each_mapper in mappers:
each_class_name = each_mapper.class_.__name__
if each_class_name not in mapper_dict:
mapper_dict[each_class_name] = each_mapper
if class_name not in mapper_dict:
missing_attributes.append(each_attribute)
continue
if 'unit_price' in each_attribute:
good_breakpoint = True
each_mapper = mapper_dict[class_name]
if attr_name not in each_mapper.all_orm_descriptors:
missing_attributes.append(each_attribute)
pass
Expand Down Expand Up @@ -136,9 +143,10 @@ def compute_formula_execution_order_for_class(class_name: str):


def compute_formula_execution_order() -> list[str]:
"""
Determine formula execution order based on "row.xx" references (dependencies),
(or raise exception if cycles detected).
""" Determine formula execution order based on "row.xx" references (dependencies).
Returns:
list[str]: list of attributes that are missing, have cyclic dependencies, or other issues (not excp)
"""
global version
logic_logger = logging.getLogger("logic_logger")
Expand All @@ -147,22 +155,12 @@ def compute_formula_execution_order() -> list[str]:
for each_key in rules_bank.orm_objects:
compute_formula_execution_order_for_class(class_name=each_key) # might raise excp

all_referenced_attributes = find_referenced_attributes(rules_bank)
if do_print_attribute := False:
all_referenced_attributes = find_referenced_attributes(rules_bank) # now consider other rule attr references
if do_print_attribute := True:
logic_logger.debug(f'\nThe following attributes have been referenced\n')
for each_attribute in all_referenced_attributes:
logic_logger.debug(f'..{each_attribute}')
missing_attributes = find_missing_attributes(all_attributes=all_referenced_attributes, rules_bank=rules_bank)
if len(missing_attributes) > 0:
# raise Exception("Missing attributes:" + str(missing_attributes))
return missing_attributes

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 []
pass # raise Exception("Missing attributes:" + str(missing_attributes))
return missing_attributes # string array of missing attrs, hopefully empty
18 changes: 14 additions & 4 deletions logic_bank/rule_type/abstractrule.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,25 @@ class AbstractRule(object):

def __init__(self, decl_meta: sqlalchemy.orm.DeclarativeMeta):
# failed -- mapped_class = get_class_by_table(declarative_base(), a_table_name) # User class
if not isinstance(decl_meta, sqlalchemy.orm.DeclarativeMeta):
self._load_error = "rule definition error, not mapped class: " + str(decl_meta)
else:
if isinstance(decl_meta, sqlalchemy.orm.DeclarativeMeta):
self._decl_meta = decl_meta
""" SQLAlchemy class definition """
class_name = self.get_class_name(decl_meta)
self.table = class_name
self._load_error = None # rule-load failures (detected during activation)
""" load error detected during activation (None means ok so far) """

elif isinstance(decl_meta, str):
rule_bank = RuleBank()
mapper = rule_bank.get_mapper_for_class_name(decl_meta)
if mapper is not None:
self._decl_meta = mapper.class_
self.table = decl_meta
else:
self._load_error = "rule definition error, not mapped class name: " + str(decl_meta)
self.table = f"unknown name: {decl_meta}"
else:
self._load_error = "rule definition error, not mapped class or class_name: " + str(decl_meta)
self.table = f"unknown object: {decl_meta}"
self._dependencies = []
"""
list of attributes this rule refers to, including parent.attribute
Expand Down
4 changes: 3 additions & 1 deletion logic_bank/rule_type/aggregate.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ def get_aggregate_dependencies(self) -> list[str]:
return referenced_attributes

def get_where_text(self, where_arg) -> str:
if isinstance(self._where, str):
if self._where is None:
text = ''
elif isinstance(self._where, str):
text = self._where
else:
text = inspect.getsource(self._where)
Expand Down
2 changes: 1 addition & 1 deletion logic_bank/rule_type/count.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def __init__(self, derive: InstrumentedAttribute, as_count_of: object, where: an

def __str__(self):
if self._where != "":
result = super().__str__() + f'Count({self._as_count_of} Where {self._where})'
result = super().__str__() + f'Count({self._as_count_of} Where {self.get_where_text(self._where)} - {self._where})'
else:
result = super().__str__() + f'Count({self._as_count_of})'
if self.insert_parent:
Expand Down
Loading

0 comments on commit bd612ed

Please sign in to comment.