Extending CEL: Context and Custom Functions¶
You've learned the basics in Your First Integration - now let's dive deeper into advanced Context patterns, function best practices, and testing strategies. This tutorial takes you from functional to production-ready CEL implementations.
Prerequisites: Complete Your First Integration to understand Context basics and custom function registration. This tutorial assumes you're comfortable with the fundamental concepts.
What You'll Master: Advanced Context patterns, reusable context builders, function best practices, comprehensive error handling, and testing strategies for production CEL implementations.
The Context Class¶
While dictionary context works well for simple cases, the Context class provides more control and features for complex applications.
Basic Context Usage¶
from cel import evaluate, Context
# Create a context object
context = Context()
# Add variables one by one
context.add_variable("user_name", "Alice")
context.add_variable("user_age", 30)
context.add_variable("permissions", ["read", "write"])
# Use the context
result = evaluate("user_name + ' is ' + string(user_age)", context)
assert result == "Alice is 30" # → String concatenation with type conversion
result = evaluate('"write" in permissions', context)
assert result == True # → List membership check for permissions
Adding Multiple Variables¶
from cel import Context, evaluate
context = Context()
# Add multiple variables at once
context.update({
"user": {
"id": "user123",
"name": "Bob",
"email": "bob@example.com",
"verified": True
},
"session": {
"created_at": "2024-01-01T10:00:00Z",
"expires_at": "2024-01-01T18:00:00Z"
},
"environment": "production"
})
# Complex expressions with nested data
policy = """
user.verified &&
environment == "production" &&
user.email.endsWith("@example.com")
"""
result = evaluate(policy, context)
assert result == True # → Complex multi-condition policy evaluation
Context vs Dictionary - When to Use Which?¶
Use Dictionary Context when: - Simple, one-off expressions - Static data that doesn't change - Quick prototyping or testing
Use Context Class when: - Adding custom functions - Building reusable evaluation environments - Need to modify context dynamically - Working with complex, evolving data structures
Step 1: Simple Dictionary Example
# Simple case - dictionary is fine
result = evaluate("x + y", {"x": 10, "y": 20})
assert result == 30 # → Basic arithmetic with dictionary context
Step 2: Define Custom Functions
# Complex case requires custom functions
def email_validator(email):
return "@" in email and "." in email
def password_hasher(password):
return f"hash_{len(password)}"
def check_permissions():
return True
Step 3: Create Context and Register Functions
context = Context()
context.add_function("validate_email", email_validator)
context.add_function("hash_password", password_hasher)
context.add_function("check_permissions", check_permissions)
Step 4: Add Variables and Evaluate
context.add_variable("base_config", {"database": {"host": "localhost", "port": 5432}})
context.add_variable("user", {"email": "test@example.com"})
result = evaluate("validate_email(user.email) && check_permissions()", context)
assert result == True # → Custom function orchestration for business logic
Custom Functions¶
One of CEL's most powerful features is the ability to call Python functions from within expressions.
Basic Function Registration¶
Step 1: Define Your Functions
from cel import Context, evaluate
def calculate_tax(income, rate=0.1):
"""Calculate tax based on income and rate."""
return income * rate
def is_valid_email(email):
"""Simple email validation."""
return "@" in email and "." in email
Step 2: Create Context and Register Functions
tax_context = Context()
tax_context.add_function("calculate_tax", calculate_tax)
tax_context.add_function("is_valid_email", is_valid_email)
Step 3: Add Variables
tax_context.add_variable("user_income", 50000)
tax_context.add_variable("user_email", "alice@example.com")
Step 4: Evaluate Expressions
tax_result = evaluate("calculate_tax(user_income, 0.15)", tax_context)
assert tax_result == 7500.0 # → Function with parameters: 50000 * 0.15
email_result = evaluate("is_valid_email(user_email)", tax_context)
assert email_result == True # → Validation function returns boolean
Functions with Complex Logic¶
Step 1: Define Complex Business Functions
from cel import Context, evaluate
def score_calculation(base_score, bonus_multiplier):
"""Calculate final score with bonus."""
return base_score * bonus_multiplier
def is_prime(n):
"""Check if number is prime (simple implementation)."""
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
def format_user_info(name, age, department):
"""Format user information string."""
return f"{name} ({age}) from {department}"
Step 2: Create Context and Register Functions
demo_context = Context()
demo_context.add_function("score_calculation", score_calculation)
demo_context.add_function("is_prime", is_prime)
demo_context.add_function("format_user_info", format_user_info)
Step 3: Add Test Data
demo_context.update({
"employee": {
"name": "Alice",
"age": 25,
"department": "Engineering",
"base_score": 85
},
"config": {
"bonus_active": True,
"multiplier": 1.2
}
})
Step 4: Test Individual Functions
calc_result = evaluate("score_calculation(employee.base_score, config.multiplier)", demo_context)
assert calc_result == 102.0 # → Mathematical function: 85 * 1.2
prime_check = evaluate("is_prime(employee.age)", demo_context)
assert prime_check == False # → Algorithmic function: 25 is not prime
info_text = evaluate('format_user_info(employee.name, employee.age, employee.department)', demo_context)
assert info_text == "Alice (25) from Engineering" # → String formatting function
Step 5: Combine Functions in Complex Rules
business_rule = """
config.bonus_active &&
score_calculation(employee.base_score, config.multiplier) > 100 &&
employee.age >= 18
"""
final_result = evaluate(business_rule, demo_context)
assert final_result == True # → Complex business rule combining multiple functions
print("✓ Complex validation functions working correctly")
Practical Example: Business Rules Engine¶
Now let's see how to combine custom functions for a real-world application - a business rules engine:
Step 1: Define Business Validation Functions
from cel import Context, evaluate
import re
from datetime import datetime, timedelta
def validate_password(password):
"""Validate password strength."""
if len(password) < 8:
return False
if not re.search(r'[A-Z]', password):
return False
if not re.search(r'[0-9]', password):
return False
return True
def days_until_expiry(expiry_date_str):
"""Calculate days until expiry."""
try:
expiry = datetime.fromisoformat(expiry_date_str.replace('Z', '+00:00'))
now = datetime.now()
# Remove timezone info for comparison
expiry_naive = expiry.replace(tzinfo=None)
delta = expiry_naive - now
return max(0, delta.days)
except:
return 0
def user_has_permission(user_id, permission, permissions_db):
"""Check if user has specific permission."""
user_perms = permissions_db.get(user_id, [])
return permission in user_perms
Step 2: Create Business Rules Context
def create_business_rules_context():
"""Create a context with business validation functions."""
context = Context()
# Register all functions
context.add_function("validate_password", validate_password)
context.add_function("days_until_expiry", days_until_expiry)
context.add_function("user_has_permission", user_has_permission)
return context
business_context = create_business_rules_context()
Step 3: Add Business Data
business_context.update({
"user": {
"id": "user123",
"password": "MySecure123",
"subscription_expires": "2030-12-31T23:59:59Z",
"verified": True
},
"permissions_db": {
"user123": ["read", "write", "admin"]
}
})
Step 4: Define Business Rules
account_access_rule = """
user.verified &&
validate_password(user.password) &&
days_until_expiry(user.subscription_expires) > 30 &&
user_has_permission(user.id, "admin", permissions_db)
"""
admin_actions_rule = """
user_has_permission(user.id, "admin", permissions_db) &&
days_until_expiry(user.subscription_expires) > 0
"""
Step 5: Evaluate and Test Rules
# Test valid user
can_access_account = evaluate(account_access_rule, business_context)
can_perform_admin_actions = evaluate(admin_actions_rule, business_context)
assert can_access_account == True # → Enterprise access control validation
assert can_perform_admin_actions == True # → Admin privilege verification
# Test with invalid user
business_context.add_variable("user", {
"id": "user456",
"password": "weak", # Fails password validation
"subscription_expires": "2023-01-01T00:00:00Z", # Expired
"verified": False
})
expired_user_access = evaluate(account_access_rule, business_context)
assert expired_user_access == False # → Security policy correctly denies access
print("✓ Business rules engine working correctly")
This example demonstrates how custom functions enable complex business logic while keeping CEL expressions readable and maintainable.
Function Best Practices¶
These patterns become essential when building production applications like those shown in Access Control Policies.
1. Error Handling¶
Step 1: Define Error-Safe Functions
def check_user_exists(user_id, database):
"""Check if user exists in database."""
return user_id in database
def get_user_status(user_id, database):
"""Get user status safely."""
user = database.get(user_id)
return user.get("status", "unknown") if user else "not_found"
def safe_divide(a, b):
"""Division with error handling."""
try:
if b == 0:
return float('inf')
return a / b
except Exception:
return 0
Step 2: Register Functions and Add Test Data
error_context = Context()
error_context.add_function("check_user_exists", check_user_exists)
error_context.add_function("get_user_status", get_user_status)
error_context.add_function("safe_divide", safe_divide)
error_context.add_variable("users_db", {
"user123": {"name": "Alice", "status": "active"}
})
Step 3: Test Error Handling
# Test individual safety functions
exists_check = evaluate('check_user_exists("user123", users_db)', error_context)
assert exists_check == True # → Database existence check with error safety
status_check = evaluate('get_user_status("user123", users_db) == "active"', error_context)
assert status_check == True # → Status validation with fallback handling
# Test combined safety check
safety_result = evaluate("""
check_user_exists("user123", users_db) &&
get_user_status("user123", users_db) == "active"
""", error_context)
assert safety_result == True # → Chained safety validations for robustness
2. Pure Functions (Recommended)¶
Step 1: Define Pure Function
# ✅ Good - pure function (no side effects)
def format_currency(amount, currency="USD"):
"""Format amount as currency string."""
return f"{currency} {amount:.2f}"
Step 2: Register Function and Add Variables
currency_context = Context()
currency_context.add_function("format_currency", format_currency)
currency_context.add_variable("price", 29.99)
Step 3: Test Pure Function
currency_result = evaluate('format_currency(price)', currency_context)
assert currency_result == "USD 29.99" # → Pure function with default parameter
eur_result = evaluate('format_currency(price, "EUR")', currency_context)
assert eur_result == "EUR 29.99" # → Pure function with explicit parameter
print("✓ Pure functions working correctly")
Advanced Context Patterns¶
The Context patterns you learned in Your First Integration work well for individual policies. But for complex applications that manage many policies and contexts - like the enterprise systems covered in Access Control Policies - you need more scalable architectures.
These patterns provide the foundation for production-ready systems:
Context Builders for Reusability¶
Complete PolicyContext Implementation
from cel import Context, evaluate
from datetime import datetime
class PolicyContext:
"""Reusable context builder for policy evaluation."""
def __init__(self):
self.context = Context()
self._setup_common_functions()
def _setup_common_functions(self):
"""Set up commonly used functions."""
def current_time():
return datetime.now()
def is_business_hours():
# For testing purposes, always return True
# In production, use: datetime.now().hour to check 9 <= hour <= 17
return True
def contains_any(text, keywords):
"""Check if text contains any of the keywords."""
return any(keyword.lower() in text.lower() for keyword in keywords)
self.context.add_function("current_time", current_time)
self.context.add_function("is_business_hours", is_business_hours)
self.context.add_function("contains_any", contains_any)
def add_user(self, user_data):
"""Add user information to context."""
self.context.add_variable("user", {
"id": user_data.get("id"),
"name": user_data.get("name"),
"email": user_data.get("email"),
"roles": user_data.get("roles", []),
"verified": user_data.get("verified", False),
"department": user_data.get("department", "unknown")
})
return self
def add_resource(self, resource_data):
"""Add resource information to context."""
self.context.add_variable("resource", {
"id": resource_data.get("id"),
"type": resource_data.get("type"),
"owner": resource_data.get("owner"),
"public": resource_data.get("public", False),
"tags": resource_data.get("tags", [])
})
return self
def add_request_info(self, method, path, ip_address):
"""Add request information to context."""
self.context.add_variable("request", {
"method": method,
"path": path,
"ip": ip_address,
"time": datetime.now().isoformat()
})
return self
def evaluate_policy(self, policy_expression):
"""Evaluate a policy expression with this context."""
return evaluate(policy_expression, self.context)
Using the Context Builder
policy_ctx = PolicyContext()
policy_ctx.add_user({
"id": "alice",
"name": "Alice Smith",
"email": "alice@company.com",
"roles": ["user", "developer"],
"verified": True,
"department": "engineering"
}).add_resource({
"id": "project-x",
"type": "repository",
"owner": "alice",
"public": False,
"tags": ["python", "web"]
}).add_request_info("GET", "/api/projects/project-x", "192.168.1.100")
Step 5: Define and Evaluate Policy
access_policy = """
user.verified &&
(user.id == resource.owner || "admin" in user.roles) &&
is_business_hours() &&
contains_any(resource.type, ["repository", "document"])
"""
access_granted = policy_ctx.evaluate_policy(access_policy)
assert access_granted == True # → Enterprise policy with reusable context builder
Context Inheritance and Composition¶
Step 1: Create Base Context Class
from cel import Context
class BaseContext:
"""Base context with common functions."""
def __init__(self):
self.context = Context()
self._add_base_functions()
def _add_base_functions(self):
def string_length(s):
return len(str(s))
def is_empty(value):
if value is None:
return True
if isinstance(value, (str, list, dict)):
return len(value) == 0
return False
self.context.add_function("string_length", string_length)
self.context.add_function("is_empty", is_empty)
Step 2: Create Specialized Web Context
class WebAppContext(BaseContext):
"""Extended context for web applications."""
def __init__(self):
super().__init__()
self._add_web_functions()
def _add_web_functions(self):
def is_safe_redirect(url):
"""Check if URL is safe for redirects."""
dangerous_schemes = ["javascript:", "data:", "vbscript:"]
return not any(url.lower().startswith(scheme) for scheme in dangerous_schemes)
def extract_domain(email):
"""Extract domain from email address."""
return email.split("@")[-1] if "@" in email else ""
self.context.add_function("is_safe_redirect", is_safe_redirect)
self.context.add_function("extract_domain", extract_domain)
Step 3: Use Inherited Context
web_context = WebAppContext()
web_context.context.update({
"redirect_url": "https://example.com/dashboard",
"user_email": "alice@company.com"
})
safety_check = evaluate("""
is_safe_redirect(redirect_url) &&
extract_domain(user_email) == "company.com"
""", web_context.context)
assert safety_check == True # → Inherited context with specialized web functions
Testing Custom Functions¶
Always test your custom functions thoroughly:
import pytest
from cel import Context, evaluate
def test_custom_functions():
"""Test custom function behavior."""
def divide_safely(a, b):
if b == 0:
return float('inf')
return a / b
context = Context()
context.add_function("divide_safely", divide_safely)
# Test normal division
result = evaluate("divide_safely(10, 2)", context)
assert result == 5.0 # → Safe division function handles normal cases
# Test division by zero
result = evaluate("divide_safely(10, 0)", context)
assert result == float('inf') # → Graceful error handling returns infinity
# Test with context variables
context.add_variable("numerator", 15)
context.add_variable("denominator", 3)
result = evaluate("divide_safely(numerator, denominator)", context)
assert result == 5.0 # → Function integration with context variables
def test_context_isolation():
"""Test that contexts don't interfere with each other."""
context1 = Context()
context1.add_variable("value", 10)
context2 = Context()
context2.add_variable("value", 20)
result1 = evaluate("value * 2", context1)
result2 = evaluate("value * 2", context2)
assert result1 == 20 # → First context: 10 * 2
assert result2 == 40 # → Second context: 20 * 2, isolated state
if __name__ == "__main__":
test_custom_functions()
test_context_isolation()
# All tests passed!
else:
# Execute tests when running through mktestdocs
test_custom_functions()
test_context_isolation()
What You've Achieved¶
You now have the advanced skills needed for production CEL implementations:
✅ Advanced Context Management - Context builders, inheritance, and composition patterns
✅ Production-Quality Functions - Error handling, pure functions, and comprehensive testing
✅ Scalable Architectures - Reusable context builders for complex applications
✅ Testing Strategies - Isolated testing and validation patterns
Ready for Production?¶
Choose your next step based on what you want to build:
🔒 Security & Access Control: - Access Control Policies - Apply these advanced patterns to build enterprise permission systems
💼 Business Applications: - Business Logic & Data Transformation - Build configurable rule engines with advanced Context patterns
🚀 Production Deployment: - Error Handling Guide - Exception types and safe-evaluation patterns
📖 Reference Material: - Python API Reference - Complete API documentation for advanced usage - CEL specification - The official CEL spec
💡 Pro Tip: With these advanced skills, you're ready to tackle enterprise-scale applications. Start with Access Control Policies or Business Logic & Data Transformation based on your use case.
Remember: CEL's power comes from combining simple, safe expressions with custom functions that encapsulate your business logic. You now have the tools to build production-ready systems!