grctl-sdk-python 0.1.3__tar.gz → 0.1.4__tar.gz

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 (46) hide show
  1. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/PKG-INFO +1 -1
  2. grctl_sdk_python-0.1.4/grctl/client/__init__.py +16 -0
  3. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/client/client.py +41 -5
  4. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/models/errors.py +4 -0
  5. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/nats/history_fetch.py +17 -13
  6. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/nats/kv_store.py +2 -1
  7. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/nats/publisher.py +1 -1
  8. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/worker/errors.py +0 -7
  9. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/worker/runner.py +1 -1
  10. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/worker/task.py +2 -1
  11. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/workflow/handle.py +2 -2
  12. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/pyproject.toml +1 -1
  13. grctl_sdk_python-0.1.3/grctl/client/__init__.py +0 -7
  14. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/LICENSE +0 -0
  15. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/README.md +0 -0
  16. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/__init__.py +0 -0
  17. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/logging_config.py +0 -0
  18. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/models/__init__.py +0 -0
  19. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/models/api.py +0 -0
  20. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/models/command.py +0 -0
  21. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/models/common.py +0 -0
  22. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/models/directive.py +0 -0
  23. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/models/history.py +0 -0
  24. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/models/run_info.py +0 -0
  25. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/models/run_info_helper.py +0 -0
  26. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/models/worker.py +0 -0
  27. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/nats/__init__.py +0 -0
  28. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/nats/connection.py +0 -0
  29. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/nats/history_sub.py +0 -0
  30. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/nats/manifest.py +0 -0
  31. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/nats/nats_client.py +0 -0
  32. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/nats/nats_manifest.yaml +0 -0
  33. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/nats/subscriber.py +0 -0
  34. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/py.typed +0 -0
  35. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/settings.py +0 -0
  36. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/worker/__init__.py +0 -0
  37. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/worker/codec.py +0 -0
  38. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/worker/context.py +0 -0
  39. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/worker/logger.py +0 -0
  40. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/worker/run_manager.py +0 -0
  41. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/worker/runtime.py +0 -0
  42. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/worker/store.py +0 -0
  43. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/worker/worker.py +0 -0
  44. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/workflow/__init__.py +0 -0
  45. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/workflow/future.py +0 -0
  46. {grctl_sdk_python-0.1.3 → grctl_sdk_python-0.1.4}/grctl/workflow/workflow.py +0 -0
@@ -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>
@@ -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
+ ]
@@ -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
@@ -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]
@@ -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:
@@ -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
@@ -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
 
@@ -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,
@@ -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
@@ -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
  [project]
2
2
  name = "grctl-sdk-python"
3
- version = "0.1.3"
3
+ version = "0.1.4"
4
4
  description = "The Python SDK for the Ground Control"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -1,7 +0,0 @@
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.nats.connection import Connection
6
-
7
- __all__ = ["Client", "Connection", "get_logger", "setup_logging"]