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
79 lines
3.2 KiB
Python
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)
|