grctl-sdk-python 0.1.2__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.
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/PKG-INFO +13 -13
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/README.md +5 -5
- grctl_sdk_python-0.1.4/grctl/client/__init__.py +16 -0
- grctl_sdk_python-0.1.4/grctl/client/client.py +165 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/models/errors.py +4 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/models/history.py +3 -0
- grctl_sdk_python-0.1.4/grctl/nats/history_fetch.py +78 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/nats/kv_store.py +2 -1
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/nats/publisher.py +1 -1
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/nats/subscriber.py +2 -2
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/worker/context.py +7 -2
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/worker/errors.py +0 -7
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/worker/runner.py +1 -1
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/worker/task.py +2 -1
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/worker/worker.py +41 -21
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/workflow/future.py +12 -1
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/workflow/handle.py +2 -2
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/pyproject.toml +9 -8
- grctl_sdk_python-0.1.2/grctl/client/__init__.py +0 -7
- grctl_sdk_python-0.1.2/grctl/client/client.py +0 -111
- grctl_sdk_python-0.1.2/grctl/nats/history_fetch.py +0 -44
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/LICENSE +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/__init__.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/logging_config.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/models/__init__.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/models/api.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/models/command.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/models/common.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/models/directive.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/models/run_info.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/models/run_info_helper.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/models/worker.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/nats/__init__.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/nats/connection.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/nats/history_sub.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/nats/manifest.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/nats/nats_client.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/nats/nats_manifest.yaml +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/py.typed +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/settings.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/worker/__init__.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/worker/codec.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/worker/logger.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/worker/run_manager.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/worker/runtime.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/worker/store.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/workflow/__init__.py +0 -0
- {grctl_sdk_python-0.1.2 → grctl_sdk_python-0.1.4}/grctl/workflow/workflow.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: grctl-sdk-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: The Python SDK for the Ground Control
|
|
5
|
-
Author:
|
|
6
|
-
Author-email:
|
|
5
|
+
Author: Cem Evren Ates
|
|
6
|
+
Author-email: Cem Evren Ates <cemevre@gmail.com>
|
|
7
7
|
License-Expression: Apache-2.0
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Classifier: Development Status :: 3 - Alpha
|
|
@@ -25,18 +25,18 @@ Requires-Dist: pydantic-settings>=2.11.0
|
|
|
25
25
|
Requires-Dist: pyyaml>=6.0.3
|
|
26
26
|
Requires-Dist: pydantic>=2.12.5
|
|
27
27
|
Requires-Python: >=3.13
|
|
28
|
-
Project-URL: Homepage, https://github.com/
|
|
29
|
-
Project-URL: Documentation, https://
|
|
30
|
-
Project-URL: Repository, https://github.com/
|
|
31
|
-
Project-URL: Issues, https://github.com/
|
|
32
|
-
Project-URL: Changelog, https://github.com/
|
|
28
|
+
Project-URL: Homepage, https://github.com/grctl/sdk-python
|
|
29
|
+
Project-URL: Documentation, https://grctl.github.io/grctl/
|
|
30
|
+
Project-URL: Repository, https://github.com/grctl/sdk-python
|
|
31
|
+
Project-URL: Issues, https://github.com/grctl/sdk-python/issues
|
|
32
|
+
Project-URL: Changelog, https://github.com/grctl/sdk-python/blob/main/CHANGELOG.md
|
|
33
33
|
Description-Content-Type: text/markdown
|
|
34
34
|
|
|
35
35
|
# Ground Control Python SDK
|
|
36
36
|
|
|
37
|
-
The official Python SDK for [Ground Control](https://github.com/
|
|
37
|
+
The official Python SDK for [Ground Control](https://github.com/grctl/grctl) — a lightweight workflow orchestration engine built for fail-safe execution.
|
|
38
38
|
|
|
39
|
-
**[Documentation](https://
|
|
39
|
+
**[Documentation](https://grctl.github.io/grctl/)**
|
|
40
40
|
|
|
41
41
|
> [!WARNING]
|
|
42
42
|
> **Status: Pre-alpha**
|
|
@@ -55,12 +55,12 @@ pip install grctl-sdk-python
|
|
|
55
55
|
|
|
56
56
|
## Contributing
|
|
57
57
|
|
|
58
|
-
Contributions are welcome! Please read our [Contributing Guide](https://github.com/
|
|
58
|
+
Contributions are welcome! Please read our [Contributing Guide](https://github.com/grctl/grctl/blob/main/CONTRIBUTING.md) for more information.
|
|
59
59
|
|
|
60
60
|
## Security
|
|
61
61
|
|
|
62
|
-
Please see our [Security Policy](https://github.com/
|
|
62
|
+
Please see our [Security Policy](https://github.com/grctl/grctl/blob/main/SECURITY.md) for reporting vulnerabilities.
|
|
63
63
|
|
|
64
64
|
## License
|
|
65
65
|
|
|
66
|
-
This project is licensed under the Apache License 2.0 - see the [LICENSE](https://github.com/
|
|
66
|
+
This project is licensed under the Apache License 2.0 - see the [LICENSE](https://github.com/grctl/grctl/blob/main/LICENSE) file for details.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Ground Control Python SDK
|
|
2
2
|
|
|
3
|
-
The official Python SDK for [Ground Control](https://github.com/
|
|
3
|
+
The official Python SDK for [Ground Control](https://github.com/grctl/grctl) — a lightweight workflow orchestration engine built for fail-safe execution.
|
|
4
4
|
|
|
5
|
-
**[Documentation](https://
|
|
5
|
+
**[Documentation](https://grctl.github.io/grctl/)**
|
|
6
6
|
|
|
7
7
|
> [!WARNING]
|
|
8
8
|
> **Status: Pre-alpha**
|
|
@@ -21,12 +21,12 @@ pip install grctl-sdk-python
|
|
|
21
21
|
|
|
22
22
|
## Contributing
|
|
23
23
|
|
|
24
|
-
Contributions are welcome! Please read our [Contributing Guide](https://github.com/
|
|
24
|
+
Contributions are welcome! Please read our [Contributing Guide](https://github.com/grctl/grctl/blob/main/CONTRIBUTING.md) for more information.
|
|
25
25
|
|
|
26
26
|
## Security
|
|
27
27
|
|
|
28
|
-
Please see our [Security Policy](https://github.com/
|
|
28
|
+
Please see our [Security Policy](https://github.com/grctl/grctl/blob/main/SECURITY.md) for reporting vulnerabilities.
|
|
29
29
|
|
|
30
30
|
## License
|
|
31
31
|
|
|
32
|
-
This project is licensed under the Apache License 2.0 - see the [LICENSE](https://github.com/
|
|
32
|
+
This project is licensed under the Apache License 2.0 - see the [LICENSE](https://github.com/grctl/grctl/blob/main/LICENSE) file for details.
|
|
@@ -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
|
+
]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Workflow Engine Client.
|
|
2
|
+
|
|
3
|
+
Provides a simple interface for interacting with workflows.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
from datetime import UTC, datetime, timedelta
|
|
9
|
+
from typing import Any, TypeVar, overload
|
|
10
|
+
|
|
11
|
+
import msgspec
|
|
12
|
+
from ulid import ULID
|
|
13
|
+
|
|
14
|
+
from grctl.models import DescribeCmd, GrctlAPIResponse, HistoryEvent, RunInfo
|
|
15
|
+
from grctl.models.command import CmdKind, Command
|
|
16
|
+
from grctl.models.errors import WorkflowAlreadyRunningError, WorkflowError, WorkflowNotFoundError
|
|
17
|
+
from grctl.nats.connection import Connection
|
|
18
|
+
from grctl.nats.history_fetch import fetch_run_history
|
|
19
|
+
from grctl.worker.codec import CodecRegistry
|
|
20
|
+
from grctl.workflow.handle import WorkflowHandle
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
_T = TypeVar("_T")
|
|
25
|
+
|
|
26
|
+
ErrWorkflowAlreadyRunningCode = 4001
|
|
27
|
+
ErrWorkflowRunNotFoundCode = 4002
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Client:
|
|
31
|
+
"""Client for interacting with the Workflow Engine."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, connection: Connection, codec: CodecRegistry | None = None) -> None:
|
|
34
|
+
self._connection = connection
|
|
35
|
+
self._codec = codec or CodecRegistry()
|
|
36
|
+
|
|
37
|
+
async def describe(self, wf_id: str) -> RunInfo:
|
|
38
|
+
"""Describe the latest run for a workflow ID."""
|
|
39
|
+
cmd = Command(
|
|
40
|
+
id=str(ULID()),
|
|
41
|
+
kind=CmdKind.run_describe,
|
|
42
|
+
timestamp=datetime.now(UTC),
|
|
43
|
+
msg=DescribeCmd(wf_id=wf_id),
|
|
44
|
+
)
|
|
45
|
+
# Use a routing-only RunInfo — publish_cmd only needs wf_id for subject routing.
|
|
46
|
+
routing_info = RunInfo(id="", wf_type="", wf_id=wf_id)
|
|
47
|
+
response_bytes = await self._connection.publisher.publish_cmd(routing_info, cmd)
|
|
48
|
+
|
|
49
|
+
response = msgspec.msgpack.decode(response_bytes, type=GrctlAPIResponse)
|
|
50
|
+
if not response.success:
|
|
51
|
+
error_msg = response.error.message if response.error else "unknown error"
|
|
52
|
+
error_code = response.error.code if response.error else 0
|
|
53
|
+
if error_code == ErrWorkflowRunNotFoundCode:
|
|
54
|
+
raise WorkflowNotFoundError(f"workflow '{wf_id}' not found: {error_msg}")
|
|
55
|
+
raise WorkflowError(f"describe failed (code={error_code}): {error_msg}")
|
|
56
|
+
|
|
57
|
+
return msgspec.msgpack.decode(response.payload, type=RunInfo)
|
|
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
|
+
|
|
79
|
+
async def run_workflow(
|
|
80
|
+
self,
|
|
81
|
+
type: str, # noqa: A002
|
|
82
|
+
id: str, # noqa: A002
|
|
83
|
+
input: Any | None = None, # noqa: A002
|
|
84
|
+
timeout: timedelta | None = None, # noqa: ASYNC109
|
|
85
|
+
return_type: type[_T] | None = None,
|
|
86
|
+
) -> _T | Any:
|
|
87
|
+
"""Run a workflow and wait for its result."""
|
|
88
|
+
wf_handle = await self.start_workflow(
|
|
89
|
+
type=type,
|
|
90
|
+
id=id,
|
|
91
|
+
input=input,
|
|
92
|
+
timeout=timeout,
|
|
93
|
+
)
|
|
94
|
+
wait_timeout = timeout.total_seconds() if timeout else None
|
|
95
|
+
try:
|
|
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
|
|
100
|
+
finally:
|
|
101
|
+
await wf_handle.future.stop()
|
|
102
|
+
|
|
103
|
+
async def get_workflow_handle(self, wfid: str) -> WorkflowHandle:
|
|
104
|
+
"""Get a handle for an already-running workflow."""
|
|
105
|
+
run_info = await self.describe(wfid)
|
|
106
|
+
|
|
107
|
+
handle = WorkflowHandle(
|
|
108
|
+
run_info=run_info,
|
|
109
|
+
payload=None,
|
|
110
|
+
connection=self._connection,
|
|
111
|
+
codec=self._codec,
|
|
112
|
+
)
|
|
113
|
+
await handle.attach()
|
|
114
|
+
return handle
|
|
115
|
+
|
|
116
|
+
async def get_history(self, wf_id: str, run_id: str | None = None) -> list[HistoryEvent]:
|
|
117
|
+
"""Return the ordered history events for a workflow run."""
|
|
118
|
+
resolved_run_id = run_id
|
|
119
|
+
if resolved_run_id is None:
|
|
120
|
+
resolved_run_id = (await self.describe(wf_id)).id
|
|
121
|
+
|
|
122
|
+
return await fetch_run_history(
|
|
123
|
+
js=self._connection.js,
|
|
124
|
+
manifest=self._connection.manifest,
|
|
125
|
+
wf_id=wf_id,
|
|
126
|
+
run_id=resolved_run_id,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
async def start_workflow(
|
|
130
|
+
self,
|
|
131
|
+
type: str, # noqa: A002
|
|
132
|
+
id: str, # noqa: A002
|
|
133
|
+
input: Any | None = None, # noqa: A002
|
|
134
|
+
timeout: timedelta | None = None, # noqa: ASYNC109
|
|
135
|
+
) -> WorkflowHandle:
|
|
136
|
+
"""Start a workflow and return a handle to track and interact with it."""
|
|
137
|
+
workflow_run_id = str(ULID())
|
|
138
|
+
|
|
139
|
+
run_info = RunInfo(
|
|
140
|
+
id=workflow_run_id,
|
|
141
|
+
wf_type=type,
|
|
142
|
+
wf_id=id,
|
|
143
|
+
timeout=int(timeout.total_seconds()) if timeout else None,
|
|
144
|
+
created_at=datetime.now(UTC),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
handle = WorkflowHandle(
|
|
148
|
+
run_info=run_info,
|
|
149
|
+
payload=input,
|
|
150
|
+
connection=self._connection,
|
|
151
|
+
codec=self._codec,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Start the workflow future (subscribe to events and publish run command)
|
|
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
|
+
|
|
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."""
|
|
@@ -202,6 +202,7 @@ class ChildWorkflowStarted(msgspec.Struct):
|
|
|
202
202
|
run_id: str
|
|
203
203
|
wf_type: str
|
|
204
204
|
wf_id: str
|
|
205
|
+
input: Any | None = None
|
|
205
206
|
|
|
206
207
|
|
|
207
208
|
class ParentEventSent(msgspec.Struct):
|
|
@@ -209,6 +210,8 @@ class ParentEventSent(msgspec.Struct):
|
|
|
209
210
|
|
|
210
211
|
event_name: str
|
|
211
212
|
payload: Any
|
|
213
|
+
parent_wf_type: str
|
|
214
|
+
parent_wf_id: str
|
|
212
215
|
|
|
213
216
|
|
|
214
217
|
RunEvents = RunCancelScheduled | RunCancelled | RunCompleted | RunFailed | RunScheduled | RunStarted | RunTimeout
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from nats.js.api import AckPolicy, ConsumerConfig, DeliverPolicy
|
|
4
|
+
from nats.js.client import JetStreamContext
|
|
5
|
+
from nats.js.errors import FetchTimeoutError, ServiceUnavailableError
|
|
6
|
+
|
|
7
|
+
from grctl.models import HistoryEvent, history_decoder
|
|
8
|
+
from grctl.nats.manifest import NatsManifest
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
_FETCH_BATCH_SIZE = 256
|
|
13
|
+
_FETCH_TIMEOUT_SECONDS = 0.25
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def fetch_run_history(
|
|
17
|
+
js: JetStreamContext,
|
|
18
|
+
manifest: NatsManifest,
|
|
19
|
+
wf_id: str,
|
|
20
|
+
run_id: str,
|
|
21
|
+
) -> list[HistoryEvent]:
|
|
22
|
+
history_subject = manifest.history_subject(wf_id=wf_id, run_id=run_id)
|
|
23
|
+
history_stream = manifest.history_stream_name()
|
|
24
|
+
subscription = await js.pull_subscribe(
|
|
25
|
+
subject=history_subject,
|
|
26
|
+
stream=history_stream,
|
|
27
|
+
config=ConsumerConfig(
|
|
28
|
+
deliver_policy=DeliverPolicy.ALL,
|
|
29
|
+
ack_policy=AckPolicy.NONE,
|
|
30
|
+
inactive_threshold=1.0,
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
events: list[HistoryEvent] = []
|
|
35
|
+
try:
|
|
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
|
|
41
|
+
finally:
|
|
42
|
+
await subscription.unsubscribe()
|
|
43
|
+
return events
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def fetch_step_history(
|
|
47
|
+
js: JetStreamContext,
|
|
48
|
+
manifest: NatsManifest,
|
|
49
|
+
wf_id: str,
|
|
50
|
+
run_id: str,
|
|
51
|
+
history_seq_id: int,
|
|
52
|
+
) -> list[HistoryEvent]:
|
|
53
|
+
if history_seq_id <= 0:
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
history_subject = manifest.history_subject(wf_id=wf_id, run_id=run_id)
|
|
57
|
+
history_stream = manifest.history_stream_name()
|
|
58
|
+
subscription = await js.pull_subscribe(
|
|
59
|
+
subject=history_subject,
|
|
60
|
+
stream=history_stream,
|
|
61
|
+
config=ConsumerConfig(
|
|
62
|
+
deliver_policy=DeliverPolicy.BY_START_SEQUENCE,
|
|
63
|
+
opt_start_seq=history_seq_id,
|
|
64
|
+
ack_policy=AckPolicy.NONE,
|
|
65
|
+
inactive_threshold=1.0,
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
events: list[HistoryEvent] = []
|
|
70
|
+
try:
|
|
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
|
|
76
|
+
finally:
|
|
77
|
+
await subscription.unsubscribe()
|
|
78
|
+
return [event for event in events if event.operation_id]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import contextlib
|
|
3
3
|
import json
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
import msgspec
|
|
6
7
|
from nats.aio.msg import Msg
|
|
@@ -11,7 +12,6 @@ from grctl.logging_config import get_logger
|
|
|
11
12
|
from grctl.models import directive_decoder
|
|
12
13
|
from grctl.nats.manifest import NatsManifest
|
|
13
14
|
from grctl.settings import get_settings
|
|
14
|
-
from grctl.worker.run_manager import RunManager
|
|
15
15
|
|
|
16
16
|
logger = get_logger(__name__)
|
|
17
17
|
|
|
@@ -24,7 +24,7 @@ class Subscriber:
|
|
|
24
24
|
js: JetStreamContext,
|
|
25
25
|
manifest: NatsManifest,
|
|
26
26
|
wf_types: list[str],
|
|
27
|
-
run_manager:
|
|
27
|
+
run_manager: Any,
|
|
28
28
|
) -> None:
|
|
29
29
|
self._js = js
|
|
30
30
|
self._manifest = manifest
|
|
@@ -198,7 +198,12 @@ class Context:
|
|
|
198
198
|
)
|
|
199
199
|
await runtime.record(
|
|
200
200
|
HistoryKind.parent_event_sent,
|
|
201
|
-
ParentEventSent(
|
|
201
|
+
ParentEventSent(
|
|
202
|
+
event_name=event_name,
|
|
203
|
+
payload=payload,
|
|
204
|
+
parent_wf_type=self._parent_run.wf_type,
|
|
205
|
+
parent_wf_id=self._parent_run.wf_id,
|
|
206
|
+
),
|
|
202
207
|
operation_id,
|
|
203
208
|
)
|
|
204
209
|
|
|
@@ -244,7 +249,7 @@ class Context:
|
|
|
244
249
|
await handle.start()
|
|
245
250
|
await runtime.record(
|
|
246
251
|
HistoryKind.child_started,
|
|
247
|
-
ChildWorkflowStarted(run_id=run_id, wf_type=workflow_type, wf_id=workflow_id),
|
|
252
|
+
ChildWorkflowStarted(run_id=run_id, wf_type=workflow_type, wf_id=workflow_id, input=workflow_input),
|
|
248
253
|
operation_id,
|
|
249
254
|
)
|
|
250
255
|
return handle
|
|
@@ -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
|
-
|
|
368
|
+
if isinstance(event, TaskCancelled):
|
|
369
|
+
raise asyncio.CancelledError
|
|
369
370
|
|
|
370
371
|
previous_attempts = sum(
|
|
371
372
|
1
|
|
@@ -19,6 +19,7 @@ from grctl.workflow.workflow import Workflow
|
|
|
19
19
|
|
|
20
20
|
logger = get_logger(__name__)
|
|
21
21
|
|
|
22
|
+
|
|
22
23
|
# Constants
|
|
23
24
|
DEFAULT_WORKFLOW_TIMEOUT_SECONDS: float = 30.0
|
|
24
25
|
WORKER_HEARTBEAT_INTERVAL_SECONDS: int = 1
|
|
@@ -54,8 +55,10 @@ class Worker:
|
|
|
54
55
|
self._connection = connection
|
|
55
56
|
self._workflow_logger = workflow_logger
|
|
56
57
|
self._stop_event = asyncio.Event()
|
|
58
|
+
self._startup_event = asyncio.Event()
|
|
57
59
|
self._subscriber: Subscriber | None = None
|
|
58
60
|
self._run_manager: RunManager | None = None
|
|
61
|
+
self._startup_error: Exception | None = None
|
|
59
62
|
|
|
60
63
|
@cached_property
|
|
61
64
|
def worker_name(self) -> str:
|
|
@@ -81,31 +84,48 @@ class Worker:
|
|
|
81
84
|
|
|
82
85
|
Creates RunManager for workflow execution and subscribes to workflow subjects.
|
|
83
86
|
"""
|
|
87
|
+
self._startup_event.clear()
|
|
88
|
+
self._startup_error = None
|
|
89
|
+
|
|
84
90
|
logger.info(
|
|
85
91
|
f"Starting worker with {len(self._workflows)} registered workflows",
|
|
86
92
|
)
|
|
87
93
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
94
|
+
try:
|
|
95
|
+
self._run_manager = RunManager(
|
|
96
|
+
worker_name=self.worker_name,
|
|
97
|
+
worker_id=self.worker_id,
|
|
98
|
+
workflows=self._workflows,
|
|
99
|
+
connection=self._connection,
|
|
100
|
+
workflow_logger=self._workflow_logger,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
wf_types = [wf.workflow_type for wf in self._workflows]
|
|
104
|
+
self._subscriber = Subscriber(
|
|
105
|
+
js=self._connection.js,
|
|
106
|
+
manifest=self._connection.manifest,
|
|
107
|
+
wf_types=wf_types,
|
|
108
|
+
run_manager=self._run_manager,
|
|
109
|
+
)
|
|
110
|
+
await self._subscriber.start()
|
|
111
|
+
self._startup_event.set()
|
|
112
|
+
|
|
113
|
+
logger.info(f"Worker {self.worker_name} ({self.worker_id}) started and ready to process messages")
|
|
114
|
+
|
|
115
|
+
# Keep worker alive
|
|
116
|
+
await self._process_messages()
|
|
117
|
+
except Exception as exc:
|
|
118
|
+
self._startup_error = exc
|
|
119
|
+
self._startup_event.set()
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
async def wait_until_ready(self, timeout_ms: float = 5.0) -> None:
|
|
123
|
+
"""Wait until worker startup succeeds or fails."""
|
|
124
|
+
await asyncio.wait_for(self._startup_event.wait(), timeout=timeout_ms)
|
|
125
|
+
if self._startup_error is not None:
|
|
126
|
+
raise self._startup_error
|
|
127
|
+
if self._subscriber is None:
|
|
128
|
+
raise RuntimeError("Worker startup completed without creating a subscriber")
|
|
109
129
|
|
|
110
130
|
async def _process_messages(self) -> None:
|
|
111
131
|
"""Keep worker alive to process commands."""
|
|
@@ -41,6 +41,8 @@ class WorkflowFuture(asyncio.Future[Any]):
|
|
|
41
41
|
run_id=run_info.id,
|
|
42
42
|
handler=self._handle_history_event,
|
|
43
43
|
)
|
|
44
|
+
self._subscriber_stopped = False
|
|
45
|
+
self.add_done_callback(self._schedule_subscriber_stop)
|
|
44
46
|
self._history_update_handlers: dict[HistoryKind, Callable[[HistoryEvent], None]] = {
|
|
45
47
|
HistoryKind.run_scheduled: self._on_non_terminal_event,
|
|
46
48
|
HistoryKind.run_started: self._on_non_terminal_event,
|
|
@@ -54,9 +56,18 @@ class WorkflowFuture(asyncio.Future[Any]):
|
|
|
54
56
|
"""Start listening for events and publish run command."""
|
|
55
57
|
await self._subscriber.start()
|
|
56
58
|
|
|
59
|
+
def _schedule_subscriber_stop(self, _: asyncio.Future) -> None:
|
|
60
|
+
# done_callback must be sync, so we schedule the async stop as a task.
|
|
61
|
+
if self._subscriber_stopped:
|
|
62
|
+
return
|
|
63
|
+
self._subscriber_stopped = True
|
|
64
|
+
asyncio.ensure_future(self._subscriber.stop()) # noqa: RUF006
|
|
65
|
+
|
|
57
66
|
async def stop(self) -> None:
|
|
58
67
|
"""Stop listening for events and cleanup."""
|
|
59
|
-
|
|
68
|
+
if not self._subscriber_stopped:
|
|
69
|
+
self._subscriber_stopped = True
|
|
70
|
+
await self._subscriber.stop()
|
|
60
71
|
|
|
61
72
|
if not self.done():
|
|
62
73
|
self.cancel()
|
|
@@ -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) ->
|
|
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,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "grctl-sdk-python"
|
|
3
|
-
version = "0.1.
|
|
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"
|
|
7
7
|
license = "Apache-2.0"
|
|
8
8
|
license-files = ["LICENSE"]
|
|
9
9
|
authors = [
|
|
10
|
-
{ name = "
|
|
10
|
+
{ name = "Cem Evren Ates", email = "cemevre@gmail.com" },
|
|
11
11
|
]
|
|
12
12
|
classifiers = [
|
|
13
13
|
"Development Status :: 3 - Alpha",
|
|
@@ -33,11 +33,11 @@ dependencies = [
|
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
[project.urls]
|
|
36
|
-
Homepage = "https://github.com/
|
|
37
|
-
Documentation = "https://
|
|
38
|
-
Repository = "https://github.com/
|
|
39
|
-
Issues = "https://github.com/
|
|
40
|
-
Changelog = "https://github.com/
|
|
36
|
+
Homepage = "https://github.com/grctl/sdk-python"
|
|
37
|
+
Documentation = "https://grctl.github.io/grctl/"
|
|
38
|
+
Repository = "https://github.com/grctl/sdk-python"
|
|
39
|
+
Issues = "https://github.com/grctl/sdk-python/issues"
|
|
40
|
+
Changelog = "https://github.com/grctl/sdk-python/blob/main/CHANGELOG.md"
|
|
41
41
|
|
|
42
42
|
[build-system]
|
|
43
43
|
requires = ["uv_build>=0.11.6,<0.12"]
|
|
@@ -60,7 +60,7 @@ module-name = "grctl"
|
|
|
60
60
|
module-root = ""
|
|
61
61
|
|
|
62
62
|
[tool.pytest.ini_options]
|
|
63
|
-
addopts = "--ignore=tests/examples --ignore=tests/e2e"
|
|
63
|
+
addopts = "--ignore=tests/examples --ignore=tests/e2e --ignore=tests/spec"
|
|
64
64
|
testpaths = ["tests"]
|
|
65
65
|
log_cli = true
|
|
66
66
|
log_cli_level = "DEBUG"
|
|
@@ -107,6 +107,7 @@ lint.ignore = [
|
|
|
107
107
|
"ANN204", # Allow missing type annotations for self and cls in tests
|
|
108
108
|
"ARG", # Unused function args for fixtures
|
|
109
109
|
"COM812",
|
|
110
|
+
"D104", # Missing docstring in public package
|
|
110
111
|
"FBT", # Don't care about booleans as positional arguments in tests for @pytest.mark.parametrize
|
|
111
112
|
"INP001", # Ignore that its not in a real python module. Doesn't matter for tests
|
|
112
113
|
# The below are debateable
|
|
@@ -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"]
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
"""Workflow Engine Client.
|
|
2
|
-
|
|
3
|
-
Provides a simple interface for interacting with workflows.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import asyncio
|
|
7
|
-
import logging
|
|
8
|
-
from datetime import UTC, datetime, timedelta
|
|
9
|
-
from typing import Any
|
|
10
|
-
|
|
11
|
-
import msgspec
|
|
12
|
-
from ulid import ULID
|
|
13
|
-
|
|
14
|
-
from grctl.models import DescribeCmd, GrctlAPIResponse, RunInfo
|
|
15
|
-
from grctl.models.command import CmdKind, Command
|
|
16
|
-
from grctl.models.errors import WorkflowError, WorkflowNotFoundError
|
|
17
|
-
from grctl.nats.connection import Connection
|
|
18
|
-
from grctl.worker.codec import CodecRegistry
|
|
19
|
-
from grctl.workflow.handle import WorkflowHandle
|
|
20
|
-
|
|
21
|
-
logger = logging.getLogger(__name__)
|
|
22
|
-
|
|
23
|
-
ErrWorkflowRunNotFoundCode = 4002
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class Client:
|
|
27
|
-
"""Client for interacting with the Workflow Engine."""
|
|
28
|
-
|
|
29
|
-
def __init__(self, connection: Connection, codec: CodecRegistry | None = None) -> None:
|
|
30
|
-
self._connection = connection
|
|
31
|
-
self._codec = codec or CodecRegistry()
|
|
32
|
-
|
|
33
|
-
async def run_workflow(
|
|
34
|
-
self,
|
|
35
|
-
workflow_type: str,
|
|
36
|
-
workflow_id: str,
|
|
37
|
-
workflow_input: Any | None = None,
|
|
38
|
-
workflow_timeout: timedelta | None = None,
|
|
39
|
-
) -> Any:
|
|
40
|
-
"""Run a workflow and wait for its result."""
|
|
41
|
-
wf_handle = await self.start_workflow(
|
|
42
|
-
workflow_type=workflow_type,
|
|
43
|
-
workflow_id=workflow_id,
|
|
44
|
-
workflow_input=workflow_input,
|
|
45
|
-
workflow_timeout=workflow_timeout,
|
|
46
|
-
)
|
|
47
|
-
timeout = workflow_timeout.total_seconds() if workflow_timeout else None
|
|
48
|
-
try:
|
|
49
|
-
return await asyncio.wait_for(wf_handle.future, timeout=timeout)
|
|
50
|
-
finally:
|
|
51
|
-
await wf_handle.future.stop()
|
|
52
|
-
|
|
53
|
-
async def get_workflow_handle(self, workflow_id: str) -> WorkflowHandle:
|
|
54
|
-
"""Get a handle for an already-running workflow."""
|
|
55
|
-
cmd = Command(
|
|
56
|
-
id=str(ULID()),
|
|
57
|
-
kind=CmdKind.run_describe,
|
|
58
|
-
timestamp=datetime.now(UTC),
|
|
59
|
-
msg=DescribeCmd(wf_id=workflow_id),
|
|
60
|
-
)
|
|
61
|
-
# Use a routing-only RunInfo — publish_cmd only needs wf_id for subject routing.
|
|
62
|
-
routing_info = RunInfo(id="", wf_type="", wf_id=workflow_id)
|
|
63
|
-
response_bytes = await self._connection.publisher.publish_cmd(routing_info, cmd)
|
|
64
|
-
|
|
65
|
-
response = msgspec.msgpack.decode(response_bytes, type=GrctlAPIResponse)
|
|
66
|
-
if not response.success:
|
|
67
|
-
error_msg = response.error.message if response.error else "unknown error"
|
|
68
|
-
error_code = response.error.code if response.error else 0
|
|
69
|
-
if error_code == ErrWorkflowRunNotFoundCode:
|
|
70
|
-
raise WorkflowNotFoundError(f"workflow '{workflow_id}' not found: {error_msg}")
|
|
71
|
-
raise WorkflowError(f"describe failed (code={error_code}): {error_msg}")
|
|
72
|
-
|
|
73
|
-
run_info = msgspec.msgpack.decode(response.payload, type=RunInfo)
|
|
74
|
-
|
|
75
|
-
handle = WorkflowHandle(
|
|
76
|
-
run_info=run_info,
|
|
77
|
-
payload=None,
|
|
78
|
-
connection=self._connection,
|
|
79
|
-
codec=self._codec,
|
|
80
|
-
)
|
|
81
|
-
await handle.attach()
|
|
82
|
-
return handle
|
|
83
|
-
|
|
84
|
-
async def start_workflow(
|
|
85
|
-
self,
|
|
86
|
-
workflow_type: str,
|
|
87
|
-
workflow_id: str,
|
|
88
|
-
workflow_input: Any | None = None,
|
|
89
|
-
workflow_timeout: timedelta | None = None,
|
|
90
|
-
) -> WorkflowHandle:
|
|
91
|
-
"""Start a workflow and return a handle to track and interact with it."""
|
|
92
|
-
workflow_run_id = str(ULID())
|
|
93
|
-
|
|
94
|
-
run_info = RunInfo(
|
|
95
|
-
id=workflow_run_id,
|
|
96
|
-
wf_type=workflow_type,
|
|
97
|
-
wf_id=workflow_id,
|
|
98
|
-
timeout=int(workflow_timeout.total_seconds()) if workflow_timeout else None,
|
|
99
|
-
created_at=datetime.now(UTC),
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
handle = WorkflowHandle(
|
|
103
|
-
run_info=run_info,
|
|
104
|
-
payload=workflow_input,
|
|
105
|
-
connection=self._connection,
|
|
106
|
-
codec=self._codec,
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
# Start the workflow future (subscribe to events and publish run command)
|
|
110
|
-
await handle.start()
|
|
111
|
-
return handle
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
from nats.js.api import AckPolicy, ConsumerConfig, DeliverPolicy
|
|
2
|
-
from nats.js.client import JetStreamContext
|
|
3
|
-
from nats.js.errors import FetchTimeoutError
|
|
4
|
-
|
|
5
|
-
from grctl.models import HistoryEvent, history_decoder
|
|
6
|
-
from grctl.nats.manifest import NatsManifest
|
|
7
|
-
|
|
8
|
-
_FETCH_BATCH_SIZE = 256
|
|
9
|
-
_FETCH_TIMEOUT_SECONDS = 0.25
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
async def fetch_step_history(
|
|
13
|
-
js: JetStreamContext,
|
|
14
|
-
manifest: NatsManifest,
|
|
15
|
-
wf_id: str,
|
|
16
|
-
run_id: str,
|
|
17
|
-
history_seq_id: int,
|
|
18
|
-
) -> list[HistoryEvent]:
|
|
19
|
-
if history_seq_id <= 0:
|
|
20
|
-
return []
|
|
21
|
-
|
|
22
|
-
history_subject = manifest.history_subject(wf_id=wf_id, run_id=run_id)
|
|
23
|
-
history_stream = manifest.history_stream_name()
|
|
24
|
-
subscription = await js.pull_subscribe(
|
|
25
|
-
subject=history_subject,
|
|
26
|
-
stream=history_stream,
|
|
27
|
-
config=ConsumerConfig(
|
|
28
|
-
deliver_policy=DeliverPolicy.BY_START_SEQUENCE,
|
|
29
|
-
opt_start_seq=history_seq_id,
|
|
30
|
-
ack_policy=AckPolicy.NONE,
|
|
31
|
-
inactive_threshold=1.0,
|
|
32
|
-
),
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
events: list[HistoryEvent] = []
|
|
36
|
-
try:
|
|
37
|
-
try:
|
|
38
|
-
while True:
|
|
39
|
-
messages = await subscription.fetch(batch=_FETCH_BATCH_SIZE, timeout=_FETCH_TIMEOUT_SECONDS)
|
|
40
|
-
events.extend(history_decoder(msg.data) for msg in messages if msg.data)
|
|
41
|
-
except (TimeoutError, FetchTimeoutError):
|
|
42
|
-
return [event for event in events if event.operation_id]
|
|
43
|
-
finally:
|
|
44
|
-
await subscription.unsubscribe()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|