Metadata-Version: 2.4
Name: django-package-hooks
Version: 1.0.0
Summary: Reusable hook system for Django packages
Author: Django Hooks Contributors
License: MIT
Project-URL: Homepage, https://github.com/yourusername/django-package-hooks
Project-URL: Documentation, https://github.com/yourusername/django-package-hooks/blob/main/README.md
Project-URL: Repository, https://github.com/yourusername/django-package-hooks
Project-URL: Issues, https://github.com/yourusername/django-package-hooks/issues
Keywords: django,hooks,plugins,extensions,middleware
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Framework :: Django
Classifier: Framework :: Django :: 3.2
Classifier: Framework :: Django :: 4.0
Classifier: Framework :: Django :: 4.1
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.0
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-django>=4.5; extra == "dev"
Requires-Dist: black>=23.0; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Requires-Dist: ruff>=0.1; extra == "dev"
Dynamic: license-file

# Django Hooks

A reusable, extensible hook system for Django packages that allows developers to inject custom logic before and after operations.

## Features

- **Two-Phase Hooks**: PRE hooks (can reject) and POST hooks (reactions only)
- **Priority-Based**: Control execution order with numeric priorities
- **Structured Errors**: Machine-readable error codes for frontend integration
- **Type-Safe**: Full type hints and Protocol support
- **Flexible**: Works with any Django package or application
- **Well-Tested**: Comprehensive test coverage

## Installation

```bash
pip install django-hooks
```

Or add to your project:

```python
# Copy django_hooks/ directory to your project
from django_hooks import HookRegistry, HookType, register_hook
```

## Quick Start

### 1. Define Your Hook Context

Extend `HookContext` with your domain-specific fields:

```python
from dataclasses import dataclass, field
from django_hooks import HookContext

@dataclass(frozen=True)
class CronjobHookContext(HookContext):
    """Context for cronjob operations."""
    job_id: str
    schedule: str
    command: str
    user_id: int
```

### 2. Create Hook Functions

```python
from django_hooks import HookType, HookRejectionError

def check_permissions(context: CronjobHookContext) -> bool:
    """PRE hook: Validate user has permission to schedule jobs."""
    if not user_has_permission(context.user_id, 'schedule_jobs'):
        raise HookRejectionError(
            error_code="PERMISSION_DENIED",
            message="You don't have permission to schedule jobs",
            details={"required_permission": "schedule_jobs"}
        )
    return True

def send_notification(context: CronjobHookContext) -> None:
    """POST hook: Notify admin when job is scheduled."""
    send_email(
        to="admin@example.com",
        subject=f"New job scheduled: {context.job_id}",
        body=f"Job '{context.command}' scheduled with cron: {context.schedule}"
    )
```

### 3. Register Hooks

```python
from django_hooks import register_hook, HookType

# Register PRE hook (validation)
register_hook(
    name="check_permissions",
    hook_type=HookType.PRE,
    callback=check_permissions,
    operation="schedule",
    priority=10  # Higher priority (runs first)
)

# Register POST hook (notification)
register_hook(
    name="send_notification",
    hook_type=HookType.POST,
    callback=send_notification,
    operation="schedule",
    priority=100  # Lower priority (runs later)
)
```

### 4. Execute Hooks in Your Service

```python
from django_hooks import HookManager, HookRejectionError

class CronjobService:
    def __init__(self):
        self.hook_manager = HookManager()
    
    def schedule_job(self, job_id: str, schedule: str, command: str, user_id: int):
        # Create context
        context = CronjobHookContext(
            operation="schedule",
            job_id=job_id,
            schedule=schedule,
            command=command,
            user_id=user_id
        )
        
        # Execute PRE hooks (can reject)
        try:
            self.hook_manager.execute_pre_hooks(context)
        except HookRejectionError as e:
            return {
                "success": False,
                "error_code": e.error_code,
                "message": e.message,
                "details": e.details
            }
        
        # Perform actual operation
        job = self._create_job(job_id, schedule, command)
        
        # Execute POST hooks (cannot reject)
        post_errors = self.hook_manager.execute_post_hooks(context)
        
        return {
            "success": True,
            "job_id": job.id,
            "post_hook_errors": [
                {
                    "hook": e.hook_name,
                    "error_code": e.error_code,
                    "message": e.message
                }
                for e in post_errors
            ]
        }
```

