A lightweight, optionally typed expression language with a custom grammar for matching arbitrary Python objects.
—
The fundamental rule evaluation system that provides rule creation, matching, filtering, and expression evaluation capabilities. These operations form the foundation of the rule engine's functionality.
Create rule objects from text expressions with optional context for type safety and custom symbol resolution.
class Rule:
def __init__(self, rule_text: str, context: Context = None):
"""
Create a new rule from a text expression.
Args:
rule_text (str): The rule expression text to parse
context (Context, optional): Context for symbol resolution and type checking
Raises:
RuleSyntaxError: If the rule text contains syntax errors
EvaluationError: If type checking fails during parsing
"""Usage Example:
import rule_engine
# Basic rule creation
rule = rule_engine.Rule('age > 18 and status == "active"')
# Rule with typed context
context = rule_engine.Context(
type_resolver=rule_engine.type_resolver_from_dict({
'age': rule_engine.DataType.FLOAT,
'status': rule_engine.DataType.STRING
})
)
typed_rule = rule_engine.Rule('age > 18', context=context)Evaluate rules against data to determine if they match the specified conditions.
def matches(self, thing, **kwargs) -> bool:
"""
Test if the rule matches the provided object.
Args:
thing: The object to evaluate against the rule
**kwargs: Additional keyword arguments passed to the evaluation context
Returns:
bool: True if the rule matches, False otherwise
Raises:
EvaluationError: If evaluation fails due to type errors or other issues
SymbolResolutionError: If symbols cannot be resolved
AttributeResolutionError: If attributes cannot be accessed
"""Usage Example:
rule = rule_engine.Rule('name == "John" and age >= 21')
person1 = {'name': 'John', 'age': 25}
person2 = {'name': 'Jane', 'age': 19}
print(rule.matches(person1)) # True
print(rule.matches(person2)) # False
# Using object attributes instead of dictionary
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
john = Person('John', 25)
print(rule.matches(john)) # TrueFilter collections of objects using rule expressions, returning only matching items.
def filter(self, things, **kwargs):
"""
Filter an iterable of objects, yielding only those that match the rule.
Args:
things: Iterable of objects to filter
**kwargs: Additional keyword arguments passed to the evaluation context
Yields:
Objects from the iterable that match the rule
Raises:
EvaluationError: If evaluation fails for any item
SymbolResolutionError: If symbols cannot be resolved
AttributeResolutionError: If attributes cannot be accessed
"""Usage Example:
rule = rule_engine.Rule('age >= 18 and department == "engineering"')
employees = [
{'name': 'Alice', 'age': 25, 'department': 'engineering'},
{'name': 'Bob', 'age': 17, 'department': 'engineering'},
{'name': 'Charlie', 'age': 30, 'department': 'marketing'},
{'name': 'Diana', 'age': 28, 'department': 'engineering'}
]
# Filter returns a generator
eligible_employees = list(rule.filter(employees))
print(f"Found {len(eligible_employees)} eligible employees")
# Can be used in loops
for employee in rule.filter(employees):
print(f"{employee['name']} is eligible")Evaluate rule expressions to compute and return values rather than just boolean results.
def evaluate(self, thing, **kwargs):
"""
Evaluate the rule expression and return the computed result.
Args:
thing: The object to evaluate against
**kwargs: Additional keyword arguments passed to the evaluation context
Returns:
The computed result of the expression evaluation
Raises:
EvaluationError: If evaluation fails due to type errors or other issues
SymbolResolutionError: If symbols cannot be resolved
AttributeResolutionError: If attributes cannot be accessed
"""Usage Example:
# Arithmetic expressions
calc_rule = rule_engine.Rule('price * quantity * (1 + tax_rate)')
order = {'price': 10.50, 'quantity': 3, 'tax_rate': 0.08}
total = calc_rule.evaluate(order)
print(f"Total: ${total:.2f}")
# String concatenation
name_rule = rule_engine.Rule('first_name + " " + last_name')
person = {'first_name': 'John', 'last_name': 'Doe'}
full_name = name_rule.evaluate(person)
print(full_name) # "John Doe"
# Complex expressions with built-in functions
data_rule = rule_engine.Rule('filter(lambda x: x > 10, numbers)')
data = {'numbers': [5, 15, 8, 20, 12]}
filtered = data_rule.evaluate(data)
print(list(filtered)) # [15, 20, 12]Validate rule syntax without creating a full rule object.
@classmethod
def is_valid(cls, text: str, context: Context = None) -> bool:
"""
Test whether a rule is syntactically correct.
Args:
text (str): The rule expression text to validate
context (Context, optional): Context for type checking validation
Returns:
bool: True if the rule is well-formed and valid
"""Usage Example:
import rule_engine
# Validate syntax before creating rule
if rule_engine.Rule.is_valid('age > 18 and status == "active"'):
rule = rule_engine.Rule('age > 18 and status == "active"')
print("Rule is valid")
else:
print("Invalid rule syntax")
# Validate with context for type checking
context = rule_engine.Context(
type_resolver=rule_engine.type_resolver_from_dict({
'age': rule_engine.DataType.FLOAT,
'status': rule_engine.DataType.STRING
})
)
if rule_engine.Rule.is_valid('age + status', context): # Will be False - type error
print("Valid")
else:
print("Invalid - type error")Generate GraphViz diagrams of parsed rule abstract syntax trees for debugging.
def to_graphviz(self):
"""
Generate a GraphViz diagram of the rule's AST.
Returns:
graphviz.Digraph: The rule diagram
Raises:
ImportError: If graphviz package is not installed
"""Usage Example:
import rule_engine
rule = rule_engine.Rule('age > 18 and (status == "active" or priority >= 5)')
digraph = rule.to_graphviz()
# Save the diagram to a file
digraph.render('rule_ast', format='png', cleanup=True)
# Or view the source
print(digraph.source)Access rule metadata and configuration information.
@property
def text(self) -> str:
"""The original rule text string."""
@property
def context(self) -> Context:
"""The context object associated with this rule."""Usage Example:
rule = rule_engine.Rule('age > 18')
print(f"Rule text: {rule.text}")
print(f"Has context: {rule.context is not None}")
# Accessing context properties
context = rule_engine.Context(default_value="unknown")
typed_rule = rule_engine.Rule('name', context=context)
print(f"Default value: {typed_rule.context.default_value}")Rules support custom keyword arguments that can be used in expressions or passed to custom resolvers.
rule = rule_engine.Rule('threshold > min_value')
result = rule.matches({'threshold': 15}, min_value=10) # TrueUse regular expression operators for string pattern matching.
email_rule = rule_engine.Rule('email =~ ".*@company\\.com$"')
user = {'email': 'john.doe@company.com'}
print(email_rule.matches(user)) # TrueWork with datetime objects and operations.
rule = rule_engine.Rule('created_date > parse_datetime("2023-01-01")')
record = {'created_date': datetime.datetime(2023, 6, 15)}
print(rule.matches(record)) # TrueAll core operations can raise various exceptions that should be handled appropriately:
import rule_engine
try:
rule = rule_engine.Rule('invalid syntax ===')
except rule_engine.RuleSyntaxError as e:
print(f"Syntax error: {e.message}")
try:
rule = rule_engine.Rule('unknown_field == "value"')
rule.matches({'known_field': 'value'})
except rule_engine.SymbolResolutionError as e:
print(f"Symbol error: {e.message}")
if e.suggestion:
print(f"Did you mean: {e.suggestion}")Install with Tessl CLI
npx tessl i tessl/pypi-rule-engine