From 15fc04ddc0b36c3dd25f9ffa3fab401be202df64 Mon Sep 17 00:00:00 2001 From: Tristan Simas Date: Sun, 11 Oct 2026 03:15:24 -0400 Subject: [PATCH] TaskProgress carries the application's phase and status vocabulary; 0.6.1 Phase and status are typed object: an application event narrows them (for example to a registered class family) and the runtime reads the spelling to_jsonable gives them on the wire. The str-or-Enum check and the unused TaskPhase and TaskStatus enums are deleted. Co-Authored-By: Claude Opus 5.5 --- pyproject.toml | 2 +- src/zmqruntime/__init__.py | 2 -- src/zmqruntime/messages.py | 44 +++++--------------------------------- tests/test_messages.py | 28 ++++++++++++++++++++++++ 4 files changed, 34 insertions(+), 42 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1ba5d25..ffee1f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "zmqruntime" -version = "0.6.0" +version = "0.6.1" description = "Generic ZMQ-based distributed execution framework" authors = [{name = "Tristan Simas", email = "tristan.simas@mail.mcgill.ca"}] license = {text = "MIT"} diff --git a/src/zmqruntime/__init__.py b/src/zmqruntime/__init__.py index 6084745..6fd14f6 100644 --- a/src/zmqruntime/__init__.py +++ b/src/zmqruntime/__init__.py @@ -81,9 +81,7 @@ "ShutdownAck", "SocketType", "StatusRequest", - "TaskPhase", "TaskProgress", - "TaskStatus", "TypedWireMessage", "WireMessage", "WireView", diff --git a/src/zmqruntime/messages.py b/src/zmqruntime/messages.py index b72ffc6..6c0c758 100644 --- a/src/zmqruntime/messages.py +++ b/src/zmqruntime/messages.py @@ -100,37 +100,6 @@ def declared_items(cls, data: Mapping[str, Any]) -> Mapping[str, Any]: # ============================================================================= -class TaskPhase(Enum): - """Generic task phases - base vocabulary for workflow states. - - Applications can extend with their own phase enums. - TaskProgress accepts TaskPhase | union types. - """ - - INIT = "init" - QUEUED = "queued" - RUNNING = "running" - COMPILE = "compile" - SUCCESS = "success" - FAILED = "failed" - CANCELLED = "cancelled" - - -class TaskStatus(Enum): - """Generic task status - base vocabulary for status values. - - Applications can extend with their own status enums. - TaskProgress accepts TaskStatus | union types. - """ - - PENDING = "pending" - STARTED = "started" - RUNNING = "running" - SUCCESS = "success" - FAILED = "failed" - CANCELLED = "cancelled" - - @dataclass(frozen=True) class WorkerState(WireMessage): """Generic worker/process information.""" @@ -147,13 +116,14 @@ class TaskProgress(WireView): """The generic fields of one progress event. Applications publish progress events that carry these fields beside their - own; the runtime reads and validates only these. Phase and status accept - strings (as transported) or application enums (encoded by value). + own; the runtime reads and validates only these. Phase and status are the + application's vocabulary: an application event narrows their type, and the + runtime sees the spelling ``to_jsonable`` gives them on the wire. """ execution_id: str - phase: str | Enum - status: str | Enum + phase: object + status: object percent: float timestamp: float completed: int @@ -169,10 +139,6 @@ def __post_init__(self): raise ValueError(f"percent must be in [0, 100], got {self.percent}") if self.completed > self.total: raise ValueError(f"completed ({self.completed}) cannot exceed total ({self.total})") - if not isinstance(self.phase, (str, Enum)): - raise TypeError(f"phase must be a string or Enum, got {type(self.phase)}") - if not isinstance(self.status, (str, Enum)): - raise TypeError(f"status must be a string or Enum, got {type(self.status)}") # ============================================================================= diff --git a/tests/test_messages.py b/tests/test_messages.py index 447a7ff..aeacddc 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -1,4 +1,7 @@ +from dataclasses import dataclass + import pytest +from metaclass_registry import AutoRegisterMeta from zmqruntime.client import EndpointClientSession, EndpointCompatibilityClientABC from zmqruntime.messages import ( @@ -14,6 +17,7 @@ ProcessResourceUsage, ResponseType, ServerRole, + TaskProgress, WorkerState, ) @@ -283,3 +287,27 @@ def test_typed_messages_reject_another_kind() -> None: messages.ImageAck.from_dict(messages.ShutdownAck("stopping").to_dict()) with pytest.raises(ValueError, match="message_type"): messages.StatusRequest.from_dict(CancelRequest(execution_id="e-1").to_dict()) + + +class _Phase(metaclass=AutoRegisterMeta): + __registry_key__ = "name" + __skip_if_no_key__ = True + name = None + + +class _Compiling(_Phase): + name = "compiling" + + +@dataclass(frozen=True) +class _ApplicationProgress(TaskProgress): + phase: type[_Phase] + status: type[_Phase] + + +def test_task_progress_carries_the_application_phase_vocabulary() -> None: + event = _ApplicationProgress("e-1", _Compiling, _Compiling, 10.0, 1.0, 1, 10) + wire = event.to_dict() + + assert wire["phase"] == wire["status"] == "compiling" + assert TaskProgress.from_dict(wire).phase == "compiling"