Creating Custom Handlers
This guide shows you how to create your own custom ConditionHandler, MatchHandler, and TransformHandler to extend Hetman Pipeline with your specific business logic.
Custom Condition Handlers
Condition handlers validate data integrity. They must implement the query() method that returns True if the condition passes, False otherwise.
Basic Structure
from pipeline.handlers.base_handler.resources.constants import HandlerMode
from pipeline.handlers.condition_handler.condition_handler import ConditionHandler
class MyCustomCondition(ConditionHandler[ValueType, ArgumentType]):
"""Your custom condition handler"""
# Define which modes are supported
SUPPORT = (HandlerMode.ROOT, HandlerMode.ITEM)
# Define error messages for each mode
ERROR_TEMPLATES = {
HandlerMode.ROOT: lambda self: "Your error message here"
}
def query(self) -> bool:
"""
Implement your validation logic here.
Returns:
True if validation passes, False otherwise
"""
# Access self.value (the value being validated)
# Access self.argument (the argument passed to the handler)
return True # Your logic here
Example 1: Custom Age Validator
from pipeline.handlers.base_handler.resources.constants import HandlerMode
from pipeline.handlers.condition_handler.condition_handler import ConditionHandler
from pipeline.core.pipe.pipe import Pipe
class IsAdult(ConditionHandler[int, None]):
"""Validates that age is 18 or older"""
SUPPORT = (HandlerMode.ROOT, HandlerMode.ITEM)
ERROR_TEMPLATES = {
HandlerMode.ROOT: lambda self: f"Must be 18 or older. You are {self.value} years old."
}
def query(self) -> bool:
return self.value >= 18
# Register it with Pipe
Pipe.Condition.IsAdult = IsAdult
# Use it
result = Pipe(
value=16,
type=int,
conditions={Pipe.Condition.IsAdult: None}
).run()
print(result.condition_errors)
# [{'id': 'is_adult', 'msg': 'Must be 18 or older. You are 16 years old.', 'value': 16}]
Example 2: Custom Business Rule
from pipeline.handlers.base_handler.resources.constants import HandlerMode
from pipeline.handlers.condition_handler.condition_handler import ConditionHandler
from pipeline.core.pipe.pipe import Pipe
class IsValidCouponCode(ConditionHandler[str, list]):
"""Validates coupon code against a list of valid codes"""
SUPPORT = (HandlerMode.ROOT,)
ERROR_TEMPLATES = {
HandlerMode.ROOT: lambda self: f"Invalid coupon code. Valid codes: {', '.join(self.argument[:3])}..."
}
def query(self) -> bool:
# self.value is the coupon code
# self.argument is the list of valid codes
return self.value.upper() in [code.upper() for code in self.argument]
# Register and use
Pipe.Condition.IsValidCouponCode = IsValidCouponCode
valid_codes = ["SUMMER2024", "WELCOME10", "FREESHIP"]
result = Pipe(
value="INVALID",
type=str,
conditions={Pipe.Condition.IsValidCouponCode: valid_codes}
).run()
print(result.condition_errors)
# [{'id': 'is_valid_coupon_code', 'msg': 'Invalid coupon code. Valid codes: SUMMER2024, WELCOME10, FREESHIP...', 'value': 'INVALID'}]
Example 3: Context-Aware Validation
from pipeline.handlers.base_handler.resources.constants import HandlerMode
from pipeline.handlers.condition_handler.condition_handler import ConditionHandler
from pipeline.core.pipeline.pipeline import Pipeline
from pipeline.core.pipe.pipe import Pipe
from pipeline.handlers.base_handler.handler_modifiers import Context
class IsGreaterThanField(ConditionHandler[int | float, int | float]):
"""Validates that value is greater than another field in context"""
SUPPORT = (HandlerMode.CONTEXT,)
ERROR_TEMPLATES = {
HandlerMode.CONTEXT: lambda self: f"Must be greater than {self.input_argument} ({self.argument})"
}
def query(self) -> bool:
return self.value > self.argument
# Register
Pipe.Condition.IsGreaterThanField = IsGreaterThanField
# Use in pipeline
pipeline = Pipeline(
min_price={"type": int},
max_price={
"type": int,
"conditions": {
Context(Pipe.Condition.IsGreaterThanField): "min_price"
}
}
)
result = pipeline.run(data={
"min_price": 100,
"max_price": 50 # Invalid: should be > 100
})
print(result.errors)
# {'max_price': [{'id': 'is_greater_than_field', 'msg': 'Must be greater than min_price (100)', 'value': 50}]}
Custom Match Handlers
Match handlers extend ConditionHandler and typically use regex patterns. They inherit the search() and fullmatch() helper methods.
Custom Transform Handlers
Transform handlers modify values. They must implement the operation() method that returns the transformed value.
Basic Structure
from pipeline.handlers.base_handler.resources.constants import HandlerMode
from pipeline.handlers.transform_handler.transform_handler import TransformHandler
class MyCustomTransform(TransformHandler[ValueType, ArgumentType]):
"""Your custom transform handler"""
SUPPORT = (HandlerMode.ROOT, HandlerMode.ITEM)
def operation(self) -> ValueType:
"""
Implement your transformation logic here.
Returns:
The transformed value
"""
# Access self.value (the value to transform)
# Access self.argument (the argument passed to the handler)
return self.value # Your transformation here
Example 1: Custom String Sanitizer
from pipeline.handlers.base_handler.resources.constants import HandlerMode
from pipeline.handlers.transform_handler.transform_handler import TransformHandler
from pipeline.core.pipe.pipe import Pipe
import re
class RemoveSpecialChars(TransformHandler[str, None]):
"""Removes all special characters, keeping only letters, numbers, and spaces"""
SUPPORT = (HandlerMode.ROOT, HandlerMode.ITEM)
def operation(self) -> str:
return re.sub(r'[^a-zA-Z0-9\s]', '', self.value)
# Register
Pipe.Transform.RemoveSpecialChars = RemoveSpecialChars
# Use
result = Pipe(
value="Hello! @World# 2024$",
type=str,
transform={Pipe.Transform.RemoveSpecialChars: None}
).run()
print(result.value) # "Hello World 2024"
Example 2: Custom Price Formatter
from pipeline.handlers.base_handler.resources.constants import HandlerMode
from pipeline.handlers.transform_handler.transform_handler import TransformHandler
from pipeline.core.pipe.pipe import Pipe
class RoundToDecimalPlaces(TransformHandler[float, int]):
"""Rounds a float to specified decimal places"""
SUPPORT = (HandlerMode.ROOT, HandlerMode.ITEM)
def operation(self) -> float:
# self.argument is the number of decimal places
return round(self.value, self.argument)
# Register
Pipe.Transform.RoundToDecimalPlaces = RoundToDecimalPlaces
# Use
result = Pipe(
value=19.99567,
type=float,
transform={Pipe.Transform.RoundToDecimalPlaces: 2}
).run()
print(result.value) # 20.0
Example 3: Custom List Transformer
from pipeline.handlers.base_handler.resources.constants import HandlerMode
from pipeline.handlers.transform_handler.transform_handler import TransformHandler
from pipeline.core.pipe.pipe import Pipe
class SortList(TransformHandler[list, bool]):
"""Sorts a list. Argument: True for ascending, False for descending"""
SUPPORT = (HandlerMode.ROOT,)
def operation(self) -> list:
return sorted(self.value, reverse=not self.argument)
# Register
Pipe.Transform.SortList = SortList
# Use
result = Pipe(
value=[5, 2, 8, 1, 9],
type=list,
transform={Pipe.Transform.SortList: True} # Ascending
).run()
print(result.value) # [1, 2, 5, 8, 9]
Example 4: Custom Data Masking
from pipeline.handlers.base_handler.resources.constants import HandlerMode
from pipeline.handlers.transform_handler.transform_handler import TransformHandler
from pipeline.core.pipe.pipe import Pipe
class MaskCreditCard(TransformHandler[str, None]):
"""Masks credit card number, showing only last 4 digits"""
SUPPORT = (HandlerMode.ROOT, HandlerMode.ITEM)
def operation(self) -> str:
# Remove spaces and dashes
clean = self.value.replace(" ", "").replace("-", "")
# Mask all but last 4 digits
if len(clean) >= 4:
return "*" * (len(clean) - 4) + clean[-4:]
return clean
# Register
Pipe.Transform.MaskCreditCard = MaskCreditCard
# Use
result = Pipe(
value="1234-5678-9012-3456",
type=str,
transform={Pipe.Transform.MaskCreditCard: None}
).run()
print(result.value) # "************3456"
Best Practices
Type Hints
Use proper type hints for ValueType and ArgumentType:
class MyHandler(ConditionHandler[str, int]):
# str = value type
# int = argument type
Type Safety
Hetman Pipeline ensures type safety by validating the value and argument type before running the handler.
Nested Types
Nested types like list[str] are not supported and any other nested types are not supported. Use list instead.
Support Multiple Modes
If your handler can work with collections, support HandlerMode.ITEM:
SUPPORT = (HandlerMode.ROOT, HandlerMode.ITEM)
Handler ID
The handler ID is automatically generated from the class name in snake_case:
class IsValidCouponCode # -> id: 'is_valid_coupon_code'
class ProductSKU # -> id: 'product_sku'
Error Templates
Provide clear, user-friendly error messages:
ERROR_TEMPLATES = {
HandlerMode.ROOT: lambda self: f"Clear message with {self.value} and {self.argument}"
}
Registering Custom Handlers
After creating your custom handler, register it with Pipe:
# For conditions
Pipe.Condition.MyCustomCondition = MyCustomCondition
# For matches
Pipe.Match.Format.MyCustomMatch = MyCustomMatch
# or
Pipe.Match.Text.MyCustomMatch = MyCustomMatch
# For transforms
Pipe.Transform.MyCustomTransform = MyCustomTransform
Then use it like any built-in handler:
result = Pipe(
value="test",
type=str,
conditions={Pipe.Condition.MyCustomCondition: argument},
matches={Pipe.Match.Format.MyCustomMatch: argument},
transform={Pipe.Transform.MyCustomTransform: argument}
).run()
Next Steps
- Learn about Error Customization to customize error messages
- Explore Handler Modifiers to use
Item()andContext()with your handlers - Check Pipeline Hooks for pre/post processing logic