A lightweight, optionally typed expression language with a custom grammar for matching arbitrary Python objects.
—
Context management system for controlling how symbols are resolved, type checking is performed, and default values are handled. The context system enables custom symbol resolution, type safety, and flexible data access patterns.
Create context objects that define how symbols are resolved and type checking is performed.
class Context:
def __init__(self, *, regex_flags=0, resolver=None, type_resolver=None,
default_timezone='local', default_value=UNDEFINED, decimal_context=None):
"""
Create a context for rule evaluation.
Args:
regex_flags (int): Flags for regex operations (re module flags)
resolver: Function for symbol value resolution
type_resolver: Function or mapping for symbol type resolution
default_timezone: Default timezone ('local', 'utc', or tzinfo instance)
default_value: Default value for unresolved symbols
decimal_context: Decimal context for float operations
"""Usage Example:
import rule_engine
# Basic context with default value
context = rule_engine.Context(default_value="N/A")
rule = rule_engine.Rule('missing_field', context=context)
result = rule.evaluate({}) # Returns "N/A"
# Context with custom resolver
def custom_resolver(symbol_name, obj):
if symbol_name == 'full_name':
return f"{obj.get('first', '')} {obj.get('last', '')}"
return obj.get(symbol_name)
context = rule_engine.Context(resolver=custom_resolver)
rule = rule_engine.Rule('full_name', context=context)
person = {'first': 'John', 'last': 'Doe'}
print(rule.evaluate(person)) # "John Doe"Resolve symbols to values using context-specific resolution functions.
def resolve(self, thing, name: str, scope: str = None):
"""
Resolve a symbol name to its value.
Args:
thing: The object to resolve the symbol against
name (str): The symbol name to resolve
scope (str, optional): The scope for symbol resolution
Returns:
The resolved value for the symbol
Raises:
SymbolResolutionError: If the symbol cannot be resolved
"""Resolve attributes from object values within the context.
def resolve_attribute(self, thing, object_, name: str):
"""
Resolve an attribute from an object value.
Args:
thing: The root object for resolution
object_: The object to access the attribute on
name (str): The attribute name to resolve
Returns:
The resolved attribute value
Raises:
AttributeResolutionError: If the attribute cannot be resolved
"""Resolve type information for symbols during rule parsing.
def resolve_type(self, name: str, scope: str = None):
"""
Resolve type information for a symbol.
Args:
name (str): The symbol name to get type information for
scope (str, optional): The scope for type resolution
Returns:
DataType: The data type for the symbol
Raises:
SymbolResolutionError: If type cannot be resolved
"""Manage temporary symbol assignments within expression contexts.
@contextlib.contextmanager
def assignments(self, *assignments):
"""
Add assignments to a thread-specific scope.
Args:
*assignments: Assignment objects to define in the scope
Yields:
Context manager for assignment scope
"""Standalone function for resolving symbols as object attributes with error handling and suggestions.
def resolve_attribute(thing, name: str):
"""
Resolve a symbol as an attribute of the provided object.
Args:
thing: The object to access the attribute on
name (str): The attribute name to resolve
Returns:
The value of the specified attribute
Raises:
SymbolResolutionError: If the attribute doesn't exist
Warning:
This exposes all attributes of the object. Use custom resolvers
for security-sensitive applications.
"""Usage Example:
import rule_engine
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
self._private = "secret"
# Using attribute resolution
context = rule_engine.Context(resolver=rule_engine.resolve_attribute)
rule = rule_engine.Rule('name + " is " + str(age)', context=context)
person = Person("Alice", 30)
result = rule.evaluate(person)
print(result) # "Alice is 30"
# Accessing private attributes (be careful!)
private_rule = rule_engine.Rule('_private', context=context)
print(private_rule.evaluate(person)) # "secret"Resolve symbols as dictionary/mapping keys with error handling and suggestions.
def resolve_item(thing, name: str):
"""
Resolve a symbol as a key in a mapping object.
Args:
thing: The mapping object to access
name (str): The key name to resolve
Returns:
The value for the specified key
Raises:
SymbolResolutionError: If the key doesn't exist or object isn't a mapping
"""Usage Example:
import rule_engine
# Using item resolution for dictionaries
context = rule_engine.Context(resolver=rule_engine.resolve_item)
rule = rule_engine.Rule('user_id > 1000 and status == "active"', context=context)
user_data = {
'user_id': 1234,
'status': 'active',
'email': 'user@example.com'
}
print(rule.matches(user_data)) # True
# Works with any mapping-like object
from collections import OrderedDict
ordered_data = OrderedDict([('priority', 1), ('task', 'important')])
task_rule = rule_engine.Rule('priority == 1', context=context)
print(task_rule.matches(ordered_data)) # TrueCreate type resolvers from dictionaries to enable type checking and validation.
def type_resolver_from_dict(dictionary: dict):
"""
Create a type resolver function from a dictionary mapping.
Args:
dictionary (dict): Mapping of symbol names to DataType constants
Returns:
A type resolver function for use with Context
Raises:
SymbolResolutionError: If a symbol is not defined in the dictionary
"""Usage Example:
import rule_engine
# Define types for symbols
type_map = {
'name': rule_engine.DataType.STRING,
'age': rule_engine.DataType.FLOAT,
'active': rule_engine.DataType.BOOLEAN,
'tags': rule_engine.DataType.ARRAY,
'metadata': rule_engine.DataType.MAPPING
}
type_resolver = rule_engine.type_resolver_from_dict(type_map)
context = rule_engine.Context(type_resolver=type_resolver)
# This will work - types match
rule = rule_engine.Rule('name + " is " + str(age)', context=context)
# This will fail - type mismatch
try:
bad_rule = rule_engine.Rule('name + age', context=context) # Can't add string + number
except rule_engine.EvaluationError as e:
print(f"Type error: {e.message}")
# This will fail - undefined symbol
try:
unknown_rule = rule_engine.Rule('unknown_field == "value"', context=context)
except rule_engine.SymbolResolutionError as e:
print(f"Unknown symbol: {e.symbol_name}")Use both type and value resolvers together for comprehensive symbol management.
import rule_engine
# Custom resolver that handles special cases
def smart_resolver(symbol_name, obj):
if symbol_name == 'full_name':
return f"{obj.get('first_name', '')} {obj.get('last_name', '')}"
elif symbol_name == 'is_adult':
return obj.get('age', 0) >= 18
return rule_engine.resolve_item(obj, symbol_name)
# Type definitions including computed fields
type_map = {
'first_name': rule_engine.DataType.STRING,
'last_name': rule_engine.DataType.STRING,
'age': rule_engine.DataType.FLOAT,
'full_name': rule_engine.DataType.STRING, # computed field
'is_adult': rule_engine.DataType.BOOLEAN # computed field
}
context = rule_engine.Context(
type_resolver=rule_engine.type_resolver_from_dict(type_map),
resolver=smart_resolver
)
rule = rule_engine.Rule('is_adult and full_name =~ "John.*"', context=context)
person = {'first_name': 'John', 'last_name': 'Doe', 'age': 25}
print(rule.matches(person)) # TrueCreate secure resolvers that limit access to specific attributes or keys.
import rule_engine
def secure_resolver(symbol_name, obj):
# Whitelist of allowed attributes
allowed_fields = {'name', 'email', 'department', 'role'}
if symbol_name not in allowed_fields:
raise rule_engine.SymbolResolutionError(symbol_name)
return getattr(obj, symbol_name, None)
context = rule_engine.Context(resolver=secure_resolver)
rule = rule_engine.Rule('department == "engineering"', context=context)
class Employee:
def __init__(self):
self.name = "John"
self.department = "engineering"
self.salary = 100000 # This won't be accessible
employee = Employee()
print(rule.matches(employee)) # True
# This will fail - salary is not in the whitelist
try:
salary_rule = rule_engine.Rule('salary > 50000', context=context)
salary_rule.matches(employee)
except rule_engine.SymbolResolutionError as e:
print(f"Access denied to: {e.symbol_name}")Access context configuration and metadata.
context = rule_engine.Context(
type_resolver=rule_engine.type_resolver_from_dict({'name': rule_engine.DataType.STRING}),
resolver=rule_engine.resolve_item,
default_value="unknown"
)
print(f"Has type resolver: {context.type_resolver is not None}")
print(f"Has custom resolver: {context.resolver is not None}")
print(f"Default value: {context.default_value}")Context operations can raise various exceptions that provide detailed error information:
import rule_engine
try:
context = rule_engine.Context()
rule = rule_engine.Rule('undefined_symbol', context=context)
rule.evaluate({})
except rule_engine.SymbolResolutionError as e:
print(f"Symbol '{e.symbol_name}' not found")
if e.thing is not rule_engine.UNDEFINED:
print(f"Available attributes: {dir(e.thing)}")
if e.suggestion:
print(f"Did you mean: {e.suggestion}")
# Type resolution errors
try:
type_resolver = rule_engine.type_resolver_from_dict({'age': rule_engine.DataType.FLOAT})
context = rule_engine.Context(type_resolver=type_resolver)
rule = rule_engine.Rule('name == "John"', context=context) # 'name' not defined
except rule_engine.SymbolResolutionError as e:
print(f"Type not defined for symbol: {e.symbol_name}")Install with Tessl CLI
npx tessl i tessl/pypi-rule-engine