grctl-sdk-python 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. grctl/client/__init__.py +16 -0
  2. grctl/client/client.py +161 -0
  3. grctl/models/__init__.py +139 -0
  4. grctl/models/api.py +13 -0
  5. grctl/models/command.py +117 -0
  6. grctl/models/common.py +10 -0
  7. grctl/models/directive.py +191 -0
  8. grctl/models/errors.py +10 -0
  9. grctl/models/history.py +358 -0
  10. grctl/models/run_info.py +77 -0
  11. grctl/models/run_info_helper.py +12 -0
  12. grctl/models/worker.py +76 -0
  13. grctl/nats/__init__.py +1 -0
  14. grctl/nats/connection.py +67 -0
  15. grctl/nats/history_fetch.py +78 -0
  16. grctl/nats/history_sub.py +66 -0
  17. grctl/nats/kv_store.py +45 -0
  18. grctl/nats/manifest.py +235 -0
  19. grctl/nats/nats_client.py +10 -0
  20. grctl/nats/nats_manifest.yaml +95 -0
  21. grctl/nats/publisher.py +46 -0
  22. grctl/nats/subscriber.py +124 -0
  23. grctl/py.typed +1 -0
  24. grctl/worker/__init__.py +8 -0
  25. grctl/worker/codec.py +49 -0
  26. grctl/worker/context.py +321 -0
  27. grctl/worker/errors.py +21 -0
  28. grctl/worker/logger.py +40 -0
  29. grctl/worker/run_manager.py +131 -0
  30. grctl/worker/runner.py +176 -0
  31. grctl/worker/runtime.py +185 -0
  32. grctl/worker/store.py +65 -0
  33. grctl/worker/task.py +406 -0
  34. grctl/worker/worker.py +173 -0
  35. grctl/workflow/__init__.py +12 -0
  36. grctl/workflow/future.py +158 -0
  37. grctl/workflow/handle.py +114 -0
  38. grctl/workflow/workflow.py +269 -0
  39. grctl_sdk_python-0.2.1.dist-info/METADATA +66 -0
  40. grctl_sdk_python-0.2.1.dist-info/RECORD +45 -0
  41. {grctl_sdk_python-0.2.0.dist-info → grctl_sdk_python-0.2.1.dist-info}/WHEEL +1 -2
  42. grctl_sdk_python-0.2.1.dist-info/licenses/LICENSE +201 -0
  43. grctl_sdk_python-0.2.0.dist-info/METADATA +0 -12
  44. grctl_sdk_python-0.2.0.dist-info/RECORD +0 -7
  45. grctl_sdk_python-0.2.0.dist-info/top_level.txt +0 -1
