grctl-sdk-python 0.1.3__py3-none-any.whl → 0.1.4__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.
grctl/client/__init__.py CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  from grctl.client.client import Client
4
4
  from grctl.logging_config import get_logger, setup_logging
5
+ from grctl.models.errors import WorkflowAlreadyRunningError, WorkflowError, WorkflowNotFoundError
5
6
  from grctl.nats.connection import Connection
6
7
 
7
- __all__ = ["Client", "Connection", "get_logger", "setup_logging"]
8
+ __all__ = [
9
+ "Client",
10
+ "Connection",
11
+ "WorkflowAlreadyRunningError",
12
+ "WorkflowError",
13
+ "WorkflowNotFoundError",
14
+ "get_logger",
15
+ "setup_logging",
16
+ ]
grctl/client/client.py CHANGED
@@ -6,14 +6,14 @@ Provides a simple interface for interacting with workflows.
6
6
  import asyncio
7
7
  import logging
8
8
  from datetime import UTC, datetime, timedelta
9
- from typing import Any
9
+ from typing import Any, TypeVar, overload
10
10
 
11
11
  import msgspec
12
12
  from ulid import ULID
13
13
 
14
14
  from grctl.models import DescribeCmd, GrctlAPIResponse, HistoryEvent, RunInfo
15
15
  from grctl.models.command import CmdKind, Command
16
- from grctl.models.errors import WorkflowError, WorkflowNotFoundError
16
+ from grctl.models.errors import WorkflowAlreadyRunningError, WorkflowError, WorkflowNotFoundError
17
17
  from grctl.nats.connection import Connection
18
18
  from grctl.nats.history_fetch import fetch_run_history
19
19
  from grctl.worker.codec import CodecRegistry
@@ -21,6 +21,9 @@ from grctl.workflow.handle import WorkflowHandle
21
21
 
22
22
  logger = logging.getLogger(__name__)
23
23
 
24
+ _T = TypeVar("_T")
25
+
26
+ ErrWorkflowAlreadyRunningCode = 4001
24
27
  ErrWorkflowRunNotFoundCode = 4002
25
28
 
26
29
 
@@ -53,13 +56,34 @@ class Client:
53
56
 
54
57
  return msgspec.msgpack.decode(response.payload, type=RunInfo)
55
58
 
59
+ @overload
60
+ async def run_workflow(
61
+ self,
62
+ type: str,
63
+ id: str,
64
+ input: Any | None = ...,
65
+ timeout: timedelta | None = ..., # noqa: ASYNC109
66
+ return_type: type[_T] = ...,
67
+ ) -> _T: ...
68
+
69
+ @overload
70
+ async def run_workflow(
71
+ self,
72
+ type: str,
73
+ id: str,
74
+ input: Any | None = ...,
75
+ timeout: timedelta | None = ..., # noqa: ASYNC109
76
+ return_type: None = ...,
77
+ ) -> Any: ...
78
+
56
79
  async def run_workflow(
57
80
  self,
58
81
  type: str, # noqa: A002
59
82
  id: str, # noqa: A002
60
83
  input: Any | None = None, # noqa: A002
61
84
  timeout: timedelta | None = None, # noqa: ASYNC109
62
- ) -> Any:
85
+ return_type: type[_T] | None = None,
86
+ ) -> _T | Any:
63
87
  """Run a workflow and wait for its result."""
64
88
  wf_handle = await self.start_workflow(
65
89
  type=type,
@@ -69,7 +93,10 @@ class Client:
69
93
  )
70
94
  wait_timeout = timeout.total_seconds() if timeout else None
71
95
  try:
72
- return await asyncio.wait_for(wf_handle.future, timeout=wait_timeout)
96
+ result = await asyncio.wait_for(wf_handle.future, timeout=wait_timeout)
97
+ if return_type is not None:
98
+ return self._codec.from_primitive(result, return_type)
99
+ return result
73
100
  finally:
74
101
  await wf_handle.future.stop()
75
102
 
@@ -125,5 +152,14 @@ class Client:
125
152
  )
126
153
 
127
154
  # Start the workflow future (subscribe to events and publish run command)
