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.

Files changed (35) hide show
  1. hatchet_sdk/client.py +3 -16
  2. hatchet_sdk/clients/admin.py +13 -33
  3. hatchet_sdk/clients/dispatcher/action_listener.py +7 -11
  4. hatchet_sdk/clients/dispatcher/dispatcher.py +20 -5
  5. hatchet_sdk/clients/durable_event_listener.py +11 -12
  6. hatchet_sdk/clients/events.py +11 -15
  7. hatchet_sdk/clients/rest/models/tenant_resource.py +2 -0
  8. hatchet_sdk/clients/rest/models/workflow_runs_metrics.py +1 -5
  9. hatchet_sdk/clients/run_event_listener.py +62 -66
  10. hatchet_sdk/clients/v1/api_client.py +1 -38
  11. hatchet_sdk/clients/workflow_listener.py +9 -10
  12. hatchet_sdk/context/context.py +9 -0
  13. hatchet_sdk/contracts/dispatcher_pb2_grpc.py +1 -1
  14. hatchet_sdk/contracts/events_pb2_grpc.py +1 -1
  15. hatchet_sdk/contracts/v1/dispatcher_pb2_grpc.py +1 -1
  16. hatchet_sdk/contracts/v1/workflows_pb2_grpc.py +1 -1
  17. hatchet_sdk/contracts/workflows_pb2_grpc.py +1 -1
  18. hatchet_sdk/features/cron.py +5 -4
  19. hatchet_sdk/features/logs.py +2 -1
  20. hatchet_sdk/features/metrics.py +4 -3
  21. hatchet_sdk/features/rate_limits.py +1 -1
  22. hatchet_sdk/features/runs.py +8 -7
  23. hatchet_sdk/features/scheduled.py +5 -4
  24. hatchet_sdk/features/workers.py +4 -3
  25. hatchet_sdk/features/workflows.py +4 -3
  26. hatchet_sdk/metadata.py +2 -2
  27. hatchet_sdk/runnables/standalone.py +3 -18
  28. hatchet_sdk/utils/aio.py +43 -0
  29. hatchet_sdk/worker/runner/run_loop_manager.py +1 -1
  30. hatchet_sdk/workflow_run.py +7 -20
  31. {hatchet_sdk-1.0.2.dist-info → hatchet_sdk-1.1.0.dist-info}/METADATA +1 -1
  32. {hatchet_sdk-1.0.2.dist-info → hatchet_sdk-1.1.0.dist-info}/RECORD +34 -34
  33. {hatchet_sdk-1.0.2.dist-info → hatchet_sdk-1.1.0.dist-info}/entry_points.txt +1 -0
  34. hatchet_sdk/utils/aio_utils.py +0 -18
  35. {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, new_event
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 new_event(conn, config)
30
+ self.event = event_client or EventClient(config)
44
31
  self.listener = RunEventListenerClient(config)
45
32
  self.workflow_listener = workflow_listener
46
- self.logInterceptor = config.logger
33
+ self.log_interceptor = config.logger
47
34
  self.debug = debug
48
35
 
49
36
  self.cron = CronClient(self.config)
@@ -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) # type: ignore[no-untyped-call]
67
- self.v0_client = WorkflowServiceStub(conn) # type: ignore[no-untyped-call]
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
- **options.model_dump(),
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
- workflow_listener=self.pooled_workflow_listener,
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
- workflow_listener=self.pooled_workflow_listener,
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
- workflow_listener=self.pooled_workflow_listener,
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
- workflow_listener=self.pooled_workflow_listener,
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
- workflow_listener=self.pooled_workflow_listener,
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)) # type: ignore[no-untyped-call]
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
- try:
236
- loop = asyncio.get_event_loop()
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
- raise ValueError(f"Error decoding payload: {e}")
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)) # type: ignore[no-untyped-call]
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) # type: ignore[no-untyped-call]
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
- self.client.ListenForDurableEvent(
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.client.RegisterDurableEvent(
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),
@@ -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, client: EventsServiceStub, config: ClientConfig):
56
- self.client = client
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
- response = self.client.BulkPush(bulk_request, metadata=get_metadata(self.token))
150
-
151
- return cast(
152
- list[Event],
153
- response.events,
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:
@@ -29,8 +29,10 @@ class TenantResource(str, Enum):
29
29
  allowed enum values
30
30
  """
31
31
  WORKER = "WORKER"
32
+ WORKER_SLOT = "WORKER_SLOT"
32
33
  EVENT = "EVENT"
33
34
  WORKFLOW_RUN = "WORKFLOW_RUN"
35
+ TASK_RUN = "TASK_RUN"
34
36
  CRON = "CRON"
35
37
  SCHEDULE = "SCHEDULE"
36
38
 
@@ -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[WorkflowRunsMetricsCounts] = None
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 typing import Any, AsyncGenerator, Callable, Generator, cast
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
- client: DispatcherStub,
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.client = client
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 __iter__(self) -> Generator[StepRunEvent, None, None]:
90
- try:
91
- loop = asyncio.get_event_loop()
92
- except RuntimeError as e:
93
- if str(e).startswith("There is no current event loop in thread"):
94
- loop = asyncio.new_event_loop()
95
- asyncio.set_event_loop(loop)
96
- else:
97
- raise e
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
- async_iter = self.__aiter__()
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
- future = asyncio.ensure_future(async_iter.__anext__())
104
- yield loop.run_until_complete(future)
105
- except StopAsyncIteration:
106
- break
107
- except Exception as e:
108
- print(f"Error in synchronous iterator: {e}")
109
- break
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
- assert isinstance(payload, str)
141
-
142
- yield StepRunEvent(type=eventType, payload=payload)
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
- payload = None
154
-
155
- try:
156
- if workflow_event.eventPayload:
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
- if not isinstance(workflow_run_id, str) and hasattr(workflow_run_id, "__str__"):
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
- if not self.client:
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