@@ -0,0 +1,16 @@
1
+ """Ground Control Python SDK client package."""
2
+
3
+ from grctl.client.client import Client
4
+ from grctl.logging_config import get_logger, setup_logging
5
+ from grctl.models.errors import WorkflowAlreadyRunningError, WorkflowError, WorkflowNotFoundError
6
+ from grctl.nats.connection import Connection
7
+
8
+ __all__ = [
9
+ "Client",
10
+ "Connection",
11
+ "WorkflowAlreadyRunningError",
12
+ "WorkflowError",
13
+ "WorkflowNotFoundError",
14
+ "get_logger",
15
+ "setup_logging",
16
+ ]
grctl/client/client.py ADDED
@@ -0,0 +1,161 @@
1
+ """Workflow Engine Client.
2
+
3
+ Provides a simple interface for interacting with workflows.
4
+ """
5
+
6
+ import logging
7
+ from datetime import UTC, datetime, timedelta
8
+ from typing import Any, TypeVar, overload
9
+
10
+ import msgspec
11
+ from ulid import ULID
12
+
13
+ from grctl.models import DescribeCmd, GrctlAPIResponse, HistoryEvent, RunInfo
14
+ from grctl.models.command import CmdKind, Command
15
+ from grctl.models.errors import WorkflowAlreadyRunningError, WorkflowError, WorkflowNotFoundError
16
+ from grctl.nats.connection import Connection
17
+ from grctl.nats.history_fetch import fetch_run_history
18
+ from grctl.worker.codec import CodecRegistry
19
+ from grctl.workflow.handle import WorkflowHandle
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ _T = TypeVar("_T")
24
+
25
+ ErrWorkflowAlreadyRunningCode = 4001
26
+ ErrWorkflowRunNotFoundCode = 4002
27
+
28
+
29
+ class Client:
30
+ """Client for interacting with the Workflow Engine."""
31
+
32
+ def __init__(self, connection: Connection, codec: CodecRegistry | None = None) -> None:
33
+ self._connection = connection
34
+ self._codec = codec or CodecRegistry()
35
+
36
+ async def describe(self, wf_id: str) -> RunInfo:
37
+ """Describe the latest run for a workflow ID."""
38
+ cmd = Command(
39
+ id=str(ULID()),
40
+ kind=CmdKind.run_describe,
41
+ timestamp=datetime.now(UTC),
42
+ msg=DescribeCmd(wf_id=wf_id),
43
+ )
44
+ # Use a routing-only RunInfo — publish_cmd only needs wf_id for subject routing.
45
+ routing_info = RunInfo(id="", wf_type="", wf_id=wf_id)
46
+ response_bytes = await self._connection.publisher.publish_cmd(routing_info, cmd)
47
+
48
+ response = msgspec.msgpack.decode(response_bytes, type=GrctlAPIResponse)
49
+ if not response.success:
50
+ error_msg = response.error.message if response.error else "unknown error"
51
+ error_code = response.error.code if response.error else 0
52
+ if error_code == ErrWorkflowRunNotFoundCode:
53
+ raise WorkflowNotFoundError(f"workflow '{wf_id}' not found: {error_msg}")
54
+ raise WorkflowError(f"describe failed (code={error_code}): {error_msg}")
55
+
56
+ return msgspec.msgpack.decode(response.payload, type=RunInfo)
57
+
58
+ @overload
59
+ async def run_workflow(
60
+ self,
61
+ type: str,
62
+ id: str,
63
+ input: Any | None = ...,
64
+ timeout: timedelta | None = ..., # noqa: ASYNC109
65
+ return_type: type[_T] = ...,
66
+ ) -> _T: ...
67
+
68
+ @overload
69
+ async def run_workflow(
70
+ self,
71
+ type: str,
72
+ id: str,
73
+ input: Any | None = ...,
74
+ timeout: timedelta | None = ..., # noqa: ASYNC109
75
+ return_type: None = ...,
76
+ ) -> Any: ...
77
+
78
+ async def run_workflow(
79
+ self,
80
+ type: str, # noqa: A002
81
+ id: str, # noqa: A002
82
+ input: Any | None = None, # noqa: A002
83
+ timeout: timedelta | None = None, # noqa: ASYNC109
84
+ return_type: type[_T] | None = None,
85
+ ) -> _T | Any:
86
+ """Run a workflow and wait for its result."""
87
+ wf_handle = await self.start_workflow(
88
+ type=type,
89
+ id=id,
90
+ input=input,
91
+ timeout=timeout,
92
+ return_type=return_type,
93
+ )
94
+ wait_timeout = timeout.total_seconds() if timeout else None
95
+ return await wf_handle.result(timeout=wait_timeout)
96
+
97
+ async def get_workflow_handle(self, wfid: str) -> WorkflowHandle:
98
+ """Get a handle for an already-running workflow."""
99
+ run_info = await self.describe(wfid)
100
+
101
+ handle = WorkflowHandle(
102
+ run_info=run_info,
103
+ payload=None,
104
+ connection=self._connection,
105
+ codec=self._codec,
106
+ )
107
+ await handle.attach()
108
+ return handle
109
+
110
+ async def get_history(self, wf_id: str, run_id: str | None = None) -> list[HistoryEvent]:
111
+ """Return the ordered history events for a workflow run."""
112
+ resolved_run_id = run_id
113
+ if resolved_run_id is None:
114
+ resolved_run_id = (await self.describe(wf_id)).id
115
+
116
+ return await fetch_run_history(
117
+ js=self._connection.js,
118
+ manifest=self._connection.manifest,
119
+ wf_id=wf_id,
120
+ run_id=resolved_run_id,
121
+ )
122
+
123
+ async def start_workflow(
124
+ self,
125
+ type: str, # noqa: A002
126
+ id: str, # noqa: A002
127
+ input: Any | None = None, # noqa: A002
128
+ timeout: timedelta | None = None, # noqa: ASYNC109
129
+ return_type: type | None = None,
130
+ ) -> WorkflowHandle:
131
+ """Start a workflow and return a handle to track and interact with it."""
132
+ workflow_run_id = str(ULID())
133
+
134
+ run_info = RunInfo(
135
+ id=workflow_run_id,
136
+ wf_type=type,
137
+ wf_id=id,
138
+ timeout=int(timeout.total_seconds()) if timeout else None,
139
+ created_at=datetime.now(UTC),
140
+ )
141
+
142
+ handle = WorkflowHandle(
143
+ run_info=run_info,
144
+ payload=input,
145
+ connection=self._connection,
146
+ codec=self._codec,
147
+ return_type=return_type,
148
+ )
149
+
150
+ # Start the workflow future (subscribe to events and publish run command)
151
+ response_bytes = await handle.start()
152
+ response = msgspec.msgpack.decode(response_bytes, type=GrctlAPIResponse)
153
+ if not response.success:
154
+ await handle.future.stop()
155
+ error_msg = response.error.message if response.error else "unknown error"
156
+ error_code = response.error.code if response.error else 0
157
+ if error_code == ErrWorkflowAlreadyRunningCode:
158
+ raise WorkflowAlreadyRunningError(f"workflow '{id}' already has an active run: {error_msg}")
159
+ raise WorkflowError(f"start_workflow failed (code={error_code}): {error_msg}")
160
+
161
+ return handle
@@ -0,0 +1,139 @@
1
+ """Messaging infrastructure for workflow run operations.
2
+
3
+ This module provides NATS-based messaging for workflow lifecycle events,
4
+ enabling clients to start workflows and track execution through observable events.
5
+ """
6
+
7
+ from grctl.models.api import GrctlAPIError, GrctlAPIResponse
8
+ from grctl.models.command import (
9
+ CancelCmd,
10
+ CmdKind,
11
+ Command,
12
+ DescribeCmd,
13
+ EventCmd,
14
+ StartCmd,
15
+ command_decoder,
16
+ command_encoder,
17
+ )
18
+ from grctl.models.directive import (
19
+ Cancel,
20
+ Complete,
21
+ Directive,
22
+ DirectiveKind,
23
+ DirectiveMessage,
24
+ Event,
25
+ Fail,
26
+ FailStep,
27
+ Start,
28
+ Step,
29
+ StepResult,
30
+ Wait,
31
+ directive_decoder,
32
+ directive_encoder,
33
+ )
34
+ from grctl.models.history import (
35
+ ChildWorkflowStarted,
36
+ DeterministicEvents,
37
+ ErrorDetails,
38
+ HistoryEvent,
39
+ HistoryKind,
40
+ ParentEventSent,
41
+ RandomRecorded,
42
+ RunCancelled,
43
+ RunCompleted,
44
+ RunEvents,
45
+ RunFailed,
46
+ RunScheduled,
47
+ RunStarted,
48
+ RunTimeout,
49
+ SleepRecorded,
50
+ StepCancelled,
51
+ StepCompleted,
52
+ StepEvents,
53
+ StepFailed,
54
+ StepStarted,
55
+ StepTimeout,
56
+ TaskAttemptFailed,
57
+ TaskCancelled,
58
+ TaskCompleted,
59
+ TaskEvents,
60
+ TaskFailed,
61
+ TaskStarted,
62
+ TimestampRecorded,
63
+ UuidRecorded,
64
+ WaitStarted,
65
+ WaitTimedOut,
66
+ history_decoder,
67
+ history_encoder,
68
+ )
69
+ from grctl.models.run_info import RunInfo, RunStateKind, RunStatus
70
+ from grctl.models.run_info_helper import RunInfoManager
71
+
72
+ __all__ = [ # noqa: RUF022
73
+ # API response types
74
+ "GrctlAPIError",
75
+ "GrctlAPIResponse",
76
+ # Command types
77
+ "Command",
78
+ "StartCmd",
79
+ "CancelCmd",
80
+ "DescribeCmd",
81
+ "EventCmd",
82
+ "CmdKind",
83
+ "command_decoder",
84
+ "command_encoder",
85
+ # Directive types
86
+ "Directive",
87
+ "DirectiveMessage",
88
+ "DirectiveKind",
89
+ "Start",
90
+ "Cancel",
91
+ "Event",
92
+ "Complete",
93
+ "Fail",
94
+ "FailStep",
95
+ "Step",
96
+ "StepResult",
97
+ "Wait",
98
+ "directive_decoder",
99
+ "directive_encoder",
100
+ # History event types
101
+ "HistoryEvent",
102
+ "HistoryKind",
103
+ "RunScheduled",
104
+ "RunStarted",
105
+ "RunCompleted",
106
+ "RunEvents",
107
+ "RunFailed",
108
+ "RunCancelled",
109
+ "RunTimeout",
110
+ "StepEvents",
111
+ "StepStarted",
112
+ "StepCompleted",
113
+ "StepFailed",
114
+ "StepCancelled",
115
+ "StepTimeout",
116
+ "TaskEvents",
117
+ "TaskStarted",
118
+ "TaskCompleted",
119
+ "TaskFailed",
120
+ "TaskAttemptFailed",
121
+ "TaskCancelled",
122
+ "DeterministicEvents",
123
+ "TimestampRecorded",
124
+ "RandomRecorded",
125
+ "UuidRecorded",
126
+ "SleepRecorded",
127
+ "WaitStarted",
128
+ "WaitTimedOut",
129
+ "ChildWorkflowStarted",
130
+ "ParentEventSent",
131
+ "history_decoder",
132
+ "history_encoder",
133
+ # Common types
134
+ "RunInfo",
135
+ "RunStateKind",
136
+ "RunStatus",
137
+ "ErrorDetails",
138
+ "RunInfoManager",
139
+ ]
grctl/models/api.py ADDED
@@ -0,0 +1,13 @@
1
+ import msgspec
2
+
3
+
4
+ class GrctlAPIError(msgspec.Struct):
5
+ code: int
6
+ message: str
7
+ detail: str = ""
8
+
9
+
10
+ class GrctlAPIResponse(msgspec.Struct):
11
+ success: bool
12
+ payload: msgspec.Raw = msgspec.field(default_factory=lambda: msgspec.Raw(b""))
13
+ error: GrctlAPIError | None = None
@@ -0,0 +1,117 @@
1
+ from datetime import datetime
2
+ from enum import StrEnum
3
+ from typing import Any
4
+
5
+ import msgspec
6
+
7
+ from grctl.models.run_info import RunInfo
8
+
9
+
10
+ class CmdKind(StrEnum):
11
+ run_start = "run.start"
12
+ run_cancel = "run.cancel"
13
+ run_describe = "run.describe"
14
+ run_terminate = "run.terminate"
15
+ run_event = "run.event"
16
+
17
+
18
+ class StartCmd(msgspec.Struct):
19
+ """Request to start workflow execution."""
20
+
21
+ run_info: RunInfo
22
+ input: Any | None
23
+
24
+
25
+ class CancelCmd(msgspec.Struct):
26
+ """Request to cancel a running workflow."""
27
+
28
+ wf_id: str
29
+ reason: str | None
30
+
31
+
32
+ class DescribeCmd(msgspec.Struct):
33
+ """Request to describe a workflow run."""
34
+
35
+ wf_id: str
36
+
37
+
38
+ class EventCmd(msgspec.Struct):
39
+ """Request to emit an event to a running workflow."""
40
+
41
+ wf_id: str
42
+ event_name: str
43
+ payload: Any | None
44
+
45
+
46
+ class TerminateCmd(msgspec.Struct):
47
+ """Request to terminate a running workflow."""
48
+
49
+ wf_id: str
50
+ reason: str | None
51
+
52
+
53
+ type CommandMessage = StartCmd | EventCmd | CancelCmd | DescribeCmd | TerminateCmd
54
+
55
+
56
+ class Command(msgspec.Struct):
57
+ id: str
58
+ kind: CmdKind
59
+ timestamp: datetime
60
+ msg: CommandMessage
61
+
62
+
63
+ # Factory map for kind-based deserialization
64
+ command_factories: dict[str, type] = {
65
+ "run.start": StartCmd,
66
+ "run.cancel": CancelCmd,
67
+ "run.describe": DescribeCmd,
68
+ "run.terminate": TerminateCmd,
69
+ "run.event": EventCmd,
70
+ }
71
+
72
+
73
+ class CommandWire(msgspec.Struct):
74
+ """Wire format for Command with compact field names matching Go server.
75
+
76
+ Encoded as a dict/map (not array) to match Go's msgpack tag expectations.
77
+ """
78
+
79
+ id: str
80
+ k: CmdKind
81
+ m: bytes
82
+ t: datetime
83
+
84
+
85
+ def command_encoder(cmd: Command) -> bytes:
86
+ """Encode command to msgpack with compact wire format."""
87
+ if cmd.msg is None:
88
+ raise ValueError("Command message cannot be None")
89
+
90
+ msg_bytes = msgspec.msgpack.encode(cmd.msg)
91
+
92
+ wire = CommandWire(
93
+ id=cmd.id,
94
+ k=cmd.kind,
95
+ m=msg_bytes,
96
+ t=cmd.timestamp,
97
+ )
98
+
99
+ return msgspec.msgpack.encode(wire)
100
+
101
+
102
+ def command_decoder(data: bytes) -> Command:
103
+ """Decode msgpack to command."""
104
+ wire = msgspec.msgpack.decode(data, type=CommandWire)
105
+
106
+ factory = command_factories.get(wire.k)
107
+ if factory is None:
108
+ raise ValueError(f"Unknown command kind: {wire.k}")
109
+
110
+ msg = msgspec.msgpack.decode(wire.m, type=factory)
111
+
112
+ return Command(
113
+ id=wire.id,
114
+ kind=wire.k,
115
+ msg=msg, # ty:ignore[invalid-argument-type]
116
+ timestamp=wire.t,
117
+ )
grctl/models/common.py ADDED
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ from msgspec import Struct
4
+
5
+
6
+ class ErrorDetails(Struct):
7
+ type: str
8
+ message: str
9
+ stack_trace: str
10
+ qualified_type: str = ""
@@ -0,0 +1,191 @@
1
+ """Custom msgpack encoding/decoding for Directive types.
2
+
3
+ This module provides msgpack serialization for Directive messages with a compact wire format
4
+ that matches the Go server implementation.
5
+ """
6
+
7
+ from datetime import datetime
8
+ from enum import StrEnum
9
+ from typing import Any
10
+
11
+ import msgspec
12
+
13
+ from grctl.logging_config import get_logger
14
+ from grctl.models.common import ErrorDetails
15
+ from grctl.models.run_info import RunInfo
16
+
17
+ logger = get_logger(__name__)
18
+
19
+
20
+ class RetryPolicy(msgspec.Struct, omit_defaults=True):
21
+ max_attempts: int | None = None
22
+ initial_delay_ms: int | None = None
23
+ backoff_multiplier: float | None = None
24
+ max_delay_ms: int | None = None
25
+ jitter: float | None = None
26
+ retryable_errors: list[str] | None = None
27
+ non_retryable_errors: list[str] | None = None
28
+
29
+
30
+ class Start(msgspec.Struct):
31
+ """Request to start workflow execution."""
32
+
33
+ input: Any | None = None
34
+ timeout_ms: int | None = 3_000 # 3 seconds in nanoseconds (Go time.Duration)
35
+
36
+
37
+ class Cancel(msgspec.Struct):
38
+ """Request to cancel a running workflow."""
39
+
40
+ reason: str | None = None
41
+
42
+
43
+ class Event(msgspec.Struct):
44
+ """Request to emit an event to a running workflow."""
45
+
46
+ event_name: str
47
+ payload: Any | None = None
48
+
49
+
50
+ class Step(msgspec.Struct):
51
+ """Request to execute a specific step in a workflow."""
52
+
53
+ step_name: str
54
+ timeout_ms: int | None = 3_000 # 3 seconds in nanoseconds (Go time.Duration)
55
+
56
+
57
+ class Wait(msgspec.Struct):
58
+ """Worker directive to park the run; optionally times out to a named step."""
59
+
60
+ timeout_ms: int = 0
61
+ timeout_step_name: str = ""
62
+
63
+
64
+ class Complete(msgspec.Struct):
65
+ """Worker directive to mark workflow complete."""
66
+
67
+ result: Any
68
+
69
+
70
+ class Fail(msgspec.Struct):
71
+ """Worker directive to mark workflow failed."""
72
+
73
+ error: ErrorDetails
74
+
75
+
76
+ class FailStep(msgspec.Struct):
77
+ """Worker directive to mark a step as failed and fail the run."""
78
+
79
+ step_name: str
80
+ error: ErrorDetails
81
+
82
+
83
+ class DirectiveKind(StrEnum):
84
+ start = "start"
85
+ cancel = "cancel"
86
+ terminate = "terminate"
87
+ complete = "complete"
88
+ fail = "fail"
89
+ step = "step"
90
+ event = "event"
91
+ wait = "wait"
92
+ wait_timeout = "wait_timeout"
93
+ step_result = "step_result"
94
+ fail_step = "fail_step"
95
+
96
+
97
+ class StepResult(msgspec.Struct):
98
+ """Worker directive to mark step complete."""
99
+
100
+ processed_msg_kind: DirectiveKind
101
+ # Any because processed_msg and next_msg are type-erased at wire level;
102
+ # the _kind fields carry the type info needed for deserialization.
103
+ processed_msg: Any
104
+ worker_id: str
105
+ kv_updates: dict[str, Any]
106
+ next_msg_kind: DirectiveKind
107
+ next_msg: Any
108
+ duration_ms: int = 0
109
+
110
+
111
+ DirectiveMessage = Start | Cancel | Event | Complete | Fail | Step | Wait | StepResult
112
+
113
+
114
+ # Factory map for kind-based deserialization
115
+ directive_factories: dict[str, type[DirectiveMessage]] = {
116
+ "start": Start,
117
+ "cancel": Cancel,
118
+ "event": Event,
119
+ "complete": Complete,
120
+ "fail": Fail,
121
+ "step": Step,
122
+ "wait": Wait,
123
+ "step_result": StepResult,
124
+ }
125
+
126
+
127
+ class Directive(msgspec.Struct):
128
+ id: str
129
+ timestamp: datetime
130
+ kind: DirectiveKind
131
+ run_info: RunInfo
132
+ msg: DirectiveMessage
133
+ attempt: int = 0
134
+ kv_revs: dict[str, Any] | None = None
135
+
136
+
137
+ class DirectiveWire(msgspec.Struct, omit_defaults=True):
138
+ """Wire format for Directive with compact field names matching Go server.
139
+
140
+ Encoded as a dict/map (not array) to match Go's msgpack tag expectations.
141
+ """
142
+
143
+ id: str
144
+ k: str # kind
145
+ m: bytes # message
146
+ r: RunInfo # run_info
147
+ t: datetime # timestamp
148
+ a: int = 0 # attempt
149
+ kv: dict[str, Any] | None = None # kv_revs
150
+
151
+
152
+ def directive_encoder(directive: Directive, enc_hook: Any = None) -> bytes:
153
+ if directive.msg is None:
154
+ raise ValueError("Directive message cannot be None")
155
+
156
+ msg_bytes = msgspec.msgpack.encode(directive.msg, enc_hook=enc_hook)
157
+
158
+ wire = DirectiveWire(
159
+ id=directive.id,
160
+ k=directive.kind,
161
+ m=msg_bytes,
162
+ r=directive.run_info,
163
+ t=directive.timestamp,
164
+ a=directive.attempt,
165
+ kv=directive.kv_revs,
166
+ )
167
+
168
+ return msgspec.msgpack.encode(wire)
169
+
170
+
171
+ def directive_decoder(data: bytes) -> Directive:
172
+ wire = msgspec.msgpack.decode(data, type=DirectiveWire)
173
+
174
+ factory = directive_factories.get(wire.k)
175
+ if factory is None:
176
+ raise ValueError(f"Unknown directive kind: {wire.k}")
177
+
178
+ msg = msgspec.msgpack.decode(wire.m, type=factory)
179
+
180
+ # Convert kind string to DirectiveKind enum
181
+ kind_enum = DirectiveKind(wire.k)
182
+
183
+ return Directive(
184
+ id=wire.id,
185
+ kind=kind_enum,
186
+ attempt=wire.a,
187
+ kv_revs=wire.kv or {},
188
+ msg=msg,
189
+ run_info=wire.r,
190
+ timestamp=wire.t,
191
+ )
grctl/models/errors.py ADDED
@@ -0,0 +1,10 @@
1
+ class WorkflowError(Exception):
2
+ """Workflow error."""
3
+
4
+
5
+ class WorkflowNotFoundError(WorkflowError):
6
+ """Raised when a workflow ID does not correspond to any active run."""
7
+
8
+
9
+ class WorkflowAlreadyRunningError(WorkflowError):
10
+ """Raised when a workflow ID already has an active run."""