Quick Start Tutorial - adnanhd/observer-pattern GitHub Wiki

Quick Start Tutorial

Get up and running with CallPyBack in minutes! This tutorial covers the essential patterns you'll use in 90% of applications.

📦 Installation

pip install callpyback

🎯 Your First CallPyBack Function

Let's start with the simplest possible example:

from callpyback import CallPyBack

@CallPyBack()
def hello_world():
    return "Hello, CallPyBack!"

result = hello_world()
print(result)  # "Hello, CallPyBack!"

What happened? CallPyBack decorated your function but didn't change its behavior. The real power comes when you add observers!

🔍 Adding Your First Observer

Let's add some basic logging:

from callpyback import CallPyBack, on_success

def log_success(result):
    print(f"✅ Function succeeded with result: {result.value}")

@CallPyBack(observers=[on_success(log_success)])
def calculate(x, y):
    return x + y

result = calculate(5, 3)
# Output: ✅ Function succeeded with result: 8
print(f"Result: {result}")  # Result: 8

Key concepts:

  • Observers: Functions that monitor your decorated function
  • on_success: Factory function that creates observers for successful executions
  • result.value: The actual return value from your function

🛡️ Error Handling

Let's add error handling to make our functions more robust:

from callpyback import CallPyBack, on_success, on_failure

def log_success(result):
    print(f"✅ Success: {result.value}")

def log_error(result):
    print(f"❌ Error: {result.exception}")

@CallPyBack(
    observers=[on_success(log_success), on_failure(log_error)],
    exception_classes=(ValueError, TypeError),  # Catch these exceptions
    default_return=0  # Return this value on error
)
def safe_divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

# Test successful execution
result = safe_divide(10, 2)
# Output: ✅ Success: 5.0
print(f"Result: {result}")  # Result: 5.0

# Test error handling
result = safe_divide(10, 0)
# Output: ❌ Error: Cannot divide by zero
print(f"Result: {result}")  # Result: 0 (default_return)

Key concepts:

  • exception_classes: Tuple of exception types to catch
  • default_return: Value to return when an exception is caught
  • on_failure: Observer for failed executions

📊 Performance Monitoring

Monitor function performance with built-in observers:

import time
from callpyback import CallPyBack
from callpyback.observers.builtin import MetricsObserver, TimingObserver

# Create monitoring observers
metrics = MetricsObserver()
timing = TimingObserver(threshold=0.1)  # Alert if execution > 100ms

@CallPyBack(observers=[metrics, timing])
def slow_function(duration):
    time.sleep(duration)
    return f"Slept for {duration} seconds"

# Execute function multiple times
slow_function(0.05)  # Fast execution
slow_function(0.15)  # Slow execution (will trigger timing alert)
slow_function(0.02)  # Fast execution

# Get performance metrics
performance_data = metrics.get_metrics()
print(f"Total executions: {performance_data['total_executions']}")
print(f"Average time: {performance_data['average_execution_time']*1000:.1f}ms")

# Check slow executions
slow_executions = timing.get_slow_executions()
print(f"Slow executions detected: {len(slow_executions)}")

Key concepts:

  • MetricsObserver: Collects execution statistics
  • TimingObserver: Detects slow executions
  • Built-in observers: Ready-to-use monitoring components

🔬 Variable Extraction

Capture local variables during function execution:

from callpyback import CallPyBack, on_completion

captured_variables = []

def capture_vars(local_variables):
    captured_variables.append(local_variables)
    print(f"📋 Captured variables: {local_variables}")

@CallPyBack(
    observers=[on_completion(capture_vars)],
    variable_names=["step1", "step2", "final_result"]
)
def data_processing(input_data):
    step1 = input_data.upper()
    step2 = step1.replace(" ", "_")
    final_result = f"processed_{step2}"
    return final_result

result = data_processing("hello world")
# Output: 📋 Captured variables: {'step1': 'HELLO WORLD', 'step2': 'HELLO_WORLD', 'final_result': 'processed_HELLO_WORLD'}
print(f"Result: {result}")

Key concepts:

  • variable_names: List of local variable names to capture
  • on_completion: Observer that always executes (success or failure)
  • Variable extraction: Powerful debugging and auditing capability

🎯 Observer States

Control when observers execute by subscribing to specific states:

from callpyback import CallPyBack, on_call, on_success, on_failure, on_completion

def before_execution(context):
    print(f"🚀 Starting {context.function_signature.name}")

def after_success(result):
    print(f"✅ Success: {result.value}")

def after_failure(result):
    print(f"❌ Failed: {result.exception}")

def always_runs(context):
    print(f"🏁 Finished with state: {context.state.name}")

@CallPyBack(
    observers=[
        on_call(before_execution),      # Before execution
        on_success(after_success),      # Only on success
        on_failure(after_failure),      # Only on failure  
        on_completion(always_runs)      # Always executes
    ],
    exception_classes=(ValueError,),
    default_return="error_handled"
)
def stateful_function(should_fail=False):
    if should_fail:
        raise ValueError("Intentional error")
    return "success_result"

# Test successful execution
print("=== Testing Success ===")
stateful_function(False)
# Output:
# 🚀 Starting stateful_function
# ✅ Success: success_result
# 🏁 Finished with state: COMPLETED

print("\n=== Testing Failure ===")
stateful_function(True)
# Output:
# 🚀 Starting stateful_function
# ❌ Failed: Intentional error
# 🏁 Finished with state: COMPLETED

