feat(s52): Notifier protocol + console notifier + MAF-free registration
This commit is contained in:
parent
4ecd571961
commit
d5b583349a
4 changed files with 196 additions and 1 deletions
48
src/portfolio_optimiser/notify.py
Normal file
48
src/portfolio_optimiser/notify.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
"""S5.2 — Varsling (notification): B11 notify-stub → leverbare Notifiers (roadmap E, målbilde §3).
|
||||
|
||||
``run_project`` takes ``notify: Callable[[Verdict], None] | None`` and calls ``notify(verdict)``
|
||||
after the verdict is captured — this module supplies the deliverable implementations behind that
|
||||
seam (the seam itself stays untouched). ``Notifier`` is the declared contract: a
|
||||
``__call__``-shaped structural superset of the seam's ``Callable``, so any plain callable still
|
||||
satisfies it.
|
||||
|
||||
**MAF-free** (D7-portable): pure stdlib. ``Verdict`` is imported ONLY under ``TYPE_CHECKING`` and
|
||||
every runtime access is duck-typed — so importing this module never pulls in ``agent_framework``.
|
||||
Registered in ``tests/test_okf.py``'s ``_MAF_FREE_MODULES`` (direct-import guard
|
||||
``test_okf_is_maf_free``) and probed transitively by
|
||||
``tests/test_notify_loadbearing.py::test_notify_transitive_import_graph_is_maf_free``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Protocol, TextIO, runtime_checkable
|
||||
|
||||
if TYPE_CHECKING: # verdicts imports agent_framework — keep it out of the runtime import graph
|
||||
from portfolio_optimiser.verdicts import Verdict
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class Notifier(Protocol):
|
||||
"""The declared notify contract (B11): structurally a superset of the run-path seam
|
||||
``Callable[[Verdict], None]`` — ``isinstance(fn, Notifier)`` holds for any plain callable."""
|
||||
|
||||
def __call__(self, verdict: Verdict) -> None: ...
|
||||
|
||||
|
||||
class NotifyError(RuntimeError):
|
||||
"""A notifier failed to deliver (e.g. webhook transport failure)."""
|
||||
|
||||
|
||||
class NotifyRefused(Exception):
|
||||
"""Fail-closed egress-gate refusal (mirrors ``verdicts.PromotionRefused``)."""
|
||||
|
||||
|
||||
class ConsoleNotifier:
|
||||
"""One-line verdict summary to a text stream (default stdout). Never receives a URL."""
|
||||
|
||||
def __init__(self, stream: TextIO = sys.stdout) -> None:
|
||||
self._stream = stream
|
||||
|
||||
def __call__(self, verdict: Verdict) -> None:
|
||||
print(f"[notify] verdict {verdict.id} decision={verdict.decision}", file=self._stream)
|
||||
Loading…
Add table
Add a link
Reference in a new issue