128
- await handle.start()
155
+ response_bytes = await handle.start()
156
+ response = msgspec.msgpack.decode(response_bytes, type=GrctlAPIResponse)
157
+ if not response.success:
158
+ await handle.future.stop()
159
+ error_msg = response.error.message if response.error else "unknown error"
160
+ error_code = response.error.code if response.error else 0
161
+ if error_code == ErrWorkflowAlreadyRunningCode:
162
+ raise WorkflowAlreadyRunningError(f"workflow '{id}' already has an active run: {error_msg}")
163
+ raise WorkflowError(f"start_workflow failed (code={error_code}): {error_msg}")
164
+
129
165
  return handle
grctl/models/errors.py CHANGED
@@ -4,3 +4,7 @@ class WorkflowError(Exception):
4
4
 
5
5
  class WorkflowNotFoundError(WorkflowError):
6
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."""
@@ -1,10 +1,14 @@
1
+ import logging
2
+
1
3
  from nats.js.api import AckPolicy, ConsumerConfig, DeliverPolicy
2
4
  from nats.js.client import JetStreamContext
3
- from nats.js.errors import FetchTimeoutError
5
+ from nats.js.errors import FetchTimeoutError, ServiceUnavailableError
4
6
 
5
7
  from grctl.models import HistoryEvent, history_decoder
6
8
  from grctl.nats.manifest import NatsManifest
7
9
 
10
+ logger = logging.getLogger(__name__)
11
+
8
12
  _FETCH_BATCH_SIZE = 256
9
13
  _FETCH_TIMEOUT_SECONDS = 0.25
10
14
 
@@ -29,14 +33,14 @@ async def fetch_run_history(
29
33
 
30
34
  events: list[HistoryEvent] = []
31
35
  try:
32
- try:
33
- while True:
34
- messages = await subscription.fetch(batch=_FETCH_BATCH_SIZE, timeout=_FETCH_TIMEOUT_SECONDS)
35
- events.extend(history_decoder(msg.data) for msg in messages if msg.data)
36
- except (TimeoutError, FetchTimeoutError):
37
- return events
36
+ while True:
37
+ messages = await subscription.fetch(batch=_FETCH_BATCH_SIZE, timeout=_FETCH_TIMEOUT_SECONDS)
38
+ events.extend(history_decoder(msg.data) for msg in messages if msg.data)
39
+ except (TimeoutError, FetchTimeoutError, ServiceUnavailableError):
40
+ pass
38
41
  finally:
39
42
  await subscription.unsubscribe()
43
+ return events
40
44
 
41
45
 
42
46
  async def fetch_step_history(
@@ -64,11 +68,11 @@ async def fetch_step_history(
64
68
 
65
69
  events: list[HistoryEvent] = []
66
70
  try:
67
- try:
68
- while True:
69
- messages = await subscription.fetch(batch=_FETCH_BATCH_SIZE, timeout=_FETCH_TIMEOUT_SECONDS)
70
- events.extend(history_decoder(msg.data) for msg in messages if msg.data)
71
- except (TimeoutError, FetchTimeoutError):
72
- return [event for event in events if event.operation_id]
71
+ while True:
72
+ messages = await subscription.fetch(batch=_FETCH_BATCH_SIZE, timeout=_FETCH_TIMEOUT_SECONDS)
73
+ events.extend(history_decoder(msg.data) for msg in messages if msg.data)
74
+ except (TimeoutError, FetchTimeoutError):
75
+ pass
73
76
  finally:
74
77
  await subscription.unsubscribe()
78
+ return [event for event in events if event.operation_id]
grctl/nats/kv_store.py CHANGED
@@ -37,7 +37,8 @@ class KVStore:
37
37
  return None
38
38
 
39
39
  except Exception as e:
40
- if "key not found" in str(e).lower():
40
+ msg = str(e).lower()
41
+ if "key not found" in msg or "no message found" in msg:
41
42
  return None
42
43
  raise
43
44
  else:
grctl/nats/publisher.py CHANGED
@@ -42,5 +42,5 @@ class Publisher:
42
42
  ) -> bytes:
43
43
  subject = self._manifest.api_subject(wf_id=run.wf_id)
44
44
  data = command_encoder(cmd)
45
- msg = await self._nc.request(subject, data)
45
+ msg = await self._nc.request(subject, data, timeout=5.0)
46
46
  return msg.data
grctl/worker/errors.py CHANGED
@@ -12,13 +12,6 @@ class WorkflowRunnerNotFoundError(Exception):
12
12
  super().__init__(f"WorkflowRunner not found for WorkflowRun ID '{run_id}'.")
13
13
 
14
14
 
15
- class WorkflowAlreadyRunningError(Exception):
16
- """Exception raised when attempting to start a workflow that is already running."""
17
-
18
- def __init__(self, run_id: str) -> None:
19
- super().__init__(f"WorkflowRun ID '{run_id}' is already running.")
20
-
21
-
22
15
  class NextDirectiveMissingError(Exception):
23
16
  """Exception raised when a workflow handler does not return a NextDirective."""
24
17
 
grctl/worker/runner.py CHANGED
@@ -122,7 +122,7 @@ class WorkflowRunner:
122
122
 
123
123
  spec = handler_config.spec
124
124
  handler = handler_config.handler
125
- if not spec.params:
125
+ if not spec.params or payload is None:
126
126
  directive = await handler(ctx)
127
127
 
128
128
  # Single param: if payload is already keyed by param name use the value,
grctl/worker/task.py CHANGED
@@ -365,7 +365,8 @@ async def _execute_task(
365
365
  return raw
366
366
  if isinstance(event, TaskFailed):
367
367
  raise _reconstruct_error(event.error)
368
- # TaskCancelled — task didn't finish; fall through to execute it live
368
+ if isinstance(event, TaskCancelled):
369
+ raise asyncio.CancelledError
369
370
 
370
371
  previous_attempts = sum(
371
372
  1
grctl/workflow/handle.py CHANGED
@@ -33,7 +33,7 @@ class WorkflowHandle:
33
33
  logger.debug("Attaching to existing workflow %s", self.run_info.wf_id)
34
34
  await self.future.start()
35
35
 
36
- async def start(self) -> None:
36
+ async def start(self) -> bytes:
37
37
  """Start the workflow future (subscribe to events and publish run command)."""
38
38
  input_value = self._codec.decode(self._codec.encode(self._payload)) if self._payload is not None else None
39
39
  cmd = Command(
@@ -48,7 +48,7 @@ class WorkflowHandle:
48
48
  logger.debug("Starting workflow history listener")
49
49
  await self.future.start()
50
50
  logger.debug("Publishing start command for workflow %s ", cmd)
51
- await self._connection.publisher.publish_cmd(self.run_info, cmd)
51
+ return await self._connection.publisher.publish_cmd(self.run_info, cmd)
52
52
 
53
53
  async def send(self, event_name: str, payload: Any | None = None) -> None:
54
54
  normalized = self._codec.decode(self._codec.encode(payload)) if payload is not None else None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grctl-sdk-python
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: The Python SDK for the Ground Control
5
5
  Author: Cem Evren Ates
6
6
  Author-email: Cem Evren Ates <cemevre@gmail.com>
@@ -1,45 +1,45 @@
1
1
  grctl/__init__.py,sha256=ovSyTyP0DJxBW_jC8CJrwYUs_BfXtMTnJqJ9qScZZyY,41
2
- grctl/client/__init__.py,sha256=FxY4a__J1SciqPTtouzQP8e76yRInchtrv_5OT9XoXc,259
3
- grctl/client/client.py,sha256=0ozm-sRz0kiV0U1VWJjNTCnd14uQJ4U_3Uk66JxGLZw,4510
2
+ grctl/client/__init__.py,sha256=ka0mRHB6DMhHqUJxOh9lJtn9_8-mDliq61dJI0BTb9s,461
3
+ grctl/client/client.py,sha256=sKaJnKNBlVpmaSoS82StFDEBLL5H5HFjKEtWtsTCWf0,5866
4
4
  grctl/logging_config.py,sha256=OMoz3FRnxAwkE9BNDuFxFu8aRzURnXeU9-75xVXiomg,1457
5
5
  grctl/models/__init__.py,sha256=-E1nliiY_Esiz-8Pc3WkyT7eYoph0czLsl3Uz8n6P4w,2752
6
6
  grctl/models/api.py,sha256=xygDA8b0E5BeQSrtfu_RToTW7XB2XWEO3n-zbh4W4S8,288
7
7
  grctl/models/command.py,sha256=SgnaAt2JNQOventxDU5C99-BAOB3WFhQADNsNfVhY54,2509
8
8
  grctl/models/common.py,sha256=XtqhHrgwIRlQGTEjrnsAfIPqajEpg9-4MkeSk87eemE,174
9
9
  grctl/models/directive.py,sha256=9N_v4YvI4Qew-_O4UHeBnQJ2HrDSkqo9uCflrCFltbY,4947
10
- grctl/models/errors.py,sha256=ckFlCRgmiYYWXqi22pY9cOIGRCgGXiG4FknhxjD4oqU,179
10
+ grctl/models/errors.py,sha256=6Nc8DVMLD_3UMeN-mMlwZKbqGo0EF66_3xT8hPGdzAY,294
11
11
  grctl/models/history.py,sha256=O_DNZcKG70b169dxBFH3HFw8vTKj9BtKeLGHkQDBoO4,8121
12
12
  grctl/models/run_info.py,sha256=OFbZ6ms_G4fpNTuScm03VbaYHpmQUwRzofvIIre3Eu8,2134
13
13
  grctl/models/run_info_helper.py,sha256=WG_lydSwkwX8PFxWZ9vMZw18cwFyQks-50x8-vMOlcE,301
14
14
  grctl/models/worker.py,sha256=Av8HQoTkkPUjV0ztq_NckC-Rem57g4ssTFyu8JrqsQo,1889
15
15
  grctl/nats/__init__.py,sha256=QTrNaWbv4WV1gLKcQlbPufZYDNCkww42edwxBoHVE-8,37
16
16
  grctl/nats/connection.py,sha256=DIQ7jz0XXRGlTeuuvV-cq4iNDj06V397vGo44Pnyqdg,1878
17
- grctl/nats/history_fetch.py,sha256=UuCWSe9jPP0Ry8HDePN_VOx0B07JNMxQmKpuxSxNTKY,2398
17
+ grctl/nats/history_fetch.py,sha256=QH7tsXhHmuwISWQezJ3MXGoOLFDvGavpncsSKkwtzAA,2454
18
18
  grctl/nats/history_sub.py,sha256=E-t5loBTYNU5OH_8Xr4WFGFo3jrU9URbvBc-TxGSmxw,2198
19
- grctl/nats/kv_store.py,sha256=fYoigg3_GHryYrphf9OoA-3uyffPr-YDz4Dt5G1u5OQ,1307
19
+ grctl/nats/kv_store.py,sha256=o4hXC_DxPa-Jg5OFeT5srVvLYgP-9LBDqL4_FY9zpXM,1358
20
20
  grctl/nats/manifest.py,sha256=NMxeuw5_j8WKqGdOp3DInGBLrYriye9RD3y4Ymr0mW8,9222
21
21
  grctl/nats/nats_client.py,sha256=3XK8K3_ix-3vcGnMXo_LAY2fn-14y8JVSjbFCF_6OC4,343
22
22
  grctl/nats/nats_manifest.yaml,sha256=FZXsJY3wqSNp3mhzh2awA_7NDq8OV8Mz0b2VK03T_dM,2661
23
- grctl/nats/publisher.py,sha256=-PCkfd4OIyjHa645oLuAGQyL3dy2bw0serMtmyF4f1o,1561
23
+ grctl/nats/publisher.py,sha256=5lqsXJ7rDP2Lis9cvniBygxdB-43OOjvuhWaVAw7PyA,1574
24
24
  grctl/nats/subscriber.py,sha256=0LG1CkR--fxPe5PuFTuFNEA1UoXupRvqvek2T0fpUQA,4436
25
25
  grctl/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
26
26
  grctl/settings.py,sha256=8O88vPcVzLHHv8JVxgNbwE3ErDqxjZf2_N4Tj35t7HM,601
27
27
  grctl/worker/__init__.py,sha256=dEnavSsxpbvAFonVcZiS-VPxtvgRneAp86XtuXqabiw,256
28
28
  grctl/worker/codec.py,sha256=ywioAv30GKxxAcAAbX7a3unFWpz9OBuzNXzy90i9iyw,1642
29
29
  grctl/worker/context.py,sha256=IzKZFS0LBQVTwgkiRaGwhUdCZs_YQrcFW3sqz_oqfZs,10439
30
- grctl/worker/errors.py,sha256=QFFruX23v1r-Xnc6gihRfLaapnUSY9HMBPWq5lwjB_U,1239
30
+ grctl/worker/errors.py,sha256=9zmDM2dulJTF6tBkS4ZeUAGqfjRIzsZNzrGJSlaH7d8,982
31
31
  grctl/worker/logger.py,sha256=vjHxk03ro1rVvDP2l94lfg-2yeN_pUks4tbrEm52YyQ,1364
32
32
  grctl/worker/run_manager.py,sha256=F1jdGrt8KS7WoLIacz6uQizfhTLpGP-21NLCgN3P4sA,4751
33
- grctl/worker/runner.py,sha256=icz-Q9SY3u4bma1iwzUaq6ifAo0E1QrWYBJknI495Ec,6866
33
+ grctl/worker/runner.py,sha256=kded9ikBLxGLA_sm0BrtMAayatiDpGwcuMjnWaFbadU,6885
34
34
  grctl/worker/runtime.py,sha256=MfuYfsV0aEhLmRqrPl4vdnOvLDsWhPpWnfp-nFEqgoY,6783
35
35
  grctl/worker/store.py,sha256=puLSeVV4RfWibELGsjdiztZcPBRRlIV1iupAaUr0JYs,1750
36
- grctl/worker/task.py,sha256=3FFbMiLdBTXAV2L6DI3kRfqYxXxu-0_mLUvieovh0TU,14487
36
+ grctl/worker/task.py,sha256=XC0mFqo3cKgejKZO5p2f7XdNpd_2uHQHLeYFRgy9_bA,14493
37
37
  grctl/worker/worker.py,sha256=RfA2_TdRDEjoTU7Q_5sTnaxgnoZVIiY6wa3G1UiF5RA,6137
38
38
  grctl/workflow/__init__.py,sha256=vu0cIFdbSgH6PSU60keXN2jkkkGn4bSG3UyRpIsSJXU,287
39
39
  grctl/workflow/future.py,sha256=oIzTArOYTwTpzVZBxxBxBeSNvswVhqE6CvJhd_-ljKE,5585
40
- grctl/workflow/handle.py,sha256=3dRhwLHRSfO7-wvka3YL_JEkUJLvy2Lnr1dMXWwAsuE,2671
40
+ grctl/workflow/handle.py,sha256=YPfGELtraB7GH60BCduP98oFCBBd5x-ewKRlYj1PxZY,2679
41
41
  grctl/workflow/workflow.py,sha256=jBdDBGjg6gE0t02CTN1-pBLk2DpMGEsk63TAuQXj7tQ,8602
42
- grctl_sdk_python-0.1.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
43
- grctl_sdk_python-0.1.3.dist-info/WHEEL,sha256=q5IF0q2xCp3ktUFRCVWsQLjl2ChNlWXBJtnI1LCGdJ8,80
44
- grctl_sdk_python-0.1.3.dist-info/METADATA,sha256=tK61B6ZhJKVHMZURivXut8gXVajyAj7iFaeOMsk5m9g,2309
45
- grctl_sdk_python-0.1.3.dist-info/RECORD,,
42
+ grctl_sdk_python-0.1.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
43
+ grctl_sdk_python-0.1.4.dist-info/WHEEL,sha256=fWriCkzqm-pffF5af4gJC9iI5FMFaJTuN9UxxxzOmdY,81
44
+ grctl_sdk_python-0.1.4.dist-info/METADATA,sha256=WWCWBeGqgUr1UO0xwASlxgjuuT7kWsh4hNjdlUGhoBQ,2309
45
+ grctl_sdk_python-0.1.4.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.11.8
2
+ Generator: uv 0.11.14
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any