Here is a complete refactoring of a monolithic job queue into a modular package structure, following the specified directory layout and design principles.

---

### 1. `queue/__init__.py`
Exports the public API.

```python
from .core import Job, JobQueue
from .retry import process_with_retry

__all__ = ["Job", "JobQueue", "process_with_retry"]
```

---

### 2. `queue/core.py`
Defines `Job` and `JobQueue`, handling basic job and queue operations.

```python
import time
from dataclasses import dataclass, field
from typing import Any, Callable, Optional, List
from datetime import datetime

@dataclass
class Job:
    id: str
    func: Callable
    args: tuple = field(default_factory=tuple)
    kwargs: dict = field(default_factory=dict)
    created_at: datetime = field(default_factory=datetime.now)
    status: str = "pending"
    attempts: int = 0
    max_attempts: int = 3
    last_error: Optional[str] = None

    def mark_running(self) -> None:
        self.status = "running"

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

    def mark_failed(self, error: str) -> None:
        self.status = "failed"
        self.last_error = error
        self.attempts += 1

    def is_expired(self, timeout: int = 300) -> bool:
        return (datetime.now() - self.created_at).total_seconds() > timeout


class JobQueue:
    def __init__(self) -> None:
        self.jobs: List[Job] = []

    def enqueue(self, job: Job) -> None:
        self.jobs.append(job)

    def dequeue(self) -> Optional[Job]:
        if not self.jobs:
            return None
        return self.jobs.pop(0)

    def get_pending_jobs(self) -> List[Job]:
        return [j for j in self.jobs if j.status == "pending"]

    def get_running_jobs(self) -> List[Job]:
        return [j for j in self.jobs if j.status == "running"]

    def get_failed_jobs(self) -> List[Job]:
        return [j for j in self.jobs if j.status == "failed"]

    def get_completed_jobs(self) -> List[Job]:
        return [j for j in self.jobs if j.status == "completed"]

    def update_job_status(self, job_id: str, status: str) -> bool:
        for job in self.jobs:
            if job.id == job_id:
                job.status = status
                return True
        return False

    def size(self) -> int:
        return len(self.jobs)
```

---

### 3. `queue/priority.py`
Implements a priority queue for job scheduling.

```python
from .core import Job
import heapq
from typing import Optional

class PriorityQueue:
    def __init__(self) -> None:
        self._queue: list = []
        self._counter = 0

    def enqueue(self, job: Job, priority: int = 0) -> None:
        heapq.heappush(self._queue, (priority, self._counter, job))
        self._counter += 1

    def dequeue(self) -> Optional[Job]:
        if not self._queue:
            return None
        _, _, job = heapq.heappop(self._queue)
        return job

    def peek(self) -> Optional[Job]:
        if not self._queue:
            return None
        return self._queue[0][2]

    def size(self) -> int:
        return len(self._queue)

    def is_empty(self) -> bool:
        return len(self._queue) == 0
```

---

### 4. `queue/retry.py`
Handles retry logic with exponential backoff.

```python
import time
import random
from typing import Callable, Any, Optional
from .core import Job

DEFAULT_BASE_DELAY = 1.0
DEFAULT_MAX_DELAY = 60.0
DEFAULT_BACKOFF_FACTOR = 2.0


def calculate_backoff(attempt: int, base_delay: float = DEFAULT_BASE_DELAY,
                      max_delay: float = DEFAULT_MAX_DELAY,
                      backoff_factor: float = DEFAULT_BACKOFF_FACTOR) -> float:
    delay = min(base_delay * (backoff_factor ** attempt), max_delay)
    jitter = delay * random.uniform(0, 0.1)
    return delay + jitter


def process_with_retry(func: Callable, *args: Any, max_attempts: int = 3,
                       base_delay: float = DEFAULT_BASE_DELAY,
                       max_delay: float = DEFAULT_MAX_DELAY,
                       backoff_factor: float = DEFAULT_BACKOFF_FACTOR,
                       **kwargs: Any) -> Any:
    last_error = None
    for attempt in range(max_attempts):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            last_error = str(e)
            if attempt < max_attempts - 1:
                delay = calculate_backoff(attempt, base_delay, max_delay, backoff_factor)
                time.sleep(delay)
            else:
                raise RuntimeError(f"Retry exhausted after {max_attempts} attempts. Last error: {last_error}")
    return None
```

---

### 5. `queue/__init__.py` (Revised)
Final exports for public API.

```python
from .core import Job, JobQueue
from .priority import PriorityQueue
from .retry import process_with_retry

__all__ = ["Job", "JobQueue", "PriorityQueue", "process_with_retry"]
```

---

### Summary of Responsibilities

| Module       | Responsibility                                |
|--------------|-----------------------------------------------|
| `core.py`    | Job dataclass, basic queue operations         |
| `priority.py`| Priority-based job scheduling                 |
| `retry.py`   | Retry logic with exponential backoff          |
| `__init__.py`| Clean public API exports                      |

This modular structure ensures clarity, maintainability, and scalability, while preserving all original functionality.