CCCPaste

rule.py

import functools
import operator
import inspect
from typing import Callable, Any, Dict, List


class Rule:
    def __init__(self, func: Callable[[Dict[str, Any]], bool], name: str = None):
        self.func = func
        self.name = name or func.__name__

    def __call__(self, context: Dict[str, Any]) -> bool:
        return self.func(context)

    def __and__(self, other: "Rule") -> "Rule":
        return Rule(lambda ctx: self(ctx) and other(ctx),
                    name=f"({self.name} AND {other.name})")

    def __or__(self, other: "Rule") -> "Rule":
        return Rule(lambda ctx: self(ctx) or other(ctx),
                    name=f"({self.name} OR {other.name})")

    def __invert__(self) -> "Rule":
        return Rule(lambda ctx: not self(ctx),
                    name=f"(NOT {self.name})")


class RuleEngine:
    def __init__(self):
        self.rules: List[Rule] = []

    def register(self, rule: Rule):
        self.rules.append(rule)

    def evaluate(self, context: Dict[str, Any]) -> Dict[str, bool]:
        results = {}
        for rule in self.rules:
            try:
                results[rule.name] = rule(context)
            except Exception as e:
                results[rule.name] = False
        return results


def auto_rule(fn):
    sig = inspect.signature(fn)
    def wrapper(ctx):
        bound = {k: ctx.get(k) for k in sig.parameters}
        return fn(**bound)
    return Rule(wrapper, name=fn.__name__)


@auto_rule
def is_adult(age):
    return age >= 18


@auto_rule
def high_income(income):
    return income > 5000


@auto_rule
def country_allowed(country):
    return country in {"CL", "MX", "CO"}


engine = RuleEngine()
engine.register(is_adult & high_income | country_allowed)
engine.register(~high_income & is_adult)

context = {"age": 25, "income": 3000, "country": "CL"}
print(engine.evaluate(context))