Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,23 @@ on:
jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build

- name: Build package
run: python -m build

- name: Store the distribution packages
uses: actions/upload-artifact@v4
with:
Expand All @@ -40,14 +40,14 @@ jobs:
url: https://pypi.org/p/action-dispatch
permissions:
id-token: write # IMPORTANT: mandatory for trusted publishing

steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

Expand All @@ -60,14 +60,14 @@ jobs:
url: https://test.pypi.org/p/action-dispatch
permissions:
id-token: write # IMPORTANT: mandatory for trusted publishing

steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/

- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
Expand Down
16 changes: 8 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,40 +13,40 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', '3.10', '3.11', '3.12']

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"

- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 action_dispatch --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 action_dispatch --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

- name: Type check with mypy
run: |
mypy action_dispatch

- name: Test with unittest
run: |
python -m unittest discover tests -v

- name: Test with coverage
run: |
coverage run -m unittest discover tests
coverage xml

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
Expand Down
14 changes: 8 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v6.0.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
Expand All @@ -10,26 +10,28 @@ repos:
- id: check-json

- repo: https://github.com/psf/black
rev: 23.3.0
rev: 25.1.0
hooks:
- id: black
language_version: python3

- repo: https://github.com/pycqa/flake8
rev: 6.0.0
rev: 7.3.0
hooks:
- id: flake8
additional_dependencies: [flake8-docstrings]
args: ["--max-line-length=88", "--extend-ignore=E203,W503,D100,D101,D102,D104,D107,D200,D401"]

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.3.0
rev: v1.17.1
hooks:
- id: mypy
additional_dependencies: [types-all]
additional_dependencies: [types-setuptools, types-requests]
args: [--strict, --ignore-missing-imports]
exclude: ^tests/

- repo: https://github.com/pycqa/isort
rev: 5.12.0
rev: 6.0.1
hooks:
- id: isort
args: ["--profile", "black"]
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,10 @@ def test_feature_description(self):
"""Test that the feature works as expected."""
# Arrange
setup_test_data()

# Act
result = perform_action()

# Assert
self.assertEqual(result, expected_value)
```
Expand Down
36 changes: 18 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ A powerful and flexible Python library for multi-dimensional action dispatching.

## Features

**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
**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

## Installation

Expand All @@ -34,7 +34,7 @@ dispatcher = ActionDispatcher(['role', 'environment'])
def admin_create_user(params):
return f"Admin creating user: {params.get('username')}"

@dispatcher.handler("create_user", role="manager")
@dispatcher.handler("create_user", role="manager")
def manager_create_user(params):
return f"Manager creating user: {params.get('username')}"

Expand All @@ -57,15 +57,15 @@ print(result) # "Admin creating user: john"
```python
dispatcher = ActionDispatcher(['role', 'environment', 'feature_flag'])

@dispatcher.handler("process_payment",
role="admin",
environment="production",
@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",
@dispatcher.handler("process_payment",
role="admin",
environment="production")
def default_payment_handler(params):
return "Processing with default system"
Expand Down Expand Up @@ -132,7 +132,7 @@ def get_users_admin_v2(params):
"permissions": ["read", "write", "delete"]
}

