# Job Queue Package Refactoring

Here's the refactored package structure with proper separation of concerns:

## `queue/__init__.py`
```python
"""Job Queue Package - A production-ready job queue with retry support."""

from .core import Job, JobQueue
from .priority import PriorityQueue
from .retry import JobRetryHandler

__all__ = [
    "Job",
    "JobQueue",
    "PriorityQueue",
    "JobRetryHandler",
]

__version__ = "1.0.0"
```

## `queue/core.py`
```python
"""Core job queue implementation with basic queue operations."""

from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Any, Callable, Optional
from queue import Queue as StdQueue
import threading


@dataclass
class Job:
    """Represents a job to be processed."""
    id: str
    name: str
    payload: dict
    created_at: datetime = field(default_factory=datetime.now)
    priority: int = 0
    status: str = "pending"  # pending, processing, completed, failed
    max_retries: int = 3
    retry_count: int = 0
    last_error: Optional[str] = None
    scheduled_at: Optional[datetime] = None

    def is_ready(self) -> bool:
        """Check if job is ready for processing."""
        if self.status != "pending":
            return False
        if self.scheduled_at and datetime.now() < self.scheduled_at:
            return False
        return True

    def mark_processing(self) -> None:
        """Mark job as being processed."""
        self.status = "processing"
        self.last_error = None

    def mark_completed(self) -> None:
        """Mark job as completed."""
        self.status = "completed"

    def mark_failed(self, error: str) -> None:
        """Mark job as failed with error message."""
        self.status = "failed"
        self.last_error = error


class JobQueue:
    """Basic job queue implementation."""

    def __init__(self, max_size: int = 1000):
        self._queue: StdQueue[Job] = StdQueue(maxsize=max_size)
        self._lock = threading.Lock()
        self._callbacks: dict[str, Callable[[Job], Any]] = {}

    def add(self, job: Job) -> bool:
        """Add a job to the queue."""
        try:
            self._queue.put_nowait(job)
            return True
        except:
            return False

    def get(self, timeout: float = 0) -> Optional[Job]:
        """Get the next job from the queue."""
        try:
            return self._queue.get_nowait()
        except:
            return None

    def get_with_timeout(self, timeout: float = 1.0) -> Optional[Job]:
        """Get the next job with timeout."""
        try:
            return self._queue.get(timeout=timeout)
        except:
            return None

    def put(self, job: Job) -> None:
        """Put a job back in the queue (for retry)."""
        self._queue.put_nowait(job)

    def size(self) -> int:
        """Return current queue size."""
        return self._queue.qsize()

    def empty(self) -> bool:
        """Check if queue is empty."""
        return self._queue.empty()

    def register_callback(self, job_id: str, callback: Callable[[Job], Any]) -> None:
        """Register a callback for a specific job."""
        with self._lock:
            self._callbacks[job_id] = callback

    def unregister_callback(self, job_id: str) -> None:
        """Unregister a callback for a specific job."""
        with self._lock:
            self._callbacks.pop(job_id, None)
```

## `queue/priority.py`
```python
"""Priority queue implementation for job scheduling."""

from __future__ import annotations
import heapq
from typing import Any, Optional
from dataclasses import dataclass
import threading


@dataclass(order=True)
class PriorityJob:
    """Internal wrapper for priority queue ordering."""
    priority: int
    created_at: float
    job: Any = field(compare=False)


class PriorityQueue:
    """Priority queue implementation for job scheduling."""

    def __init__(self):
        self._heap: list[PriorityJob] = []
        self._lock = threading.Lock()
        self._counter = 0

    def push(self, job: Any, priority: int = 0) -> None:
        """Add a job with given priority (higher = more urgent)."""
        with self._lock:
            priority_job = PriorityJob(
                priority=priority,
                created_at=self._counter,
                job=job
            )
            heapq.heappush(self._heap, priority_job)
            self._counter += 1

    def pop(self) -> Optional[Any]:
        """Remove and return the highest priority job."""
        with self._lock:
            if not self._heap:
                return None
            priority_job = heapq.heappop(self._heap)
            return priority_job.job

    def peek(self) -> Optional[Any]:
        """Return the highest priority job without removing it."""
        with self._lock:
            if not self._heap:
                return None
            return self._heap[0].job

    def size(self) -> int:
        """Return current queue size."""
        with self._lock:
            return len(self._heap)

    def empty(self) -> bool:
        """Check if queue is empty."""
        with self._lock:
            return len(self._heap) == 0

    def get_all(self) -> list[Any]:
        """Get all jobs (for debugging/testing)."""
        with self._lock:
            return [item.job for item in self._heap]

    def clear(self) -> None:
        """Clear all jobs from the queue."""
        with self._lock:
            self._heap.clear()
```