Key concepts:

  • on_call: Executes before function starts
  • State-based observers: Different observers for different execution phases
  • context: Rich information about function execution

🏭 Production Example

Here's a more realistic example combining multiple features:

import time
import random
from callpyback import CallPyBack, on_call, on_success, on_failure
from callpyback.observers.builtin import MetricsObserver, LoggingObserver

# Setup monitoring
metrics = MetricsObserver(priority=100)
logger = LoggingObserver(priority=50)

# Track business metrics
business_metrics = {"orders_processed": 0, "total_revenue": 0}

def track_business_metrics(result):
    if isinstance(result.value, dict) and "revenue" in result.value:
        business_metrics["orders_processed"] += 1
        business_metrics["total_revenue"] += result.value["revenue"]

def log_order_start(context):
    print(f"📦 Processing order: {context.arguments}")

def handle_order_failure(result):
    print(f"🚨 Order processing failed: {result.exception}")
    # Could trigger alerts, create support tickets, etc.

@CallPyBack(
    observers=[
        metrics,                                # Built-in metrics
        logger,                                 # Built-in logging
        on_call(log_order_start),              # Business logging
        on_success(track_business_metrics),    # Business metrics
        on_failure(handle_order_failure)       # Error handling
    ],
    variable_names=["order_total", "processing_fee"],
    exception_classes=(ValueError, ConnectionError),
    default_return={"status": "failed", "order_id": None}
)
def process_order(customer_id, items, payment_method):
    # Simulate order processing
    order_total = sum(item["price"] for item in items)
    processing_fee = order_total * 0.03
    
    # Simulate potential failures
    if random.random() < 0.1:  # 10% failure rate
        raise ConnectionError("Payment gateway unavailable")
    
    if order_total <= 0:
        raise ValueError("Invalid order total")
    
    # Simulate processing time
    time.sleep(random.uniform(0.01, 0.05))
    
    return {
        "status": "completed",
        "order_id": f"ORD_{random.randint(1000, 9999)}",
        "revenue": order_total,
        "fee": processing_fee
    }

# Process some orders
orders = [
    (1001, [{"item": "laptop", "price": 999.99}], "credit_card"),
    (1002, [{"item": "mouse", "price": 29.99}, {"item": "keyboard", "price": 79.99}], "paypal"),
    (1003, [{"item": "monitor", "price": 299.99}], "credit_card"),
    (1004, [], "credit_card"),  # Invalid order - empty items
    (1005, [{"item": "tablet", "price": 499.99}], "credit_card")
]

print("=== Processing Orders ===")
for customer_id, items, payment_method in orders:
    result = process_order(customer_id, items, payment_method)
    print(f"Order result: {result}")
    print()

# Show final metrics
print("=== Final Metrics ===")
perf_metrics = metrics.get_metrics()
print(f"Total orders attempted: {perf_metrics['total_executions']}")
print(f"Success rate: {(perf_metrics['function_stats']['process_order']['successes'] / perf_metrics['function_stats']['process_order']['calls'] * 100):.1f}%")
print(f"Average processing time: {perf_metrics['average_execution_time']*1000:.1f}ms")
print(f"Orders completed: {business_metrics['orders_processed']}")
print(f"Total revenue: ${business_metrics['total_revenue']:.2f}")

🚀 Next Steps

Congratulations! You've learned the fundamentals of CallPyBack. Here's what to explore next:

📚 Learn More About:

🎯 Try These Patterns:

  • API Monitoring: Monitor HTTP request/response cycles
  • Database Auditing: Track query performance and access patterns
  • ML Pipeline Monitoring: Monitor training and inference workflows
  • Financial Transaction Auditing: Compliance and fraud detection

🔧 Advanced Features:

💡 Pro Tips

1. Start Simple

Begin with basic observers (on_success, on_failure) and gradually add complexity.

2. Use Built-in Observers

The built-in observers (MetricsObserver, LoggingObserver, TimingObserver) cover most production needs.

3. Think About Priorities

Assign logical priorities to your observers:

  • Security/Audit: 800-999
  • Business Logic: 500-799
  • Monitoring: 100-499
  • Debug/Dev: 1-99

4. Variable Extraction is Powerful

Use variable extraction for debugging, auditing, and compliance. It's like having a debugger built into production code.

5. Error Handling Strategy

Always define exception_classes and default_return for production functions. This ensures graceful degradation.

🔗 Common Patterns

Web Application Monitoring

@CallPyBack(
    observers=[
        on_call(lambda ctx: log_request(ctx.arguments)),
        on_success(lambda result: log_response(result.value)),
        on_failure(lambda result: log_error(result.exception))
    ]
)
def api_endpoint(request_data):
    return process_request(request_data)

Database Operation Tracking

@CallPyBack(
    observers=[timing_observer, metrics_observer],
    variable_names=["query", "rows_affected"]
)
def database_operation(table, operation):
    query = f"SELECT * FROM {table}"
    rows_affected = execute_query(query)
    return rows_affected

Batch Processing Monitoring

@CallPyBack(
    observers=[progress_observer, metrics_observer],
    variable_names=["processed_count", "error_count"]
)
def batch_processor(items):
    processed_count = 0
    error_count = 0
    
    for item in items:
        try:
            process_item(item)
            processed_count += 1
        except Exception:
            error_count += 1
    
    return {"processed": processed_count, "errors": error_count}

Ready for more? Check out the Observer System documentation to understand the powerful patterns behind CallPyBack!