@api_dispatcher.handler("get_users", role="user", api_version="v2")
@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']),
Expand All @@ -143,7 +143,7 @@ def get_users_regular_v2(params):
def api_get_users(request):
try:
result = api_dispatcher.dispatch(
request.user,
request.user,
"get_users",
request_id=request.id
)
Expand All @@ -157,14 +157,14 @@ def api_get_users(request):
```python
service_dispatcher = ActionDispatcher(['environment', 'service_version'])

@service_dispatcher.handler("process_order",
environment="production",
@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",
@service_dispatcher.handler("process_order",
environment="staging")
def process_order_staging(params):
# Use staging database with verbose logging
Expand All @@ -177,8 +177,8 @@ def process_order_staging(params):
plugin_dispatcher = ActionDispatcher(['plugin_type', 'version'])

# Plugins can register their handlers
@plugin_dispatcher.handler("transform_data",
plugin_type="image_processor",
@plugin_dispatcher.handler("transform_data",
plugin_type="image_processor",
version="2.0")
def image_transform_v2(params):
return enhanced_image_transform(params['data'])
Expand Down Expand Up @@ -214,7 +214,7 @@ Decorator to register a global handler that works across all contexts.
#### `register(action, handler, **kwargs)`
Programmatically register a handler.

- `action` (str): Action name
- `action` (str): Action name
- `handler` (callable): Handler function
- `**kwargs`: Dimension values for routing

Expand Down Expand Up @@ -276,7 +276,7 @@ python -m unittest tests.test_action_dispatcher.TestActionDispatcher -v
# Format code
black action_dispatch tests

# Lint code
# Lint code
flake8 action_dispatch tests

# Type checking
Expand Down
9 changes: 4 additions & 5 deletions action_dispatch/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
"""
Action Dispatch - A flexible action dispatching system with multi-dimensional routing capabilities.
"""Action Dispatch - Multi-dimensional routing system.

A powerful Python library that provides dynamic action dispatching based on context dimensions
such as user roles, environments, API versions, and any custom attributes.
A Python library providing dynamic action dispatching based on context
dimensions like user roles, environments, API versions, and custom attributes.

Key Features:
- Multi-dimensional routing
Expand Down Expand Up @@ -33,9 +32,9 @@
from .action_dispatcher import ActionDispatcher
from .exceptions import (
ActionDispatchError,
InvalidDimensionError,
HandlerNotFoundError,
InvalidActionError,
InvalidDimensionError,
)

__version__ = "0.1.0"
Expand Down
18 changes: 9 additions & 9 deletions action_dispatch/action_dispatcher.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from functools import partial
from collections import defaultdict
import warnings
from typing import Any, Callable, Dict, List, Optional, Union
from collections import defaultdict
from functools import partial
from typing import Any, Callable, Optional, Union

try:
from .exceptions import (
InvalidDimensionError,
HandlerNotFoundError,
InvalidActionError,
InvalidDimensionError,
)
except ImportError:
pass
Expand All @@ -21,7 +21,8 @@ class ActionDispatcher:
def __init__(self, dimensions: Optional[list[str]] = None) -> None:
if dimensions is not None and not isinstance(dimensions, list):
warnings.warn(
f"ActionDispatcher dimensions should be a list, got {type(dimensions).__name__}. Setting to empty list."
f"ActionDispatcher dimensions should be a list, got "
f"{type(dimensions).__name__}. Setting to empty list."
)
self.dimensions = []
else:
Expand All @@ -44,10 +45,9 @@ def _create_dynamic_methods(self) -> None:
def decorator_factory(
dimensions: list[str],
) -> Callable[[str], Callable[..., Callable[..., Any]]]:
def decorator(
action: str, **scope_kwargs: Any
) -> Callable[
[Callable[[dict[str, Any]], Any]], Callable[[dict[str, Any]], Any]
def decorator(action: str, **scope_kwargs: Any) -> Callable[
[Callable[[dict[str, Any]], Any]],
Callable[[dict[str, Any]], Any],
]:
def wrapper(
func: Callable[[dict[str, Any]], Any],
Expand Down
8 changes: 5 additions & 3 deletions action_dispatch/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class ActionDispatchError(Exception):
class InvalidDimensionError(ActionDispatchError):
"""Raised when an invalid dimension parameter is provided."""

def __init__(self, dimension, available_dimensions):
def __init__(self, dimension: str, available_dimensions: list[str]) -> None:
self.dimension = dimension
self.available_dimensions = available_dimensions
super().__init__(
Expand All @@ -24,7 +24,7 @@ def __init__(self, dimension, available_dimensions):
class HandlerNotFoundError(ActionDispatchError):
"""Raised when no handler is found for a given action and rules."""

def __init__(self, action, rules):
def __init__(self, action: str, rules: dict[str, str]) -> None:
self.action = action
self.rules = rules
super().__init__(f"No handler found for action '{action}' with rules {rules}")
Expand All @@ -33,5 +33,7 @@ def __init__(self, action, rules):
class InvalidActionError(ActionDispatchError):
"""Raised when an invalid action name is provided."""

def __init__(self, message="Action name must be provided for dispatching."):
def __init__(
self, message: str = "Action name must be provided for dispatching."
) -> None:
super().__init__(message)
Loading
Loading