"""An outside master in another process, driven through its control API. Same interface as `stubs.master.StubMaster`, so the harness and the flow runner can swap it in. Used for the independent JS master in `impl/js/src/master.mjs` (MAS-WP-0009). """ from __future__ import annotations import json import subprocess import urllib.request from pathlib import Path from typing import Optional ROOT = Path(__file__).resolve().parents[1] JS_MASTER = ROOT / "impl/js/src/master.mjs" def _call(url: str, body: Optional[dict] = None): data = None if body is None else json.dumps(body).encode() req = urllib.request.Request(url, data=data, method="GET" if body is None else "POST", headers={"Content-Type": "application/json"}) with urllib.request.urlopen(req, timeout=15) as r: return json.loads(r.read().decode()) class ExternalMaster: def __init__(self, argv: list[str]): self.proc = subprocess.Popen(argv, stdout=subprocess.PIPE, text=True) info = json.loads(self.proc.stdout.readline()) self.events_url, self.control_url = info["events_url"], info["control_url"] self._email_url = None @classmethod def js(cls, key: bytes, peer_id: str = "cms-1") -> "ExternalMaster": return cls(["node", str(JS_MASTER), "--peer-id", peer_id, "--key-hex", key.hex()]) @property def email_url(self): return self._email_url @email_url.setter def email_url(self, url): self._email_url = url _call(self.control_url + "/config", {"email_url": url}) @property def received(self) -> list[dict]: return _call(self.control_url + "/received") def of_type(self, type_: str) -> list[dict]: return [m for m in self.received if m["type"] == type_] def command(self, type_: str, case: Optional[dict] = None, **fields): out = _call(self.control_url + "/command", {"type": type_, **({"case": case} if case else {}), **{k: v for k, v in fields.items() if v is not None}}) return out["status"], out["body"] 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=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"): return self.command("handoff.cancel", case, handoff_id=handoff_id, reason=reason) def pump(self): return [] def close(self): self.proc.terminate() self.proc.wait(timeout=10)