1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
from __future__ import annotations
from dataclasses import dataclass
from enum import StrEnum
from typing import Protocol, runtime_checkable
from abcreg import AnnotatedProtocolMeta # type: ignore[import-untyped]
from fastapi import Request
from starlette.responses import Response
_DEFAULT_METHODS = ("mr_triggers", "on_mr")
class MrTrigger(StrEnum):
OPENED = "opened"
UPDATED = "updated"
@dataclass(frozen=True, slots=True)
class MrEvent:
"""Context passed to plugin actions when an MR is opened or updated."""
project: str
trigger: MrTrigger
mr_ref: str
mr_oid: str
title: str
ours: str
theirs: str | None
head: str
@dataclass(frozen=True, slots=True)
class ActionProgress:
"""One progress update from an async MR action runner."""
message: str
fraction: float | None = None
done: bool = False
ok: bool | None = None
@runtime_checkable
class ProgressSink(Protocol):
"""Receives progress from ``Action.on_mr`` so the caller can stream updates."""
async def report(self, progress: ActionProgress) -> None: ...
def _install_default_methods(interface: type, impl: type) -> None:
"""Give implementations the protocol's optional methods before signature checks."""
if getattr(impl, "_is_protocol", False):
return
skip = set(interface.__mro__)
for name in _DEFAULT_METHODS:
if any(name in base.__dict__ for base in impl.__mro__ if base not in skip):
continue
for base in interface.__mro__:
default = base.__dict__.get(name)
if callable(default):
setattr(impl, name, default)
break
class _ActionMeta(AnnotatedProtocolMeta):
def __init__(
cls,
name: str,
bases: tuple[type, ...],
namespace: dict[str, object],
**kwargs: object,
) -> None:
if not getattr(cls, "_is_protocol", False):
for base in bases:
if isinstance(base, _ActionMeta) and getattr(base, "_is_protocol", False):
_install_default_methods(base, cls)
super().__init__(name, bases, namespace, **kwargs)
def register(cls, subclass: type) -> type:
_install_default_methods(cls, subclass)
super().register(subclass)
return subclass
@runtime_checkable
class Action(Protocol, metaclass=_ActionMeta):
"""
Action: A per-project action provided by a plugin.
``project`` is the project path/slug as used in URLs (under PROJECTROOT).
Subclass this protocol, or register a virtual subclass with ``@Action.register``.
Optional MR hooks: declare interest with ``mr_triggers`` and implement ``on_mr``
as an async runner that reports progress through ``progress``.
"""
def action_name(self) -> str: ...
def action(self, project: str, req: Request | None = None) -> Response | None: ...
def mr_triggers(self) -> frozenset[MrTrigger]:
return frozenset()
async def on_mr(self, event: MrEvent, progress: ProgressSink) -> None:
return None