mail-address-semantics/mas_modes/stubs/master.py
tegwick e91cd01a02 MAS-WP-0010 T01-T05: mode contract v0.1, schemas, callback stub, email as master
mas-modes-v0.1-spec/ (contract, message + gateway-event schemas, transition
table, message fixtures, statute) vendored into mas_modes. EmailMode
implements the master role over the reference ticket service (MasterStore):
handoff, negotiation by email, statuses in the thread, timeouts, resume by
email, principle 10 shared via mas_modes.mailer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5787@bnt-lap001
Assistant-Session: 5db26b82-138b-40cc-9856-4d5eb0f752fd
2026-10-19 10:57:20 +02:00

79 lines
3.2 KiB
Python

"""A minimal outside master (CMS / ticketing stand-in) for email in the slave role.
It records every message email sends it, issues case identifiers, and
drives email through the contract: assign, policy, mail.send, cancel.
Nothing happens on its own: `pump()` performs queued auto-assignments, so
tests stay deterministic and no handler calls back into email.
master = StubMaster(email_url="http://127.0.0.1:PORT/master", auto_assign=True)
base = serve(master.server()) # POST /events
"""
from __future__ import annotations
import itertools
import threading
from typing import Optional
from .. import message_errors
from ..contract import Idempotent, json_server, message, post, refuse
class StubMaster:
def __init__(self, email_url: Optional[str] = None, auto_assign: bool = False,
id_prefix: str = "EXT-"):
self.email_url = email_url
self.auto_assign = auto_assign
self.ids = (f"{id_prefix}{n}" for n in itertools.count(100))
self.received: list[dict] = []
self.pending_intakes: list[str] = []
self.lock = threading.Lock()
self._once = Idempotent()
def receive(self, msg: dict) -> tuple[int, dict]:
return self._once(msg, self._receive)
def _receive(self, msg):
errors = message_errors(msg)
if errors:
return refuse("schema", "; ".join(errors), 400)
with self.lock:
self.received.append(msg)
if msg["type"] == "case.intake":
self.pending_intakes.append(msg["intake_ref"])
return 200, {"ok": True}
def of_type(self, type_: str) -> list[dict]:
return [m for m in self.received if m["type"] == type_]
# -- commands to email -------------------------------------------------------
def command(self, type_: str, case: Optional[dict] = None, **fields) -> tuple[Optional[int], object]:
return post(self.email_url, message(type_, case, **fields))
def pump(self) -> list[tuple]:
"""Assign case ids to queued intakes (when auto_assign is on)."""
out = []
while self.auto_assign:
with self.lock:
if not self.pending_intakes:
break
ref = self.pending_intakes.pop(0)
out.append(self.assign(ref, next(self.ids)))
return out
def assign(self, intake_ref: str, resource_id: str):
return self.command("case.assign", intake_ref=intake_ref, resource_id=resource_id)
def policy(self, case: dict, autonomous_send: Optional[bool] = None, transfers=None):
return self.command("case.policy", case, autonomous_send=autonomous_send, transfers=transfers)
def send(self, case: dict, in_reply_to: str, body: str, subject: Optional[str] = None):
return self.command("mail.send", case, in_reply_to=in_reply_to, body=body, subject=subject)
def cancel(self, case: dict, handoff_id: str, reason: str = "timed-out"):
"""The master owns timeouts in the slave role: it ends a handoff itself."""
return self.command("handoff.cancel", case, handoff_id=handoff_id, reason=reason)
def server(self, host="127.0.0.1", port=0):
return json_server({"/events": self.receive}, host, port)