## Core Concepts

### Hook Types

- **PRE Hooks**: Execute before operation
  - Can reject operation (return `False` or raise `HookRejectionError`)
  - Used for validation, authorization, rate limiting
  - Exceptions stop the operation

- **POST Hooks**: Execute after operation
  - Cannot reject operation (transaction already committed)
  - Used for logging, notifications, cleanup
  - Errors captured but don't affect operation success

### Hook Priority

Lower number = higher priority (executes first):

```python
register_hook("security_check", HookType.PRE, callback, priority=10)   # Runs first
register_hook("business_rule", HookType.PRE, callback, priority=50)    # Runs second
register_hook("optional_check", HookType.PRE, callback, priority=100)  # Runs third
```

### Error Handling

PRE hooks can reject with structured errors:

```python
raise HookRejectionError(
    error_code="DAILY_LIMIT_EXCEEDED",  # ALL_CAPS for frontend
    message="You have exceeded your daily limit of 50 jobs",
    details={
        "limit": 50,
        "current_count": 51,
        "reset_at": "2025-12-23T00:00:00Z"
    }
)
```

Frontend can map error codes to user-friendly messages:

```typescript
const ERROR_MESSAGES = {
  DAILY_LIMIT_EXCEEDED: "You've reached your daily limit. Try again tomorrow.",
  PERMISSION_DENIED: "You don't have permission to perform this action.",
  QUOTA_EXCEEDED: "Your account quota has been exceeded."
};
```

## Advanced Usage

### Custom Registry

Create isolated registries for different services:

```python
from django_hooks import HookRegistry, HookManager

# Create custom registry with specific operations
registry = HookRegistry(operations=['schedule', 'execute', 'pause', 'delete'])
manager = HookManager(registry)

# Register hooks to this registry only
registry.register("my_hook", HookType.PRE, callback, operation="schedule")
```

### Global vs Operation-Specific Hooks

```python
# Global hook (runs for all operations)
register_hook("audit_log", HookType.POST, log_audit, operation="*")

# Operation-specific hook (runs only for 'delete')
register_hook("check_admin", HookType.PRE, check_admin, operation="delete")
```

### Hook Communication via Metadata

Hooks can share data through the mutable `metadata` dict:

```python
def hook1(context: HookContext) -> bool:
    # First hook sets data
    context.metadata['user_tier'] = get_user_tier(context.user_id)
    return True

def hook2(context: HookContext) -> bool:
    # Second hook reads data
    if context.metadata.get('user_tier') == 'premium':
        # Allow premium users to bypass limit
        return True
    return check_standard_limit(context)
```

### Testing Hooks

```python
import pytest
from django_hooks import clear_hooks, register_hook, HookType

@pytest.fixture(autouse=True)
def reset_hooks():
    """Clear hooks before each test."""
    clear_hooks()
    yield
    clear_hooks()

def test_hook_rejection():
    """Test that hook can reject operation."""
    def always_reject(context):
        raise HookRejectionError("TEST_ERROR", "Test rejection")
    
    register_hook("test_hook", HookType.PRE, always_reject)
    
    # Your test code here
    with pytest.raises(HookRejectionError) as exc:
        service.perform_operation()
    
    assert exc.value.error_code == "TEST_ERROR"
```

## API Reference

### Core Classes

- **`HookContext`**: Base context class (extend with your fields)
- **`HookType`**: Enum with `PRE` and `POST` values
- **`HookRegistry`**: Manages hook registration and retrieval
- **`HookManager`**: Executes hooks in correct order
- **`Hook`**: Internal representation of a registered hook

### Exceptions

- **`HookRejectionError`**: Raised by PRE hooks to reject operation
- **`HookExecutionError`**: Raised when hook fails unexpectedly
- **`HookSystemError`**: Base exception for all hook errors

### Functions

- **`register_hook(name, hook_type, callback, operation="*", priority=100)`**: Register global hook
- **`unregister_hook(name)`**: Remove hook by name
- **`clear_hooks()`**: Clear all hooks (for testing)
- **`get_global_registry()`**: Get global registry instance

