- 8 DB models (services, incidents, monitors, subscribers, etc.) - Full CRUD API for services, incidents, monitors - Public status page with live data - Incident detail page with timeline - API key authentication - Uptime monitoring scheduler - 13 tests passing - TECHNICAL_DESIGN.md with full spec
161 lines
4.9 KiB
Python
161 lines
4.9 KiB
Python
"""Helpers for manipulations with graphs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterator, Set as AbstractSet
|
|
from typing import TypeVar
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
def strongly_connected_components(
|
|
vertices: AbstractSet[T], edges: dict[T, list[T]]
|
|
) -> Iterator[set[T]]:
|
|
"""Compute Strongly Connected Components of a directed graph.
|
|
|
|
Args:
|
|
vertices: the labels for the vertices
|
|
edges: for each vertex, gives the target vertices of its outgoing edges
|
|
|
|
Returns:
|
|
An iterator yielding strongly connected components, each
|
|
represented as a set of vertices. Each input vertex will occur
|
|
exactly once; vertices not part of a SCC are returned as
|
|
singleton sets.
|
|
|
|
From https://code.activestate.com/recipes/578507/.
|
|
"""
|
|
identified: set[T] = set()
|
|
stack: list[T] = []
|
|
index: dict[T, int] = {}
|
|
boundaries: list[int] = []
|
|
|
|
def dfs(v: T) -> Iterator[set[T]]:
|
|
index[v] = len(stack)
|
|
stack.append(v)
|
|
boundaries.append(index[v])
|
|
|
|
for w in edges[v]:
|
|
if w not in index:
|
|
yield from dfs(w)
|
|
elif w not in identified:
|
|
while index[w] < boundaries[-1]:
|
|
boundaries.pop()
|
|
|
|
if boundaries[-1] == index[v]:
|
|
boundaries.pop()
|
|
scc = set(stack[index[v] :])
|
|
del stack[index[v] :]
|
|
identified.update(scc)
|
|
yield scc
|
|
|
|
for v in vertices:
|
|
if v not in index:
|
|
yield from dfs(v)
|
|
|
|
|
|
def prepare_sccs(
|
|
sccs: list[set[T]], edges: dict[T, list[T]]
|
|
) -> dict[AbstractSet[T], set[AbstractSet[T]]]:
|
|
"""Use original edges to organize SCCs in a graph by dependencies between them."""
|
|
sccsmap = {}
|
|
for scc in sccs:
|
|
scc_frozen = frozenset(scc)
|
|
for v in scc:
|
|
sccsmap[v] = scc_frozen
|
|
data: dict[AbstractSet[T], set[AbstractSet[T]]] = {}
|
|
for scc in sccs:
|
|
deps: set[AbstractSet[T]] = set()
|
|
for v in scc:
|
|
deps.update(sccsmap[x] for x in edges[v])
|
|
data[frozenset(scc)] = deps
|
|
return data
|
|
|
|
|
|
class topsort(Iterator[set[T]]): # noqa: N801
|
|
"""Topological sort using Kahn's algorithm.
|
|
|
|
Uses in-degree counters and a reverse adjacency list, so the total work
|
|
is O(V + E).
|
|
|
|
Implemented as a class rather than a generator for better mypyc
|
|
compilation.
|
|
|
|
Args:
|
|
data: A map from vertices to all vertices that it has an edge
|
|
connecting it to. NOTE: dependency sets in this data
|
|
structure are modified in place to remove self-dependencies.
|
|
Orphans are handled internally and are not added to `data`.
|
|
|
|
Returns:
|
|
An iterator yielding sets of vertices that have an equivalent
|
|
ordering.
|
|
|
|
Example:
|
|
Suppose the input has the following structure:
|
|
|
|
{A: {B, C}, B: {D}, C: {D}}
|
|
|
|
The algorithm treats orphan dependencies as if normalized to:
|
|
|
|
{A: {B, C}, B: {D}, C: {D}, D: {}}
|
|
|
|
It will yield the following values:
|
|
|
|
{D}
|
|
{B, C}
|
|
{A}
|
|
"""
|
|
|
|
def __init__(self, data: dict[T, set[T]]) -> None:
|
|
# Single pass: remove self-deps, build reverse adjacency list,
|
|
# compute in-degree counts, detect orphans, and find initial ready set.
|
|
in_degree: dict[T, int] = {}
|
|
rev: dict[T, list[T]] = {}
|
|
ready: set[T] = set()
|
|
for item, deps in data.items():
|
|
deps.discard(item) # Ignore self dependencies.
|
|
deg = len(deps)
|
|
in_degree[item] = deg
|
|
if deg == 0:
|
|
ready.add(item)
|
|
if item not in rev:
|
|
rev[item] = []
|
|
for dep in deps:
|
|
if dep in rev:
|
|
rev[dep].append(item)
|
|
else:
|
|
rev[dep] = [item]
|
|
if dep not in data:
|
|
# Orphan: appears as dependency but has no entry in data.
|
|
in_degree[dep] = 0
|
|
ready.add(dep)
|
|
|
|
self.in_degree = in_degree
|
|
self.rev = rev
|
|
self.ready = ready
|
|
self.remaining = len(in_degree) - len(ready)
|
|
|
|
def __iter__(self) -> Iterator[set[T]]:
|
|
return self
|
|
|
|
def __next__(self) -> set[T]:
|
|
ready = self.ready
|
|
if not ready:
|
|
assert self.remaining == 0, (
|
|
f"A cyclic dependency exists amongst "
|
|
f"{[k for k, deg in self.in_degree.items() if deg > 0]!r}"
|
|
)
|
|
raise StopIteration
|
|
in_degree = self.in_degree
|
|
rev = self.rev
|
|
new_ready: set[T] = set()
|
|
for item in ready:
|
|
for dependent in rev[item]:
|
|
new_deg = in_degree[dependent] - 1
|
|
in_degree[dependent] = new_deg
|
|
if new_deg == 0:
|
|
new_ready.add(dependent)
|
|
self.remaining -= len(new_ready)
|
|
self.ready = new_ready
|
|
return ready
|