79 lines
2.3 KiB
Python
79 lines
2.3 KiB
Python
from typing import List
|
|
from models import Ticket
|
|
|
|
class TrackDAG:
|
|
def __init__(self, tickets: List[Ticket]):
|
|
self.tickets = tickets
|
|
self.ticket_map = {t.id: t for t in tickets}
|
|
|
|
def get_ready_tasks(self) -> List[Ticket]:
|
|
"""Returns tickets that are 'todo' and whose dependencies are all 'completed'."""
|
|
ready = []
|
|
for ticket in self.tickets:
|
|
if ticket.status == 'todo':
|
|
# Check if all dependencies exist and are completed
|
|
all_done = True
|
|
for dep_id in ticket.depends_on:
|
|
dep = self.ticket_map.get(dep_id)
|
|
if not dep or dep.status != 'completed':
|
|
all_done = False
|
|
break
|
|
if all_done:
|
|
ready.append(ticket)
|
|
return ready
|
|
|
|
def has_cycle(self) -> bool:
|
|
"""Returns True if there's a dependency cycle."""
|
|
visited = set()
|
|
rec_stack = set()
|
|
|
|
def is_cyclic(ticket_id):
|
|
if ticket_id in rec_stack:
|
|
return True
|
|
if ticket_id in visited:
|
|
return False
|
|
|
|
visited.add(ticket_id)
|
|
rec_stack.add(ticket_id)
|
|
|
|
ticket = self.ticket_map.get(ticket_id)
|
|
if ticket:
|
|
for neighbor in ticket.depends_on:
|
|
if is_cyclic(neighbor):
|
|
return True
|
|
|
|
rec_stack.remove(ticket_id)
|
|
return False
|
|
|
|
for ticket in self.tickets:
|
|
if ticket.id not in visited:
|
|
if is_cyclic(ticket.id):
|
|
return True
|
|
return False
|
|
|
|
def topological_sort(self) -> List[str]:
|
|
"""
|
|
Returns a list of ticket IDs in topological order.
|
|
Raises ValueError if a cycle is detected.
|
|
"""
|
|
if self.has_cycle():
|
|
raise ValueError("Dependency cycle detected")
|
|
|
|
visited = set()
|
|
stack = []
|
|
|
|
def visit(ticket_id):
|
|
if ticket_id in visited:
|
|
return
|
|
visited.add(ticket_id)
|
|
ticket = self.ticket_map.get(ticket_id)
|
|
if ticket:
|
|
for dep_id in ticket.depends_on:
|
|
visit(dep_id)
|
|
stack.append(ticket_id)
|
|
|
|
for ticket in self.tickets:
|
|
visit(ticket.id)
|
|
|
|
return stack
|