## Best Practices

### ✅ DO

- Use PRE hooks for validation only (no side effects)
- Use POST hooks for side effects (logging, notifications)
- Provide clear error codes for frontend
- Set appropriate priorities (10=security, 50=business, 100=optional)
- Test hooks in isolation
- Clear hooks between tests

### ❌ DON'T

- Modify external state in PRE hooks
- Use POST hooks to reject operations
- Create hooks with long execution times
- Depend on hook execution order without setting priorities
- Use global state in hooks

## Hook Types Reference

| Type | When | Can Reject? | Use For | Error Handling |
|------|------|-------------|---------|----------------|
| **PRE** | Before operation | ✅ Yes | Validation, authorization, rate limits | Raises `HookRejectionError` to stop operation |
| **POST** | After operation | ❌ No | Logging, notifications, cleanup | Errors captured, doesn't affect operation |

## Priority Guide

Lower number = higher priority (executes first):

| Priority Range | Use For | Examples |
|---------------|---------|----------|
| **0-10** | Critical security | Authentication, SQL injection checks |
| **10-50** | Authorization | Permission checks, role validation |
| **50-100** | Business rules | Quotas, limits, validation |
| **100-200** | Logging | Audit logs, activity tracking |
| **200-500** | Notifications | Emails, webhooks, alerts |
| **500-900** | Analytics | Usage tracking, metrics |
| **900-1000** | Cleanup | Temp file cleanup, cache invalidation |

## Common Error Codes

Structure error codes for frontend integration:

### Permission Errors
```python
PERMISSION_DENIED          # No permission for action
INSUFFICIENT_PRIVILEGES    # User role insufficient
UNAUTHORIZED_ACCESS        # Authentication required
```

### Limit Errors
```python
DAILY_LIMIT_EXCEEDED       # Daily quota exceeded
MONTHLY_QUOTA_EXCEEDED     # Monthly quota exceeded
RATE_LIMIT_EXCEEDED        # Too many requests
TOO_MANY_ACTIVE_ITEMS      # Concurrent items limit
```

### Validation Errors
```python
INVALID_INPUT              # Generic validation failure
INVALID_FORMAT             # Format doesn't match
DUPLICATE_ENTRY            # Already exists
NOT_FOUND                  # Referenced item not found
```

### Business Rule Errors
```python
MINIMUM_NOT_MET            # Below minimum threshold
MAXIMUM_EXCEEDED           # Above maximum threshold
OPERATION_NOT_ALLOWED      # Business rule violation
DEPENDENCY_EXISTS          # Can't delete, has dependencies
```

## Frontend Integration

### TypeScript Types

```typescript
interface HookError {
  hook: string;
  error_code: string;
  message: string;
  details?: Record<string, any>;
}

interface OperationResult {
  success: boolean;
  entity_id?: string;
  error_code?: string;
  message?: string;
  details?: Record<string, any>;
  post_hook_errors?: HookError[];
}
```

### Error Message Mapping

```typescript
const ERROR_MESSAGES: Record<string, string> = {
  PERMISSION_DENIED: "You don't have permission to perform this action.",
  DAILY_LIMIT_EXCEEDED: "You've reached your daily limit. Try again tomorrow.",
  RATE_LIMIT_EXCEEDED: "Too many requests. Please slow down.",
  INVALID_INPUT: "Please check your input and try again.",
  OPERATION_NOT_ALLOWED: "This operation is not allowed.",
};
```

### Result Handler

```typescript
function handleOperationResult(result: OperationResult) {
  if (!result.success) {
    // Operation rejected by PRE hook
    const errorMessage = ERROR_MESSAGES[result.error_code] || result.message;
    showError(errorMessage);
    
    // Show additional details if available
    if (result.details) {
      console.error("Error details:", result.details);
    }
    return;
  }
  
  // Operation succeeded
  showSuccess("Operation completed successfully");
  
  // Check POST hook warnings
  if (result.post_hook_errors?.length) {
    result.post_hook_errors.forEach(err => {
      console.warn(`Hook ${err.hook} failed: ${err.message}`);
      // Optionally show warning to user
      showWarning(`Some background tasks failed: ${err.message}`);
    });
  }
}
```