## `queue/retry.py`
```python
"""Retry logic with exponential backoff for job processing."""

from __future__ import annotations
import time
import random
from typing import Optional, Callable, Any
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging

logger = logging.getLogger(__name__)


@dataclass
class RetryConfig:
    """Configuration for retry behavior."""
    max_retries: int = 3
    base_delay: float = 1.0  # seconds
    max_delay: float = 60.0  # seconds
    backoff_multiplier: float = 2.0
    jitter: bool = True


class JobRetryHandler:
    """Handles retry logic with exponential backoff."""

    def __init__(self, config: Optional[RetryConfig] = None):
        self.config = config or RetryConfig()
        self._retry_times: dict[str, datetime] = {}

    def should_retry(self, job: Any) -> bool:
        """Check if job should be retried."""
        if hasattr(job, 'retry_count'):
            return job.retry_count < job.max_retries
        return False

    def get_delay(self, retry_count: int) -> float:
        """Calculate delay for next retry with exponential backoff."""
        delay = min(
            self.config.base_delay * (self.config.backoff_multiplier ** retry_count),
            self.config.max_delay
        )
        if self.config.jitter:
            delay *= (0.5 + random.random())
        return delay

    def calculate_next_retry_time(self, retry_count: int) -> float:
        """Calculate seconds until next retry."""
        return self.get_delay(retry_count)

    def wait_before_retry(self, retry_count: int) -> None:
        """Wait before retrying with exponential backoff."""
        delay = self.get_delay(retry_count)
        logger.info(f"Waiting {delay:.2f}s before retry {retry_count + 1}")
        time.sleep(delay)

    def execute_with_retry(
        self,
        job: Any,
        processor: Callable[[Any], Any],
        on_success: Optional[Callable[[Any], Any]] = None,
        on_failure: Optional[Callable[[Any, str], Any]] = None
    ) -> tuple[bool, Optional[str]]:
        """Execute job with retry logic.

        Returns:
            tuple: (success: bool, error: Optional[str])
        """
        for attempt in range(job.max_retries + 1):
            try:
                result = processor(job)
                if on_success:
                    on_success(job)
                return True, None
            except Exception as e:
                error_msg = str(e)
                job.retry_count = attempt
                job.last_error = error_msg

                if attempt >= job.max_retries:
                    logger.error(f"Job {job.id} failed after {attempt + 1} attempts: {error_msg}")
                    if on_failure:
                        on_failure(job, error_msg)
                    return False, error_msg

                logger.warning(f"Job {job.id} attempt {attempt + 1} failed: {error_msg}")
                self.wait_before_retry(attempt)

        return False, "Max retries exceeded"

    def record_retry(self, job_id: str) -> None:
        """Record retry timestamp for monitoring."""
        self._retry_times[job_id] = datetime.now()

    def get_retry_count(self, job_id: str) -> int:
        """Get retry count for a job."""
        return self._retry_times.get(job_id, 0)
```

## Usage Example

```python
# main.py
from queue import Job, JobQueue, PriorityQueue, JobRetryHandler
from queue.retry import RetryConfig

# Create components
job_queue = JobQueue()
priority_queue = PriorityQueue()
retry_handler = JobRetryHandler(RetryConfig(max_retries=3))

# Create a job
job = Job(
    id="job-001",
    name="process_data",
    payload={"data": "example"},
    priority=5
)

# Add to priority queue
priority_queue.push(job, priority=job.priority)

# Process with retry
success, error = retry_handler.execute_with_retry(
    job,
    processor=lambda j: process_job(j),
    on_success=lambda j: print(f"Job {j.id} completed"),
    on_failure=lambda j, e: print(f"Job {j.id} failed: {e}")
)

def process_job(job: Job) -> None:
    """Example job processor."""
    if job.id == "fail-001":
        raise Exception("Simulated failure")
```

This refactored structure provides:
- ✅ **Single Responsibility**: Each module handles one concern
- ✅ **Clean API**: Public exports in `__init__.py`
- ✅ **Proper Dependencies**: Modules import from each other correctly
- ✅ **Type Safety**: Full type hints throughout
- ✅ **Thread Safety**: Locks where needed
- ✅ **Extensibility**: Easy to add new features