This refactoring separates concerns cleanly, making the code more modular, testable, and maintainable.

Here is the content for each file in the target structure:

### 📁 `queue/core.py`
*(Handles the definition of a job and the main queue wrapper.)*

```python
from dataclasses import dataclass, field
from typing import Callable, Any, List
from queue.priority import PriorityQueue

@dataclass(order=True)
class Job:
    """
    Represents a unit of work in the queue.
    The 'priority' field is used for sorting in the PriorityQueue.
    """
    priority: int = field(compare=True)
    job_id: str = field(compare=False)
    func: Callable[..., Any] = field(compare=False)
    args: tuple = field(compare=False, default_factory=tuple)
    kwargs: dict = field(compare=False, default_factory=dict)
    retries: int = field(default=0, compare=False)
    max_retries: int = field(default=3, compare=False)

    def __str__(self):
        return f"Job(ID={self.job_id}, Priority={self.priority}, Retries={self.retries})"


class JobQueue:
    """
    The primary interface for managing and retrieving jobs.
    Uses PriorityQueue internally.
    """
    def __init__(self):
        # The PriorityQueue manages the actual heap structure
        self._queue = PriorityQueue()

    def enqueue(self, job: Job):
        """Adds a job to the queue."""
        self._queue.put(job)
        print(f"[Queue] Enqueued job: {job.job_id} (P={job.priority})")

    def dequeue(self) -> Job | None:
        """Removes and returns the highest priority job."""
        return self._queue.get()

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

    def size(self) -> int:
        """Returns the number of jobs currently in the queue."""
        return self._queue.qsize()

```

### 📁 `queue/priority.py`
*(Implements the priority queue using Python's `heapq` module.)*

```python
import heapq
from typing import Any, List
from queue.core import Job

class PriorityQueue:
    """
    A standard min-heap implementation wrapper for Job objects.
    Since Job is ordered by 'priority', heapq works directly.
    """
    def __init__(self):
        self._heap: List[Job] = []

    def put(self, job: Job):
        """Adds an item to the heap."""
        heapq.heappush(self._heap, job)

    def get(self) -> Job | None:
        """Removes and returns the item with the highest priority (lowest number)."""
        if not self._heap:
            return None
        return heapq.heappop(self._heap)

    def empty(self) -> bool:
        """Checks if the heap is empty."""
        return not self._heap

    def qsize(self) -> int:
        """Returns the number of items in the heap."""
        return len(self._heap)

```

### 📁 `queue/retry.py`
*(Encapsulates the complex logic for handling retries and exponential backoff.)*

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

def exponential_backoff(attempt: int, base_delay: float = 1.0, max_delay: float = 60.0) -> float:
    """
    Calculates the sleep time using exponential backoff.
    Delay = base_delay * (2 ^ (attempt - 1))
    """
    delay = base_delay * (2 ** (attempt - 1))
    return min(delay, max_delay)

def process_with_retry(job: Job, execution_function: Callable[..., Any]) -> bool:
    """
    Executes a job with integrated retry logic and exponential backoff.

    Args:
        job: The Job object containing the function and arguments.
        execution_function: The actual function to execute (optional, used for clarity).

    Returns:
        True if the job succeeded, False if it failed after all retries.
    """
    max_retries = job.max_retries
    
    for attempt in range(1, max_retries + 2): # +1 for the initial attempt
        try:
            print(f"\n--- Attempt {attempt} for Job {job.job_id} ---")
            # Execute the job function
            result = job.func(*job.args, **job.kwargs)
            print(f"✅ Job {job.job_id} succeeded on attempt {attempt}.")
            return True
        
        except Exception as e:
            print(f"❌ Job {job.job_id} failed on attempt {attempt}: {e}")
            
            if attempt > max_retries + 1:
                print(f"🛑 Job {job.job_id} failed permanently after {max_retries + 1} attempts.")
                return False

            # Calculate backoff time
            delay = exponential_backoff(attempt, base_delay=1.0)
            print(f"⏳ Waiting for {delay:.2f} seconds before next retry...")
            time.sleep(delay)
            
            # Update the job object's retry count for tracking
            job.retries = attempt

    return False # Should not be reachable if logic is correct
```

### 📁 `queue/__init__.py`
*(The public API file. This is what users import.)*

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

# Define the public API
__all__ = [
    "Job",
    "JobQueue",
    "process_with_retry"
]
```

***

### Example Usage (Demonstration)

To show how this package works, here is how a user would interact with the new structure:

```python
# main.py (outside the queue package)

import time
from queue import Job, JobQueue, process_with_retry

# --- Define a failing function ---
ATTEMPTS_BEFORE_SUCCESS = 3
call_count = 0

def risky_task(data: str):
    """A task that fails a few times before succeeding."""
    global call_count
    call_count += 1
    print(f"    [TASK] Executing risky task for data: {data} (Call #{call_count})")
    
    if call_count < ATTEMPTS_BEFORE_SUCCESS:
        raise ConnectionError("Simulated network failure.")
    
    return f"Task completed successfully for {data}"

# 1. Setup
queue = JobQueue()

# 2. Create Jobs
# Job 1: High priority, will succeed after 2 retries (3 attempts total)
job1 = Job(
    priority=1, 
    job_id="TASK_A", 
    func=risky_task, 
    args=("Important Data",), 
    max_retries=3
)
queue.enqueue(job1)

# Job 2: Low priority, will fail permanently
job2 = Job(
    priority=10, 
    job_id="TASK_B", 
    func=lambda: 1/0, # Always fails
    max_retries=1
)
queue.enqueue(job2)


# 3. Processing Loop
print("\n" + "="*40)
print("STARTING QUEUE PROCESSING")
print("="*40)

while not queue.empty():
    job = queue.dequeue()
    
    if job:
        success = process_with_retry(job, job.func)
        
        if not success:
            print(f"\n[System] Job {job.job_id} has been permanently abandoned.")
        else:
            # Job is done, move on
            pass
    else:
        break

print("\n" + "="*40)
print("QUEUE PROCESSING COMPLETE")
```