Workflow Click Fixture
{html.escape(status)}
from __future__ import annotations import html import json import logging import threading import uuid import webbrowser from dataclasses import dataclass from http import HTTPStatus from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from typing import ClassVar from urllib.error import HTTPError, URLError from urllib.request import Request, urlopen from pydantic import BaseModel, Field from wf_authoring import node _LOGGER = logging.getLogger(__name__) _MAX_SESSION_EVENTS = 10 class Snapshot(BaseModel): title: str url: str button_text: str status_text: str clicked: bool events: list[str] = Field(default_factory=list, max_length=10) class OpenPageInput(BaseModel): button_label: str = "Click to continue" open_browser: bool = Field( default=False, description="Open the page in the default browser for manual runs.", ) class OpenPageOutput(BaseModel): session_id: str url: str before: Snapshot class WaitForClickInput(BaseModel): session_id: str simulate: bool = Field( default=True, description="When true, perform a deterministic HTTP click instead of waiting.", ) timeout_seconds: float = Field(default=10.0, gt=0) class WaitForClickOutput(BaseModel): clicked: bool after: Snapshot class CollectSnapshotsInput(BaseModel): session_id: str before: Snapshot after: Snapshot class CollectSnapshotsOutput(BaseModel): before: Snapshot after: Snapshot closed: bool @dataclass(slots=True) class _ClickSession: session_id: str button_label: str server: ThreadingHTTPServer thread: threading.Thread clicked: threading.Event events: list[str] @property def url(self) -> str: host = self.server.server_address[0] port = self.server.server_address[1] return f"http://{host}:{port}/" def snapshot(self) -> Snapshot: clicked = self.clicked.is_set() return Snapshot( title="Workflow Click Fixture", url=self.url, button_text=self.button_label, status_text="Button clicked" if clicked else "Waiting for click", clicked=clicked, events=list(self.events[-10:]), ) def add_event(self, event: str) -> None: """Record bounded click-session events for compact snapshots.""" self.events.append(event) if len(self.events) > _MAX_SESSION_EVENTS: del self.events[: len(self.events) - _MAX_SESSION_EVENTS] def close(self) -> None: self.server.shutdown() self.server.server_close() self.thread.join(timeout=2) if self.thread.is_alive(): _LOGGER.warning( "click session %s server thread did not stop within timeout", self.session_id, ) _SESSIONS: dict[str, _ClickSession] = {} _SESSIONS_LOCK = threading.Lock() class _ClickHandler(BaseHTTPRequestHandler): server: _ClickServer def log_message(self, _format: str, *_args: object) -> None: """Keep example runs quiet; workflow trace carries the useful output.""" def do_GET(self) -> None: if self.path == "/": self._send_html() return if self.path == "/snapshot": self._send_json(self.server.session.snapshot().model_dump(mode="json")) return self.send_error(HTTPStatus.NOT_FOUND) def do_POST(self) -> None: if self.path != "/click": self.send_error(HTTPStatus.NOT_FOUND) return session = self.server.session session.clicked.set() session.add_event("click") self._send_json(session.snapshot().model_dump(mode="json")) def _send_html(self) -> None: session = self.server.session status = "Button clicked" if session.clicked.is_set() else "Waiting for click" html_doc = f"""
{html.escape(status)}