```python
# https://dodona.be/nl/courses/3363/series/36083/activities/1049251771
import copy
from pathlib import Path
from itertools import chain
def longest_path_length(source: int, sink: int, graph_filename: str | Path) -> int:
    """
    Calculate the length of the longest path in the given graph between source and sink.
    
    >>> longest_path_length(0, 4, 'data/04-data01.txt')
    9
    """
    graph = parse_graph(graph_filename)
    path_lengths = [-1 for _ in range(max(len(graph), source, sink) + 1)]
    path_lengths[source] = 0
    for node in topological_ordering(graph):
        if node is not source:
            path_lengths[node] = max(path_lengths[predecessor] + weight for predecessor, weight in incoming_edges(graph, node))
    return path_lengths[sink]
def longest_path(source: int, sink: int, graph_filename: str | Path) -> tuple[int, ...]:
    """
    Calculate the longest path in the given graph between source and sink.
    The path is constructed by using a backtracking algorithm.
    
    >>> longest_path(0, 4, 'data/04-data01.txt')
    (0, 2, 3, 4)
    """
    graph = parse_graph(graph_filename)
    size = max(len(graph), source, sink) + 1
    previous = [-1 for _ in range(size)]
    path_lengths = [-1 for _ in range(size)]
    path_lengths[source] = 0
    # Calculate the path by weights
    for node in topological_ordering(graph):
        if node is not source:
            # Calculate the longest path based on the incoming edges in the DAG
            for predecessor, weight in incoming_edges(graph, node):
                if path_lengths[node] < path_lengths[predecessor] + weight:
                    previous[node] = predecessor
                    path_lengths[node] = path_lengths[predecessor] + weight
    # Reconstruct the path by backtracking
    path = []
    current = sink
    
    while previous[current] >= 0:
        path.append(current)
        current = previous[current]
        
    path.reverse()
    
    return tuple(path)
def parse_graph(graph_filename: str | Path) -> dict[int, list[tuple[int, int]]]:
    """
    Returns the list of edges in the given input file.
    For every node, the list of outgoing edges and their weights are given.
    
    >>> parse_graph('data/04-data01.txt')
    {0: [(1, 7), (2, 4)], 1: [(4, 1)], 2: [(3, 2)], 3: [(4, 3)]}
    """
    # Graph of all nodes, with a list of outgoing edges
    graph = {}
    with open(graph_filename, 'r', encoding='utf-8') as graph_file:
        for line in graph_file:
            source, value = line.split('->')
            source = int(source)
            target, weight = map(int, value.split(':'))
            if source not in graph:
                graph[source] = []
            graph[source].append((target, weight))
        return graph
def topological_ordering(graph: dict[int, list[tuple[int, int]]]) -> list[int]:
    """
    Returns a valid topological ordering of the given graph. 
    
    >>> topological_ordering({0: [(1, 7), (2, 4)], 1: [(4, 1)], 2: [(3, 2)], 3: [(4, 3)]})
    [0, 1, 2, 3]
    """
    
    # List of nodes in topological order
    node_order = []
    
    # Set of nodes that still need to be visited. 
    remaining_nodes = set(graph)
    
    while len(remaining_nodes) > 0:
        # Select the next node.
        source, value = min(graph.items(), key=lambda item: len(item[1]))
        
        node_order.append(source)
        
        # Remove all outgoing edges from remaining nodes.
        for target, weight in graph[source]:
            remaining_nodes.remove(target)
    
    return node_order
```