hatchet-sdk 1.0.2__py3-none-any.whl → 1.1.0__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.
Potentially problematic release.
This version of hatchet-sdk might be problematic. Click here for more details.
- hatchet_sdk/client.py +3 -16
- hatchet_sdk/clients/admin.py +13 -33
- hatchet_sdk/clients/dispatcher/action_listener.py +7 -11
- hatchet_sdk/clients/dispatcher/dispatcher.py +20 -5
- hatchet_sdk/clients/durable_event_listener.py +11 -12
- hatchet_sdk/clients/events.py +11 -15
- hatchet_sdk/clients/rest/models/tenant_resource.py +2 -0
- hatchet_sdk/clients/rest/models/workflow_runs_metrics.py +1 -5
- hatchet_sdk/clients/run_event_listener.py +62 -66
- hatchet_sdk/clients/v1/api_client.py +1 -38
- hatchet_sdk/clients/workflow_listener.py +9 -10
- hatchet_sdk/context/context.py +9 -0
- hatchet_sdk/contracts/dispatcher_pb2_grpc.py +1 -1
- hatchet_sdk/contracts/events_pb2_grpc.py +1 -1
- hatchet_sdk/contracts/v1/dispatcher_pb2_grpc.py +1 -1
- hatchet_sdk/contracts/v1/workflows_pb2_grpc.py +1 -1
- hatchet_sdk/contracts/workflows_pb2_grpc.py +1 -1
- hatchet_sdk/features/cron.py +5 -4
- hatchet_sdk/features/logs.py +2 -1
- hatchet_sdk/features/metrics.py +4 -3
- hatchet_sdk/features/rate_limits.py +1 -1
- hatchet_sdk/features/runs.py +8 -7
- hatchet_sdk/features/scheduled.py +5 -4
- hatchet_sdk/features/workers.py +4 -3
- hatchet_sdk/features/workflows.py +4 -3
- hatchet_sdk/metadata.py +2 -2
- hatchet_sdk/runnables/standalone.py +3 -18
- hatchet_sdk/utils/aio.py +43 -0
- hatchet_sdk/worker/runner/run_loop_manager.py +1 -1
- hatchet_sdk/workflow_run.py +7 -20
- {hatchet_sdk-1.0.2.dist-info → hatchet_sdk-1.1.0.dist-info}/METADATA +1 -1
- {hatchet_sdk-1.0.2.dist-info → hatchet_sdk-1.1.0.dist-info}/RECORD +34 -34
- {hatchet_sdk-1.0.2.dist-info → hatchet_sdk-1.1.0.dist-info}/entry_points.txt +1 -0
- hatchet_sdk/utils/aio_utils.py +0 -18
- {hatchet_sdk-1.0.2.dist-info → hatchet_sdk-1.1.0.dist-info}/WHEEL +0 -0
hatchet_sdk/client.py
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
|
|
3
|
-
import grpc
|
|
4
|
-
|
|
5
1
|
from hatchet_sdk.clients.admin import AdminClient
|
|
6
2
|
from hatchet_sdk.clients.dispatcher.dispatcher import DispatcherClient
|
|
7
|
-
from hatchet_sdk.clients.events import EventClient
|
|
3
|
+
from hatchet_sdk.clients.events import EventClient
|
|
8
4
|
from hatchet_sdk.clients.run_event_listener import RunEventListenerClient
|
|
9
5
|
from hatchet_sdk.clients.workflow_listener import PooledWorkflowRunListener
|
|
10
6
|
from hatchet_sdk.config import ClientConfig
|
|
11
|
-
from hatchet_sdk.connection import new_conn
|
|
12
7
|
from hatchet_sdk.features.cron import CronClient
|
|
13
8
|
from hatchet_sdk.features.logs import LogsClient
|
|
14
9
|
from hatchet_sdk.features.metrics import MetricsClient
|
|
@@ -29,21 +24,13 @@ class Client:
|
|
|
29
24
|
workflow_listener: PooledWorkflowRunListener | None | None = None,
|
|
30
25
|
debug: bool = False,
|
|
31
26
|
):
|
|
32
|
-
try:
|
|
33
|
-
loop = asyncio.get_running_loop()
|
|
34
|
-
except RuntimeError:
|
|
35
|
-
loop = asyncio.new_event_loop()
|
|
36
|
-
asyncio.set_event_loop(loop)
|
|
37
|
-
|
|
38
|
-
conn: grpc.Channel = new_conn(config, False)
|
|
39
|
-
|
|
40
27
|
self.config = config
|
|
41
28
|
self.admin = admin_client or AdminClient(config)
|
|
42
29
|
self.dispatcher = dispatcher_client or DispatcherClient(config)
|
|
43
|
-
self.event = event_client or
|
|
30
|
+
self.event = event_client or EventClient(config)
|
|
44
31
|
self.listener = RunEventListenerClient(config)
|
|
45
32
|
self.workflow_listener = workflow_listener
|
|
46
|
-
self.
|
|
33
|
+
self.log_interceptor = config.logger
|
|
47
34
|
self.debug = debug
|
|
48
35
|
|
|
49
36
|
self.cron = CronClient(self.config)
|
hatchet_sdk/clients/admin.py
CHANGED
|
@@ -8,8 +8,6 @@ from google.protobuf import timestamp_pb2
|
|
|
8
8
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
9
9
|
|
|
10
10
|
from hatchet_sdk.clients.rest.tenacity_utils import tenacity_retry
|
|
11
|
-
from hatchet_sdk.clients.run_event_listener import RunEventListenerClient
|
|
12
|
-
from hatchet_sdk.clients.workflow_listener import PooledWorkflowRunListener
|
|
13
11
|
from hatchet_sdk.config import ClientConfig
|
|
14
12
|
from hatchet_sdk.connection import new_conn
|
|
15
13
|
from hatchet_sdk.contracts import workflows_pb2 as v0_workflow_protos
|
|
@@ -36,6 +34,7 @@ class ScheduleTriggerWorkflowOptions(BaseModel):
|
|
|
36
34
|
child_index: int | None = None
|
|
37
35
|
child_key: str | None = None
|
|
38
36
|
namespace: str | None = None
|
|
37
|
+
additional_metadata: JSONSerializableMapping = Field(default_factory=dict)
|
|
39
38
|
|
|
40
39
|
|
|
41
40
|
class TriggerWorkflowOptions(ScheduleTriggerWorkflowOptions):
|
|
@@ -63,14 +62,11 @@ class AdminClient:
|
|
|
63
62
|
def __init__(self, config: ClientConfig):
|
|
64
63
|
conn = new_conn(config, False)
|
|
65
64
|
self.config = config
|
|
66
|
-
self.client = AdminServiceStub(conn)
|
|
67
|
-
self.v0_client = WorkflowServiceStub(conn)
|
|
65
|
+
self.client = AdminServiceStub(conn)
|
|
66
|
+
self.v0_client = WorkflowServiceStub(conn)
|
|
68
67
|
self.token = config.token
|
|
69
|
-
self.listener_client = RunEventListenerClient(config=config)
|
|
70
68
|
self.namespace = config.namespace
|
|
71
69
|
|
|
72
|
-
self.pooled_workflow_listener: PooledWorkflowRunListener | None = None
|
|
73
|
-
|
|
74
70
|
class TriggerWorkflowRequest(BaseModel):
|
|
75
71
|
model_config = ConfigDict(extra="ignore")
|
|
76
72
|
|
|
@@ -153,7 +149,11 @@ class AdminClient:
|
|
|
153
149
|
name=name,
|
|
154
150
|
schedules=[self._parse_schedule(schedule) for schedule in schedules],
|
|
155
151
|
input=json.dumps(input),
|
|
156
|
-
|
|
152
|
+
parent_id=options.parent_id,
|
|
153
|
+
parent_step_run_id=options.parent_step_run_id,
|
|
154
|
+
child_index=options.child_index,
|
|
155
|
+
child_key=options.child_key,
|
|
156
|
+
additional_metadata=json.dumps(options.additional_metadata),
|
|
157
157
|
)
|
|
158
158
|
|
|
159
159
|
@tenacity_retry
|
|
@@ -302,9 +302,6 @@ class AdminClient:
|
|
|
302
302
|
) -> WorkflowRunRef:
|
|
303
303
|
request = self._create_workflow_run_request(workflow_name, input, options)
|
|
304
304
|
|
|
305
|
-
if not self.pooled_workflow_listener:
|
|
306
|
-
self.pooled_workflow_listener = PooledWorkflowRunListener(self.config)
|
|
307
|
-
|
|
308
305
|
try:
|
|
309
306
|
resp = cast(
|
|
310
307
|
v0_workflow_protos.TriggerWorkflowResponse,
|
|
@@ -320,8 +317,7 @@ class AdminClient:
|
|
|
320
317
|
|
|
321
318
|
return WorkflowRunRef(
|
|
322
319
|
workflow_run_id=resp.workflow_run_id,
|
|
323
|
-
|
|
324
|
-
workflow_run_event_listener=self.listener_client,
|
|
320
|
+
config=self.config,
|
|
325
321
|
)
|
|
326
322
|
|
|
327
323
|
## IMPORTANT: Keep this method's signature in sync with the wrapper in the OTel instrumentor
|
|
@@ -338,9 +334,6 @@ class AdminClient:
|
|
|
338
334
|
async with spawn_index_lock:
|
|
339
335
|
request = self._create_workflow_run_request(workflow_name, input, options)
|
|
340
336
|
|
|
341
|
-
if not self.pooled_workflow_listener:
|
|
342
|
-
self.pooled_workflow_listener = PooledWorkflowRunListener(self.config)
|
|
343
|
-
|
|
344
337
|
try:
|
|
345
338
|
resp = cast(
|
|
346
339
|
v0_workflow_protos.TriggerWorkflowResponse,
|
|
@@ -357,8 +350,7 @@ class AdminClient:
|
|
|
357
350
|
|
|
358
351
|
return WorkflowRunRef(
|
|
359
352
|
workflow_run_id=resp.workflow_run_id,
|
|
360
|
-
|
|
361
|
-
workflow_run_event_listener=self.listener_client,
|
|
353
|
+
config=self.config,
|
|
362
354
|
)
|
|
363
355
|
|
|
364
356
|
## IMPORTANT: Keep this method's signature in sync with the wrapper in the OTel instrumentor
|
|
@@ -367,9 +359,6 @@ class AdminClient:
|
|
|
367
359
|
self,
|
|
368
360
|
workflows: list[WorkflowRunTriggerConfig],
|
|
369
361
|
) -> list[WorkflowRunRef]:
|
|
370
|
-
if not self.pooled_workflow_listener:
|
|
371
|
-
self.pooled_workflow_listener = PooledWorkflowRunListener(self.config)
|
|
372
|
-
|
|
373
362
|
bulk_request = v0_workflow_protos.BulkTriggerWorkflowRequest(
|
|
374
363
|
workflows=[
|
|
375
364
|
self._create_workflow_run_request(
|
|
@@ -390,8 +379,7 @@ class AdminClient:
|
|
|
390
379
|
return [
|
|
391
380
|
WorkflowRunRef(
|
|
392
381
|
workflow_run_id=workflow_run_id,
|
|
393
|
-
|
|
394
|
-
workflow_run_event_listener=self.listener_client,
|
|
382
|
+
config=self.config,
|
|
395
383
|
)
|
|
396
384
|
for workflow_run_id in resp.workflow_run_ids
|
|
397
385
|
]
|
|
@@ -404,9 +392,6 @@ class AdminClient:
|
|
|
404
392
|
## IMPORTANT: The `pooled_workflow_listener` must be created 1) lazily, and not at `init` time, and 2) on the
|
|
405
393
|
## main thread. If 1) is not followed, you'll get an error about something being attached to the wrong event
|
|
406
394
|
## loop. If 2) is not followed, you'll get an error about the event loop not being set up.
|
|
407
|
-
if not self.pooled_workflow_listener:
|
|
408
|
-
self.pooled_workflow_listener = PooledWorkflowRunListener(self.config)
|
|
409
|
-
|
|
410
395
|
async with spawn_index_lock:
|
|
411
396
|
bulk_request = v0_workflow_protos.BulkTriggerWorkflowRequest(
|
|
412
397
|
workflows=[
|
|
@@ -428,18 +413,13 @@ class AdminClient:
|
|
|
428
413
|
return [
|
|
429
414
|
WorkflowRunRef(
|
|
430
415
|
workflow_run_id=workflow_run_id,
|
|
431
|
-
|
|
432
|
-
workflow_run_event_listener=self.listener_client,
|
|
416
|
+
config=self.config,
|
|
433
417
|
)
|
|
434
418
|
for workflow_run_id in resp.workflow_run_ids
|
|
435
419
|
]
|
|
436
420
|
|
|
437
421
|
def get_workflow_run(self, workflow_run_id: str) -> WorkflowRunRef:
|
|
438
|
-
if not self.pooled_workflow_listener:
|
|
439
|
-
self.pooled_workflow_listener = PooledWorkflowRunListener(self.config)
|
|
440
|
-
|
|
441
422
|
return WorkflowRunRef(
|
|
442
423
|
workflow_run_id=workflow_run_id,
|
|
443
|
-
|
|
444
|
-
workflow_run_event_listener=self.listener_client,
|
|
424
|
+
config=self.config,
|
|
445
425
|
)
|
|
@@ -152,7 +152,7 @@ class ActionListener:
|
|
|
152
152
|
self.config = config
|
|
153
153
|
self.worker_id = worker_id
|
|
154
154
|
|
|
155
|
-
self.aio_client = DispatcherStub(new_conn(self.config, True))
|
|
155
|
+
self.aio_client = DispatcherStub(new_conn(self.config, True))
|
|
156
156
|
self.token = self.config.token
|
|
157
157
|
|
|
158
158
|
self.retries = 0
|
|
@@ -232,14 +232,8 @@ class ActionListener:
|
|
|
232
232
|
if self.heartbeat_task is not None:
|
|
233
233
|
return
|
|
234
234
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
except RuntimeError as e:
|
|
238
|
-
if str(e).startswith("There is no current event loop in thread"):
|
|
239
|
-
loop = asyncio.new_event_loop()
|
|
240
|
-
asyncio.set_event_loop(loop)
|
|
241
|
-
else:
|
|
242
|
-
raise e
|
|
235
|
+
loop = asyncio.get_event_loop()
|
|
236
|
+
|
|
243
237
|
self.heartbeat_task = loop.create_task(self.heartbeat())
|
|
244
238
|
|
|
245
239
|
def __aiter__(self) -> AsyncGenerator[Action | None, None]:
|
|
@@ -298,7 +292,9 @@ class ActionListener:
|
|
|
298
292
|
)
|
|
299
293
|
)
|
|
300
294
|
except (ValueError, json.JSONDecodeError) as e:
|
|
301
|
-
|
|
295
|
+
logger.error(f"Error decoding payload: {e}")
|
|
296
|
+
|
|
297
|
+
action_payload = ActionPayload()
|
|
302
298
|
|
|
303
299
|
action = Action(
|
|
304
300
|
tenant_id=assigned_action.tenantId,
|
|
@@ -384,7 +380,7 @@ class ActionListener:
|
|
|
384
380
|
f"action listener connection interrupted, retrying... ({self.retries}/{DEFAULT_ACTION_LISTENER_RETRY_COUNT})"
|
|
385
381
|
)
|
|
386
382
|
|
|
387
|
-
self.aio_client = DispatcherStub(new_conn(self.config, True))
|
|
383
|
+
self.aio_client = DispatcherStub(new_conn(self.config, True))
|
|
388
384
|
|
|
389
385
|
if self.listen_strategy == "v2":
|
|
390
386
|
# we should await for the listener to be established before
|
|
@@ -34,20 +34,23 @@ DEFAULT_REGISTER_TIMEOUT = 30
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class DispatcherClient:
|
|
37
|
-
config: ClientConfig
|
|
38
|
-
|
|
39
37
|
def __init__(self, config: ClientConfig):
|
|
40
38
|
conn = new_conn(config, False)
|
|
41
|
-
self.client = DispatcherStub(conn)
|
|
39
|
+
self.client = DispatcherStub(conn)
|
|
42
40
|
|
|
43
|
-
aio_conn = new_conn(config, True)
|
|
44
|
-
self.aio_client = DispatcherStub(aio_conn) # type: ignore[no-untyped-call]
|
|
45
41
|
self.token = config.token
|
|
46
42
|
self.config = config
|
|
47
43
|
|
|
44
|
+
## IMPORTANT: This needs to be created lazily so we don't require
|
|
45
|
+
## an event loop to instantiate the client.
|
|
46
|
+
self.aio_client: DispatcherStub | None = None
|
|
47
|
+
|
|
48
48
|
async def get_action_listener(
|
|
49
49
|
self, req: GetActionListenerRequest
|
|
50
50
|
) -> ActionListener:
|
|
51
|
+
if not self.aio_client:
|
|
52
|
+
aio_conn = new_conn(self.config, True)
|
|
53
|
+
self.aio_client = DispatcherStub(aio_conn)
|
|
51
54
|
|
|
52
55
|
# Override labels with the preset labels
|
|
53
56
|
preset_labels = self.config.worker_preset_labels
|
|
@@ -95,6 +98,10 @@ class DispatcherClient:
|
|
|
95
98
|
async def _try_send_step_action_event(
|
|
96
99
|
self, action: Action, event_type: StepActionEventType, payload: str
|
|
97
100
|
) -> grpc.aio.UnaryUnaryCall[StepActionEvent, ActionEventResponse]:
|
|
101
|
+
if not self.aio_client:
|
|
102
|
+
aio_conn = new_conn(self.config, True)
|
|
103
|
+
self.aio_client = DispatcherStub(aio_conn)
|
|
104
|
+
|
|
98
105
|
event_timestamp = Timestamp()
|
|
99
106
|
event_timestamp.GetCurrentTime()
|
|
100
107
|
|
|
@@ -122,6 +129,10 @@ class DispatcherClient:
|
|
|
122
129
|
async def send_group_key_action_event(
|
|
123
130
|
self, action: Action, event_type: GroupKeyActionEventType, payload: str
|
|
124
131
|
) -> grpc.aio.UnaryUnaryCall[GroupKeyActionEvent, ActionEventResponse]:
|
|
132
|
+
if not self.aio_client:
|
|
133
|
+
aio_conn = new_conn(self.config, True)
|
|
134
|
+
self.aio_client = DispatcherStub(aio_conn)
|
|
135
|
+
|
|
125
136
|
event_timestamp = Timestamp()
|
|
126
137
|
event_timestamp.GetCurrentTime()
|
|
127
138
|
|
|
@@ -191,6 +202,10 @@ class DispatcherClient:
|
|
|
191
202
|
worker_id: str | None,
|
|
192
203
|
labels: dict[str, str | int],
|
|
193
204
|
) -> None:
|
|
205
|
+
if not self.aio_client:
|
|
206
|
+
aio_conn = new_conn(self.config, True)
|
|
207
|
+
self.aio_client = DispatcherStub(aio_conn)
|
|
208
|
+
|
|
194
209
|
worker_labels = {}
|
|
195
210
|
|
|
196
211
|
for key, value in labels.items():
|
|
@@ -84,14 +84,6 @@ class RegisterDurableEventRequest(BaseModel):
|
|
|
84
84
|
|
|
85
85
|
class DurableEventListener:
|
|
86
86
|
def __init__(self, config: ClientConfig):
|
|
87
|
-
try:
|
|
88
|
-
asyncio.get_running_loop()
|
|
89
|
-
except RuntimeError:
|
|
90
|
-
loop = asyncio.new_event_loop()
|
|
91
|
-
asyncio.set_event_loop(loop)
|
|
92
|
-
|
|
93
|
-
conn = new_conn(config, True)
|
|
94
|
-
self.client = V1DispatcherStub(conn) # type: ignore[no-untyped-call]
|
|
95
87
|
self.token = config.token
|
|
96
88
|
self.config = config
|
|
97
89
|
|
|
@@ -129,11 +121,14 @@ class DurableEventListener:
|
|
|
129
121
|
self.interrupt.set()
|
|
130
122
|
|
|
131
123
|
async def _init_producer(self) -> None:
|
|
124
|
+
conn = new_conn(self.config, True)
|
|
125
|
+
client = V1DispatcherStub(conn)
|
|
126
|
+
|
|
132
127
|
try:
|
|
133
128
|
if not self.listener:
|
|
134
129
|
while True:
|
|
135
130
|
try:
|
|
136
|
-
self.listener = await self._retry_subscribe()
|
|
131
|
+
self.listener = await self._retry_subscribe(client)
|
|
137
132
|
|
|
138
133
|
logger.debug("Workflow run listener connected.")
|
|
139
134
|
|
|
@@ -282,6 +277,7 @@ class DurableEventListener:
|
|
|
282
277
|
|
|
283
278
|
async def _retry_subscribe(
|
|
284
279
|
self,
|
|
280
|
+
client: V1DispatcherStub,
|
|
285
281
|
) -> grpc.aio.UnaryStreamCall[ListenForDurableEventRequest, DurableEvent]:
|
|
286
282
|
retries = 0
|
|
287
283
|
|
|
@@ -298,8 +294,8 @@ class DurableEventListener:
|
|
|
298
294
|
grpc.aio.UnaryStreamCall[
|
|
299
295
|
ListenForDurableEventRequest, DurableEvent
|
|
300
296
|
],
|
|
301
|
-
|
|
302
|
-
self._request(),
|
|
297
|
+
client.ListenForDurableEvent(
|
|
298
|
+
self._request(), # type: ignore[arg-type]
|
|
303
299
|
metadata=get_metadata(self.token),
|
|
304
300
|
),
|
|
305
301
|
)
|
|
@@ -315,7 +311,10 @@ class DurableEventListener:
|
|
|
315
311
|
def register_durable_event(
|
|
316
312
|
self, request: RegisterDurableEventRequest
|
|
317
313
|
) -> Literal[True]:
|
|
318
|
-
self.
|
|
314
|
+
conn = new_conn(self.config, True)
|
|
315
|
+
client = V1DispatcherStub(conn)
|
|
316
|
+
|
|
317
|
+
client.RegisterDurableEvent(
|
|
319
318
|
request.to_proto(),
|
|
320
319
|
timeout=5,
|
|
321
320
|
metadata=get_metadata(self.token),
|
hatchet_sdk/clients/events.py
CHANGED
|
@@ -3,15 +3,16 @@ import datetime
|
|
|
3
3
|
import json
|
|
4
4
|
from typing import List, cast
|
|
5
5
|
|
|
6
|
-
import grpc
|
|
7
6
|
from google.protobuf import timestamp_pb2
|
|
8
7
|
from pydantic import BaseModel, Field
|
|
9
8
|
|
|
10
9
|
from hatchet_sdk.clients.rest.tenacity_utils import tenacity_retry
|
|
11
10
|
from hatchet_sdk.config import ClientConfig
|
|
11
|
+
from hatchet_sdk.connection import new_conn
|
|
12
12
|
from hatchet_sdk.contracts.events_pb2 import (
|
|
13
13
|
BulkPushEventRequest,
|
|
14
14
|
Event,
|
|
15
|
+
Events,
|
|
15
16
|
PushEventRequest,
|
|
16
17
|
PutLogRequest,
|
|
17
18
|
PutStreamEventRequest,
|
|
@@ -21,13 +22,6 @@ from hatchet_sdk.metadata import get_metadata
|
|
|
21
22
|
from hatchet_sdk.utils.typing import JSONSerializableMapping
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
def new_event(conn: grpc.Channel, config: ClientConfig) -> "EventClient":
|
|
25
|
-
return EventClient(
|
|
26
|
-
client=EventsServiceStub(conn), # type: ignore[no-untyped-call]
|
|
27
|
-
config=config,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
25
|
def proto_timestamp_now() -> timestamp_pb2.Timestamp:
|
|
32
26
|
t = datetime.datetime.now().timestamp()
|
|
33
27
|
seconds = int(t)
|
|
@@ -52,8 +46,10 @@ class BulkPushEventWithMetadata(BaseModel):
|
|
|
52
46
|
|
|
53
47
|
|
|
54
48
|
class EventClient:
|
|
55
|
-
def __init__(self,
|
|
56
|
-
|
|
49
|
+
def __init__(self, config: ClientConfig):
|
|
50
|
+
conn = new_conn(config, False)
|
|
51
|
+
self.client = EventsServiceStub(conn)
|
|
52
|
+
|
|
57
53
|
self.token = config.token
|
|
58
54
|
self.namespace = config.namespace
|
|
59
55
|
|
|
@@ -146,11 +142,11 @@ class EventClient:
|
|
|
146
142
|
]
|
|
147
143
|
)
|
|
148
144
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
145
|
+
return list(
|
|
146
|
+
cast(
|
|
147
|
+
Events,
|
|
148
|
+
self.client.BulkPush(bulk_request, metadata=get_metadata(self.token)),
|
|
149
|
+
).events
|
|
154
150
|
)
|
|
155
151
|
|
|
156
152
|
def log(self, message: str, step_run_id: str) -> None:
|
|
@@ -22,17 +22,13 @@ from typing import Any, ClassVar, Dict, List, Optional, Set
|
|
|
22
22
|
from pydantic import BaseModel, ConfigDict
|
|
23
23
|
from typing_extensions import Self
|
|
24
24
|
|
|
25
|
-
from hatchet_sdk.clients.rest.models.workflow_runs_metrics_counts import (
|
|
26
|
-
WorkflowRunsMetricsCounts,
|
|
27
|
-
)
|
|
28
|
-
|
|
29
25
|
|
|
30
26
|
class WorkflowRunsMetrics(BaseModel):
|
|
31
27
|
"""
|
|
32
28
|
WorkflowRunsMetrics
|
|
33
29
|
""" # noqa: E501
|
|
34
30
|
|
|
35
|
-
counts: Optional[
|
|
31
|
+
counts: Optional[Dict[str, Any]] = None
|
|
36
32
|
__properties: ClassVar[List[str]] = ["counts"]
|
|
37
33
|
|
|
38
34
|
model_config = ConfigDict(
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import json
|
|
3
2
|
from enum import Enum
|
|
4
|
-
from
|
|
3
|
+
from queue import Empty, Queue
|
|
4
|
+
from threading import Thread
|
|
5
|
+
from typing import Any, AsyncGenerator, Callable, Generator, Literal, TypeVar, cast
|
|
5
6
|
|
|
6
7
|
import grpc
|
|
7
8
|
from pydantic import BaseModel
|
|
@@ -56,6 +57,8 @@ workflow_run_event_type_mapping = {
|
|
|
56
57
|
ResourceEventType.RESOURCE_EVENT_TYPE_TIMED_OUT: WorkflowRunEventType.WORKFLOW_RUN_EVENT_TYPE_TIMED_OUT,
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
T = TypeVar("T")
|
|
61
|
+
|
|
59
62
|
|
|
60
63
|
class StepRunEvent(BaseModel):
|
|
61
64
|
type: StepRunEventType
|
|
@@ -65,18 +68,20 @@ class StepRunEvent(BaseModel):
|
|
|
65
68
|
class RunEventListener:
|
|
66
69
|
def __init__(
|
|
67
70
|
self,
|
|
68
|
-
|
|
69
|
-
token: str,
|
|
71
|
+
config: ClientConfig,
|
|
70
72
|
workflow_run_id: str | None = None,
|
|
71
73
|
additional_meta_kv: tuple[str, str] | None = None,
|
|
72
74
|
):
|
|
73
|
-
self.
|
|
75
|
+
self.config = config
|
|
74
76
|
self.stop_signal = False
|
|
75
|
-
self.token = token
|
|
76
77
|
|
|
77
78
|
self.workflow_run_id = workflow_run_id
|
|
78
79
|
self.additional_meta_kv = additional_meta_kv
|
|
79
80
|
|
|
81
|
+
## IMPORTANT: This needs to be created lazily so we don't require
|
|
82
|
+
## an event loop to instantiate the client.
|
|
83
|
+
self.client: DispatcherStub | None = None
|
|
84
|
+
|
|
80
85
|
def abort(self) -> None:
|
|
81
86
|
self.stop_signal = True
|
|
82
87
|
|
|
@@ -86,27 +91,46 @@ class RunEventListener:
|
|
|
86
91
|
async def __anext__(self) -> StepRunEvent:
|
|
87
92
|
return await self._generator().__anext__()
|
|
88
93
|
|
|
89
|
-
def
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
def async_to_sync_thread(
|
|
95
|
+
self, async_iter: AsyncGenerator[T, None]
|
|
96
|
+
) -> Generator[T, None, None]:
|
|
97
|
+
q = Queue[T | Literal["DONE"]]()
|
|
98
|
+
done_sentinel: Literal["DONE"] = "DONE"
|
|
99
|
+
|
|
100
|
+
def runner() -> None:
|
|
101
|
+
loop = asyncio.new_event_loop()
|
|
102
|
+
asyncio.set_event_loop(loop)
|
|
103
|
+
|
|
104
|
+
async def consume() -> None:
|
|
105
|
+
try:
|
|
106
|
+
async for item in async_iter:
|
|
107
|
+
q.put(item)
|
|
108
|
+
finally:
|
|
109
|
+
q.put(done_sentinel)
|
|
98
110
|
|
|
99
|
-
|
|
111
|
+
try:
|
|
112
|
+
loop.run_until_complete(consume())
|
|
113
|
+
finally:
|
|
114
|
+
loop.stop()
|
|
115
|
+
loop.close()
|
|
116
|
+
|
|
117
|
+
thread = Thread(target=runner)
|
|
118
|
+
thread.start()
|
|
100
119
|
|
|
101
120
|
while True:
|
|
102
121
|
try:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
except
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
item = q.get(timeout=1)
|
|
123
|
+
if item == "DONE":
|
|
124
|
+
break
|
|
125
|
+
yield item
|
|
126
|
+
except Empty:
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
thread.join()
|
|
130
|
+
|
|
131
|
+
def __iter__(self) -> Generator[StepRunEvent, None, None]:
|
|
132
|
+
for item in self.async_to_sync_thread(self.__aiter__()):
|
|
133
|
+
yield item
|
|
110
134
|
|
|
111
135
|
async def _generator(self) -> AsyncGenerator[StepRunEvent, None]:
|
|
112
136
|
while True:
|
|
@@ -128,18 +152,10 @@ class RunEventListener:
|
|
|
128
152
|
raise Exception(
|
|
129
153
|
f"Unknown event type: {workflow_event.eventType}"
|
|
130
154
|
)
|
|
131
|
-
payload = None
|
|
132
|
-
|
|
133
|
-
try:
|
|
134
|
-
if workflow_event.eventPayload:
|
|
135
|
-
payload = json.loads(workflow_event.eventPayload)
|
|
136
|
-
except Exception:
|
|
137
|
-
payload = workflow_event.eventPayload
|
|
138
|
-
pass
|
|
139
155
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
156
|
+
yield StepRunEvent(
|
|
157
|
+
type=eventType, payload=workflow_event.eventPayload
|
|
158
|
+
)
|
|
143
159
|
elif workflow_event.resourceType == RESOURCE_TYPE_WORKFLOW_RUN:
|
|
144
160
|
if workflow_event.eventType in step_run_event_type_mapping:
|
|
145
161
|
workflowRunEventType = step_run_event_type_mapping[
|
|
@@ -150,17 +166,10 @@ class RunEventListener:
|
|
|
150
166
|
f"Unknown event type: {workflow_event.eventType}"
|
|
151
167
|
)
|
|
152
168
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
payload = json.loads(workflow_event.eventPayload)
|
|
158
|
-
except Exception:
|
|
159
|
-
pass
|
|
160
|
-
|
|
161
|
-
assert isinstance(payload, str)
|
|
162
|
-
|
|
163
|
-
yield StepRunEvent(type=workflowRunEventType, payload=payload)
|
|
169
|
+
yield StepRunEvent(
|
|
170
|
+
type=workflowRunEventType,
|
|
171
|
+
payload=workflow_event.eventPayload,
|
|
172
|
+
)
|
|
164
173
|
|
|
165
174
|
if workflow_event.hangup:
|
|
166
175
|
listener = None
|
|
@@ -188,6 +197,10 @@ class RunEventListener:
|
|
|
188
197
|
async def retry_subscribe(self) -> AsyncGenerator[WorkflowEvent, None]:
|
|
189
198
|
retries = 0
|
|
190
199
|
|
|
200
|
+
if self.client is None:
|
|
201
|
+
aio_conn = new_conn(self.config, True)
|
|
202
|
+
self.client = DispatcherStub(aio_conn)
|
|
203
|
+
|
|
191
204
|
while retries < DEFAULT_ACTION_LISTENER_RETRY_COUNT:
|
|
192
205
|
try:
|
|
193
206
|
if retries > 0:
|
|
@@ -200,7 +213,7 @@ class RunEventListener:
|
|
|
200
213
|
SubscribeToWorkflowEventsRequest(
|
|
201
214
|
workflowRunId=self.workflow_run_id,
|
|
202
215
|
),
|
|
203
|
-
metadata=get_metadata(self.token),
|
|
216
|
+
metadata=get_metadata(self.config.token),
|
|
204
217
|
),
|
|
205
218
|
)
|
|
206
219
|
elif self.additional_meta_kv is not None:
|
|
@@ -211,7 +224,7 @@ class RunEventListener:
|
|
|
211
224
|
additionalMetaKey=self.additional_meta_kv[0],
|
|
212
225
|
additionalMetaValue=self.additional_meta_kv[1],
|
|
213
226
|
),
|
|
214
|
-
metadata=get_metadata(self.token),
|
|
227
|
+
metadata=get_metadata(self.config.token),
|
|
215
228
|
),
|
|
216
229
|
)
|
|
217
230
|
else:
|
|
@@ -228,33 +241,16 @@ class RunEventListener:
|
|
|
228
241
|
|
|
229
242
|
class RunEventListenerClient:
|
|
230
243
|
def __init__(self, config: ClientConfig):
|
|
231
|
-
self.token = config.token
|
|
232
244
|
self.config = config
|
|
233
|
-
self.client: DispatcherStub | None = None
|
|
234
245
|
|
|
235
246
|
def stream_by_run_id(self, workflow_run_id: str) -> RunEventListener:
|
|
236
247
|
return self.stream(workflow_run_id)
|
|
237
248
|
|
|
238
249
|
def stream(self, workflow_run_id: str) -> RunEventListener:
|
|
239
|
-
|
|
240
|
-
workflow_run_id = str(workflow_run_id)
|
|
241
|
-
|
|
242
|
-
if not self.client:
|
|
243
|
-
aio_conn = new_conn(self.config, True)
|
|
244
|
-
self.client = DispatcherStub(aio_conn) # type: ignore[no-untyped-call]
|
|
245
|
-
|
|
246
|
-
return RunEventListener(
|
|
247
|
-
client=self.client, token=self.token, workflow_run_id=workflow_run_id
|
|
248
|
-
)
|
|
250
|
+
return RunEventListener(config=self.config, workflow_run_id=workflow_run_id)
|
|
249
251
|
|
|
250
252
|
def stream_by_additional_metadata(self, key: str, value: str) -> RunEventListener:
|
|
251
|
-
|
|
252
|
-
aio_conn = new_conn(self.config, True)
|
|
253
|
-
self.client = DispatcherStub(aio_conn) # type: ignore[no-untyped-call]
|
|
254
|
-
|
|
255
|
-
return RunEventListener(
|
|
256
|
-
client=self.client, token=self.token, additional_meta_kv=(key, value)
|
|
257
|
-
)
|
|
253
|
+
return RunEventListener(config=self.config, additional_meta_kv=(key, value))
|
|
258
254
|
|
|
259
255
|
async def on(
|
|
260
256
|
self, workflow_run_id: str, handler: Callable[[StepRunEvent], Any] | None = None
|