A powerful and flexible Python library for multi-dimensional action dispatching. Route function calls dynamically based on context dimensions like user roles, environments, API versions, or any custom attributes.
Multi-dimensional Routing - Dispatch based on multiple context attributes simultaneously Dynamic Handler Registration - Register handlers using decorators or programmatically Flexible Context Matching - Support for exact matches and fallback strategies
Requirements: Python 3.9+
pip install action-dispatchfrom action_dispatch import ActionDispatcher
# Create dispatcher with dimensions
dispatcher = ActionDispatcher(['role', 'environment'])
# Register handlers using decorators
@dispatcher.handler("create_user", role="admin")
def admin_create_user(params):
return f"Admin creating user: {params.get('username')}"
@dispatcher.handler("create_user", role="manager")
def manager_create_user(params):
return f"Manager creating user: {params.get('username')}"
# Define context class
class RequestContext:
def __init__(self, role, environment):
self.role = role
self.environment = environment
# Dispatch actions based on context
admin_context = RequestContext("admin", "production")
result = dispatcher.dispatch(admin_context, "create_user", username="john")
print(result) # "Admin creating user: john"dispatcher = ActionDispatcher(['role', 'environment', 'feature_flag'])
@dispatcher.handler("process_payment",
role="admin",
environment="production",
feature_flag="new_payment_system")
def new_payment_handler(params):
return "Processing with new payment system"
@dispatcher.handler("process_payment",
role="admin",
environment="production")
def default_payment_handler(params):
return "Processing with default system"# Global handlers work across all contexts
@dispatcher.global_handler("health_check")
def health_check(params):
return {"status": "healthy", "timestamp": time.time()}
# Global handlers have priority over scoped handlers
result = dispatcher.dispatch(any_context, "health_check")def custom_handler(params):
return "Custom response"
# Register without decorators
dispatcher.register("custom_action", custom_handler, role="user")
# Register global handler
dispatcher.register_global("system_status", lambda p: "OK")from action_dispatch import HandlerNotFoundError, InvalidDimensionError
try:
result = dispatcher.dispatch(context, "unknown_action")
except HandlerNotFoundError as e:
print(f"No handler found for action: {e.action}")
print(f"Context rules: {e.rules}")
try:
dispatcher.register("action", handler, invalid_dimension="value")
except InvalidDimensionError as e:
print(f"Invalid dimension: {e.dimension}")
print(f"Available dimensions: {e.available_dimensions}")Enable LRU caching to improve performance when you have many dimensions or high-frequency lookups with repeated action + scope combinations.
# Enable cache on initialization
dispatcher = ActionDispatcher(
dimensions=['region', 'platform', 'version', 'tier'],
enable_cache=True,
cache_maxsize=512
)
@dispatcher.handler("get_data", region="asia", platform="mobile")
def get_data_asia_mobile(params):
return "data for asia mobile"
# First lookup - cache miss
result = dispatcher.get_handler("get_data", region="asia", platform="mobile")
# Second lookup - cache hit (faster)
result = dispatcher.get_handler("get_data", region="asia", platform="mobile")
# Check cache statistics
info = dispatcher.cache_info()
print(f"Cache hits: {info['hits']}, misses: {info['misses']}")
# Output: Cache hits: 1, misses: 1# Create dispatcher without cache
dispatcher = ActionDispatcher(dimensions=['platform'])
# Enable cache later
dispatcher.enable_cache(maxsize=256)
print(dispatcher.is_cache_enabled) # True
# Clear cache manually (useful after bulk handler registration)
dispatcher.clear_cache()
# Disable cache when no longer needed
dispatcher.disable_cache()
print(dispatcher.is_cache_enabled) # False| Scenario | Recommendation |
|---|---|
| 2-5 dimensions | Cache optional |
| 5-10 dimensions | Consider enabling cache |
| 10+ dimensions | Recommend enabling cache |
| High QPS (>10K) | Recommend enabling cache |
| Dynamic handler registration | Cache auto-invalidates on registration |
from action_dispatch import ActionDispatcher
# Set up dispatcher for API routing
api_dispatcher = ActionDispatcher(['role', 'api_version'])
@api_dispatcher.handler("get_users", role="admin", api_version="v2")
def get_users_admin_v2(params):
return {
"users": get_all_users(),
"total": count_all_users(),
"permissions": ["read", "write", "delete"]
}
@api_dispatcher.handler("get_users", role="user", api_version="v2")
def get_users_regular_v2(params):
return {
"users": get_user_own_data(params['context_object']),
"permissions": ["read"]
}
# API endpoint
def api_get_users(request):
try:
result = api_dispatcher.dispatch(
request.user,
"get_users",
request_id=request.id
)
return JsonResponse(result)
except HandlerNotFoundError:
return JsonResponse({"error": "Forbidden"}, status=403)service_dispatcher = ActionDispatcher(['environment', 'service_version'])
@service_dispatcher.handler("process_order",
environment="production",
service_version="v2")
def process_order_prod_v2(params):
# Use production database and new algorithm
return production_order_processor.process(params['order_data'])
@service_dispatcher.handler("process_order",
environment="staging")
def process_order_staging(params):
# Use staging database with verbose logging
return staging_order_processor.process(params['order_data'])plugin_dispatcher = ActionDispatcher(['plugin_type', 'version'])
# Plugins can register their handlers
@plugin_dispatcher.handler("transform_data",
plugin_type="image_processor",
version="2.0")
def image_transform_v2(params):
return enhanced_image_transform(params['data'])
# Dynamic plugin loading
for plugin in load_plugins():
plugin.register_handlers(plugin_dispatcher)
# Process with appropriate plugin
result = plugin_dispatcher.dispatch(context, "transform_data", data=input_data)Create a new dispatcher with optional dimensions and caching.
dimensions(list, optional): List of dimension names for routingenable_cache(bool, optional): Enable LRU cache for handler lookups (default: False)cache_maxsize(int, optional): Maximum size of the LRU cache (default: 256)
Decorator to register a handler for specific action and dimensions.
action(str): Action name**kwargs: Dimension values for routing
Decorator to register a global handler that works across all contexts.
action(str): Action name
Programmatically register a handler.
action(str): Action namehandler(callable): Handler function**kwargs: Dimension values for routing
Dispatch an action based on context.
context_object: Object with dimension attributesaction_name(str): Action to dispatch**kwargs: Additional parameters passed to handler
Enable LRU cache for handler lookups at runtime.
maxsize(int, optional): Maximum cache size (uses existing value if None)
Disable cache and clear cached data.
Clear all cached handler lookups.
Get cache statistics. Returns None if cache is disabled.
Returns a dict with:
hits: Number of cache hitsmisses: Number of cache missesmaxsize: Maximum cache sizecurrsize: Current number of cached items
Property that returns True if cache is currently enabled.
ActionDispatchError: Base exception classInvalidDimensionError: Raised for invalid dimension parametersHandlerNotFoundError: Raised when no handler is foundInvalidActionError: Raised for invalid action names
# Clone the repository
git clone https://github.com/eowl/action-dispatch.git
cd action-dispatch
# Create virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install development dependencies
pip install -e ".[dev]"
# Install pre-commit hooks
pre-commit install# Run all tests
python -m unittest discover tests -v
# Run with coverage
coverage run -m unittest discover tests
coverage report
coverage html
# Run specific test file
python -m unittest tests.test_action_dispatcher -v
# Run specific test class
python -m unittest tests.test_action_dispatcher.TestActionDispatcher -v# Format code
black action_dispatch tests
# Lint code
flake8 action_dispatch tests
# Type checking
mypy action_dispatchWe welcome contributions! Please see our Contributing Guide for details.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Add tests for your changes
- Ensure all tests pass (
python -m pytest) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
See CHANGELOG.md for a list of changes and releases.
- Inspired by the need for flexible routing in complex applications
- Built with modern Python best practices
- Thoroughly tested and documented