## Architecture Overview

### Component Structure

```
django_hooks/
├── __init__.py          # Public API (HookType, HookContext, register_hook, etc.)
├── core.py              # Hook system implementation (HookRegistry, HookManager)
└── exceptions.py        # Exception hierarchy (HookRejectionError, etc.)
```

### Hook Execution Flow

#### PRE Hooks (Validation Phase)
```
1. Get hooks for operation + PRE type
2. Sort by priority (low to high)
3. For each hook:
   ├─ Execute callback(context)
   ├─ If returns False → Raise HookRejectionError
   ├─ If raises HookRejectionError → Re-raise (stop operation)
   └─ If raises other Exception → Wrap in HookExecutionError
4. All hooks passed → Continue to operation
```

#### POST Hooks (Reaction Phase)
```
1. Get hooks for operation + POST type
2. Sort by priority (low to high)
3. errors = []
4. For each hook:
   ├─ Execute callback(context)
   └─ If raises Exception → Capture in errors list (continue)
5. Return errors list (operation already succeeded)
```

### Metadata Communication

Hooks can't modify context fields (frozen), but can share data via mutable `metadata`:

```python
def hook1(context: HookContext) -> bool:
    # First hook sets data
    context.metadata['user_tier'] = get_user_tier(context.user_id)
    context.metadata['rate_limit'] = get_rate_limit(context.user_id)
    return True

def hook2(context: HookContext) -> bool:
    # Second hook reads data
    tier = context.metadata.get('user_tier')
    if tier == 'premium':
        # Premium users get higher limits
        return True
    return check_standard_rules(context)
```

## Advanced Usage

### Global vs Operation-Specific Hooks

```python
# Global hook (runs for ALL operations)
register_hook('audit_all', HookType.POST, audit_log, operation='*', priority=100)

# Operation-specific hooks
register_hook('validate_create', HookType.PRE, validate, operation='create', priority=50)
register_hook('validate_delete', HookType.PRE, validate, operation='delete', priority=50)
```

### Hook Factories

Create reusable hook generators:

```python
def create_permission_check(required_permission: str):
    """Factory for permission check hooks."""
    def hook(context):
        if not user_has_permission(context.user_id, required_permission):
            raise HookRejectionError(
                error_code='PERMISSION_DENIED',
                message=f"Requires {required_permission}",
                details={'permission': required_permission}
            )
        return True
    return hook

# Register for different operations
register_hook('check_schedule', HookType.PRE, 
              create_permission_check('schedule'), operation='schedule', priority=10)
register_hook('check_delete', HookType.PRE,
              create_permission_check('delete'), operation='delete', priority=10)
```

### Custom Registry (Isolated)

Create service-specific registries:

```python
from django_hooks import HookRegistry, HookManager

# Create isolated registry
registry = HookRegistry(operations=['create', 'update', 'delete'])
manager = HookManager(registry)

# Register hooks to this registry only
registry.register('my_hook', HookType.PRE, callback, operation='create')

# Use in service
manager.execute_pre_hooks(context)
```

**Use When:**
- Testing (isolation)
- Multi-tenant (separate hooks per tenant)
- Multiple services in same app

## Complete Example: Cronjob Package

See `examples/cronjob_hooks.py` for a complete working example with:
- ✅ 7 production-ready hooks (4 PRE, 3 POST)
- ✅ Permission checks
- ✅ Rate limiting
- ✅ Audit logging
- ✅ Email notifications
- ✅ Metadata communication
- ✅ Error handling

## Documentation

- **`README.md`** (this file) - API reference, examples, best practices
- **`IMPLEMENTATION.md`** - Complete step-by-step integration guide
- **`examples/cronjob_hooks.py`** - Full working example
- **`tests/test_hooks_example.py`** - Test examples

## License

MIT License - Free to use in any project

## Contributing

This is a standalone package. To use in your project:

1. Copy the `django_hooks/` directory to your project
2. Or package and publish to PyPI
3. Or add as a git submodule
