hatchet-sdk 1.0.0__py3-none-any.whl → 1.0.0a1__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 (65) hide show
  1. hatchet_sdk/__init__.py +27 -16
  2. hatchet_sdk/client.py +13 -63
  3. hatchet_sdk/clients/admin.py +203 -124
  4. hatchet_sdk/clients/dispatcher/action_listener.py +42 -42
  5. hatchet_sdk/clients/dispatcher/dispatcher.py +18 -16
  6. hatchet_sdk/clients/durable_event_listener.py +327 -0
  7. hatchet_sdk/clients/rest/__init__.py +12 -1
  8. hatchet_sdk/clients/rest/api/log_api.py +258 -0
  9. hatchet_sdk/clients/rest/api/task_api.py +32 -6
  10. hatchet_sdk/clients/rest/api/workflow_runs_api.py +626 -0
  11. hatchet_sdk/clients/rest/models/__init__.py +12 -1
  12. hatchet_sdk/clients/rest/models/v1_log_line.py +94 -0
  13. hatchet_sdk/clients/rest/models/v1_log_line_level.py +39 -0
  14. hatchet_sdk/clients/rest/models/v1_log_line_list.py +110 -0
  15. hatchet_sdk/clients/rest/models/v1_task_summary.py +80 -64
  16. hatchet_sdk/clients/rest/models/v1_trigger_workflow_run_request.py +95 -0
  17. hatchet_sdk/clients/rest/models/v1_workflow_run_display_name.py +98 -0
  18. hatchet_sdk/clients/rest/models/v1_workflow_run_display_name_list.py +114 -0
  19. hatchet_sdk/clients/rest/models/workflow_run_shape_item_for_workflow_run_details.py +9 -4
  20. hatchet_sdk/clients/rest_client.py +21 -0
  21. hatchet_sdk/clients/run_event_listener.py +0 -1
  22. hatchet_sdk/context/context.py +85 -147
  23. hatchet_sdk/contracts/dispatcher_pb2_grpc.py +1 -1
  24. hatchet_sdk/contracts/events_pb2.py +2 -2
  25. hatchet_sdk/contracts/events_pb2_grpc.py +1 -1
  26. hatchet_sdk/contracts/v1/dispatcher_pb2.py +36 -0
  27. hatchet_sdk/contracts/v1/dispatcher_pb2.pyi +38 -0
  28. hatchet_sdk/contracts/v1/dispatcher_pb2_grpc.py +145 -0
  29. hatchet_sdk/contracts/v1/shared/condition_pb2.py +39 -0
  30. hatchet_sdk/contracts/v1/shared/condition_pb2.pyi +72 -0
  31. hatchet_sdk/contracts/v1/shared/condition_pb2_grpc.py +29 -0
  32. hatchet_sdk/contracts/v1/workflows_pb2.py +67 -0
  33. hatchet_sdk/contracts/v1/workflows_pb2.pyi +228 -0
  34. hatchet_sdk/contracts/v1/workflows_pb2_grpc.py +234 -0
  35. hatchet_sdk/contracts/workflows_pb2_grpc.py +1 -1
  36. hatchet_sdk/features/cron.py +3 -3
  37. hatchet_sdk/features/scheduled.py +2 -2
  38. hatchet_sdk/hatchet.py +427 -151
  39. hatchet_sdk/opentelemetry/instrumentor.py +8 -13
  40. hatchet_sdk/rate_limit.py +33 -39
  41. hatchet_sdk/runnables/contextvars.py +12 -0
  42. hatchet_sdk/runnables/standalone.py +194 -0
  43. hatchet_sdk/runnables/task.py +144 -0
  44. hatchet_sdk/runnables/types.py +138 -0
  45. hatchet_sdk/runnables/workflow.py +764 -0
  46. hatchet_sdk/utils/aio_utils.py +0 -79
  47. hatchet_sdk/utils/proto_enums.py +0 -7
  48. hatchet_sdk/utils/timedelta_to_expression.py +23 -0
  49. hatchet_sdk/utils/typing.py +2 -2
  50. hatchet_sdk/v0/clients/rest_client.py +9 -0
  51. hatchet_sdk/v0/worker/action_listener_process.py +18 -2
  52. hatchet_sdk/waits.py +120 -0
  53. hatchet_sdk/worker/action_listener_process.py +64 -30
  54. hatchet_sdk/worker/runner/run_loop_manager.py +35 -25
  55. hatchet_sdk/worker/runner/runner.py +72 -49
  56. hatchet_sdk/worker/runner/utils/capture_logs.py +3 -11
  57. hatchet_sdk/worker/worker.py +155 -118
  58. hatchet_sdk/workflow_run.py +4 -5
  59. {hatchet_sdk-1.0.0.dist-info → hatchet_sdk-1.0.0a1.dist-info}/METADATA +1 -2
  60. {hatchet_sdk-1.0.0.dist-info → hatchet_sdk-1.0.0a1.dist-info}/RECORD +62 -42
  61. {hatchet_sdk-1.0.0.dist-info → hatchet_sdk-1.0.0a1.dist-info}/entry_points.txt +2 -0
  62. hatchet_sdk/semver.py +0 -30
  63. hatchet_sdk/worker/runner/utils/error_with_traceback.py +0 -6
  64. hatchet_sdk/workflow.py +0 -527
  65. {hatchet_sdk-1.0.0.dist-info → hatchet_sdk-1.0.0a1.dist-info}/WHEEL +0 -0
@@ -8,18 +8,19 @@ from concurrent.futures import ThreadPoolExecutor
8
8
  from enum import Enum
9
9
  from multiprocessing import Queue
10
10
  from threading import Thread, current_thread
11
- from typing import Any, Callable, Dict, TypeVar, cast
11
+ from typing import Any, Callable, Dict, Literal, cast, overload
12
12
 
13
13
  from pydantic import BaseModel
14
14
 
15
- from hatchet_sdk.client import new_client_raw
15
+ from hatchet_sdk.client import Client
16
16
  from hatchet_sdk.clients.admin import AdminClient
17
17
  from hatchet_sdk.clients.dispatcher.action_listener import Action, ActionType
18
18
  from hatchet_sdk.clients.dispatcher.dispatcher import DispatcherClient
19
+ from hatchet_sdk.clients.durable_event_listener import DurableEventListener
19
20
  from hatchet_sdk.clients.run_event_listener import RunEventListenerClient
20
21
  from hatchet_sdk.clients.workflow_listener import PooledWorkflowRunListener
21
22
  from hatchet_sdk.config import ClientConfig
22
- from hatchet_sdk.context.context import Context
23
+ from hatchet_sdk.context.context import Context, DurableContext
23
24
  from hatchet_sdk.context.worker_context import WorkerContext
24
25
  from hatchet_sdk.contracts.dispatcher_pb2 import (
25
26
  GROUP_KEY_EVENT_TYPE_COMPLETED,
@@ -30,12 +31,18 @@ from hatchet_sdk.contracts.dispatcher_pb2 import (
30
31
  STEP_EVENT_TYPE_STARTED,
31
32
  )
32
33
  from hatchet_sdk.logger import logger
34
+ from hatchet_sdk.runnables.contextvars import (
35
+ ctx_step_run_id,
36
+ ctx_worker_id,
37
+ ctx_workflow_run_id,
38
+ spawn_index_lock,
39
+ workflow_spawn_indices,
40
+ )
41
+ from hatchet_sdk.runnables.task import Task
42
+ from hatchet_sdk.runnables.types import R, TWorkflowInput
33
43
  from hatchet_sdk.utils.typing import WorkflowValidator
34
44
  from hatchet_sdk.worker.action_listener_process import ActionEvent
35
- from hatchet_sdk.worker.runner.utils.capture_logs import copy_context_vars, sr, wr
36
- from hatchet_sdk.workflow import Step
37
-
38
- T = TypeVar("T")
45
+ from hatchet_sdk.worker.runner.utils.capture_logs import copy_context_vars
39
46
 
40
47
 
41
48
  class WorkerStatus(Enum):
@@ -49,28 +56,28 @@ class Runner:
49
56
  def __init__(
50
57
  self,
51
58
  name: str,
52
- event_queue: "Queue[Any]",
53
- max_runs: int | None = None,
59
+ event_queue: "Queue[ActionEvent]",
60
+ config: ClientConfig,
61
+ slots: int | None = None,
54
62
  handle_kill: bool = True,
55
- action_registry: dict[str, Step[T]] = {},
63
+ action_registry: dict[str, Task[TWorkflowInput, R]] = {},
56
64
  validator_registry: dict[str, WorkflowValidator] = {},
57
- config: ClientConfig = ClientConfig(),
58
65
  labels: dict[str, str | int] = {},
59
66
  ):
60
67
  # We store the config so we can dynamically create clients for the dispatcher client.
61
68
  self.config = config
62
- self.client = new_client_raw(config)
69
+ self.client = Client(config)
63
70
  self.name = self.client.config.namespace + name
64
- self.max_runs = max_runs
71
+ self.slots = slots
65
72
  self.tasks: dict[str, asyncio.Task[Any]] = {} # Store run ids and futures
66
73
  self.contexts: dict[str, Context] = {} # Store run ids and contexts
67
- self.action_registry: dict[str, Step[T]] = action_registry
74
+ self.action_registry = action_registry
68
75
  self.validator_registry = validator_registry
69
76
 
70
77
  self.event_queue = event_queue
71
78
 
72
79
  # The thread pool is used for synchronous functions which need to run concurrently
73
- self.thread_pool = ThreadPoolExecutor(max_workers=max_runs)
80
+ self.thread_pool = ThreadPoolExecutor(max_workers=slots)
74
81
  self.threads: Dict[str, Thread] = {} # Store run ids and threads
75
82
 
76
83
  self.killing = False
@@ -82,9 +89,10 @@ class Runner:
82
89
  self.admin_client = AdminClient(self.config)
83
90
  self.workflow_run_event_listener = RunEventListenerClient(self.config)
84
91
  self.client.workflow_listener = PooledWorkflowRunListener(self.config)
92
+ self.durable_event_listener = DurableEventListener(self.config)
85
93
 
86
94
  self.worker_context = WorkerContext(
87
- labels=labels, client=new_client_raw(config).dispatcher
95
+ labels=labels, client=Client(config=config).dispatcher
88
96
  )
89
97
 
90
98
  def create_workflow_run_url(self, action: Action) -> str:
@@ -130,7 +138,7 @@ class Runner:
130
138
  ActionEvent(
131
139
  action=action,
132
140
  type=STEP_EVENT_TYPE_FAILED,
133
- payload=str(errorWithTraceback(f"{e}", e)),
141
+ payload=str(pretty_format_exception(f"{e}", e)),
134
142
  )
135
143
  )
136
144
 
@@ -172,7 +180,7 @@ class Runner:
172
180
  ActionEvent(
173
181
  action=action,
174
182
  type=GROUP_KEY_EVENT_TYPE_FAILED,
175
- payload=str(errorWithTraceback(f"{e}", e)),
183
+ payload=str(pretty_format_exception(f"{e}", e)),
176
184
  )
177
185
  )
178
186
 
@@ -195,7 +203,9 @@ class Runner:
195
203
 
196
204
  return inner_callback
197
205
 
198
- def thread_action_func(self, context: Context, step: Step[T], action: Action) -> T:
206
+ def thread_action_func(
207
+ self, ctx: Context, task: Task[TWorkflowInput, R], action: Action
208
+ ) -> R:
199
209
  if action.step_run_id is not None and action.step_run_id != "":
200
210
  self.threads[action.step_run_id] = current_thread()
201
211
  elif (
@@ -204,22 +214,23 @@ class Runner:
204
214
  ):
205
215
  self.threads[action.get_group_key_run_id] = current_thread()
206
216
 
207
- return step.call(context)
217
+ return task.call(ctx)
208
218
 
209
219
  # We wrap all actions in an async func
210
220
  async def async_wrapped_action_func(
211
221
  self,
212
- context: Context,
213
- step: Step[T],
222
+ ctx: Context,
223
+ task: Task[TWorkflowInput, R],
214
224
  action: Action,
215
225
  run_id: str,
216
- ) -> T:
217
- wr.set(context.workflow_run_id)
218
- sr.set(context.step_run_id)
226
+ ) -> R:
227
+ ctx_step_run_id.set(action.step_run_id)
228
+ ctx_workflow_run_id.set(action.workflow_run_id)
229
+ ctx_worker_id.set(action.worker_id)
219
230
 
220
231
  try:
221
- if step.is_async_function:
222
- return await step.aio_call(context)
232
+ if task.is_async_function:
233
+ return await task.aio_call(ctx)
223
234
  else:
224
235
  pfunc = functools.partial(
225
236
  # we must copy the context vars to the new thread, as only asyncio natively supports
@@ -227,8 +238,8 @@ class Runner:
227
238
  copy_context_vars,
228
239
  contextvars.copy_context().items(),
229
240
  self.thread_action_func,
230
- context,
231
- step,
241
+ ctx,
242
+ task,
232
243
  action,
233
244
  )
234
245
 
@@ -236,7 +247,7 @@ class Runner:
236
247
  return await loop.run_in_executor(self.thread_pool, pfunc)
237
248
  except Exception as e:
238
249
  logger.error(
239
- errorWithTraceback(
250
+ pretty_format_exception(
240
251
  f"exception raised in action ({action.action_id}, retry={action.retry_count}):\n{e}",
241
252
  e,
242
253
  )
@@ -255,14 +266,29 @@ class Runner:
255
266
  if run_id in self.contexts:
256
267
  del self.contexts[run_id]
257
268
 
258
- def create_context(self, action: Action) -> Context:
259
- return Context(
269
+ @overload
270
+ def create_context(
271
+ self, action: Action, is_durable: Literal[True] = True
272
+ ) -> DurableContext: ...
273
+
274
+ @overload
275
+ def create_context(
276
+ self, action: Action, is_durable: Literal[False] = False
277
+ ) -> Context: ...
278
+
279
+ def create_context(
280
+ self, action: Action, is_durable: bool = True
281
+ ) -> Context | DurableContext:
282
+ constructor = DurableContext if is_durable else Context
283
+
284
+ return constructor(
260
285
  action,
261
286
  self.dispatcher_client,
262
287
  self.admin_client,
263
288
  self.client.event,
264
289
  self.client.rest,
265
290
  self.client.workflow_listener,
291
+ self.durable_event_listener,
266
292
  self.workflow_run_event_listener,
267
293
  self.worker_context,
268
294
  self.client.config.namespace,
@@ -276,11 +302,12 @@ class Runner:
276
302
  # Find the corresponding action function from the registry
277
303
  action_func = self.action_registry.get(action_name)
278
304
 
279
- context = self.create_context(action)
280
-
281
- self.contexts[action.step_run_id] = context
282
-
283
305
  if action_func:
306
+ context = self.create_context(
307
+ action, True if action_func.is_durable else False
308
+ )
309
+
310
+ self.contexts[action.step_run_id] = context
284
311
  self.event_queue.put(
285
312
  ActionEvent(action=action, type=STEP_EVENT_TYPE_STARTED, payload="")
286
313
  )
@@ -301,20 +328,16 @@ class Runner:
301
328
  # do nothing, this should be caught in the callback
302
329
  pass
303
330
 
331
+ ## Once the step run completes, we need to remove the workflow spawn index
332
+ ## so we don't leak memory
333
+ if action.workflow_run_id in workflow_spawn_indices:
334
+ async with spawn_index_lock:
335
+ workflow_spawn_indices.pop(action.workflow_run_id)
336
+
304
337
  ## IMPORTANT: Keep this method's signature in sync with the wrapper in the OTel instrumentor
305
338
  async def handle_start_group_key_run(self, action: Action) -> Exception | None:
306
339
  action_name = action.action_id
307
- context = Context(
308
- action,
309
- self.dispatcher_client,
310
- self.admin_client,
311
- self.client.event,
312
- self.client.rest,
313
- self.client.workflow_listener,
314
- self.workflow_run_event_listener,
315
- self.worker_context,
316
- self.client.config.namespace,
317
- )
340
+ context = self.create_context(action)
318
341
 
319
342
  self.contexts[action.get_group_key_run_id] = context
320
343
 
@@ -412,7 +435,7 @@ class Runner:
412
435
 
413
436
  if output is not None:
414
437
  try:
415
- return json.dumps(output)
438
+ return json.dumps(output, default=str)
416
439
  except Exception as e:
417
440
  logger.error(f"Could not serialize output: {e}")
418
441
  return str(output)
@@ -427,6 +450,6 @@ class Runner:
427
450
  running = len(self.tasks.keys())
428
451
 
429
452
 
430
- def errorWithTraceback(message: str, e: Exception) -> str:
453
+ def pretty_format_exception(message: str, e: Exception) -> str:
431
454
  trace = "".join(traceback.format_exception(type(e), e, e.__traceback__))
432
455
  return f"{message}\n{trace}"
@@ -1,4 +1,3 @@
1
- import contextvars
2
1
  import functools
3
2
  import logging
4
3
  from concurrent.futures import ThreadPoolExecutor
@@ -8,14 +7,7 @@ from typing import Any, Awaitable, Callable, ItemsView, ParamSpec, TypeVar
8
7
 
9
8
  from hatchet_sdk.clients.events import EventClient
10
9
  from hatchet_sdk.logger import logger
11
-
12
- wr: contextvars.ContextVar[str | None] = contextvars.ContextVar(
13
- "workflow_run_id", default=None
14
- )
15
- sr: contextvars.ContextVar[str | None] = contextvars.ContextVar(
16
- "step_run_id", default=None
17
- )
18
-
10
+ from hatchet_sdk.runnables.contextvars import ctx_step_run_id, ctx_workflow_run_id
19
11
 
20
12
  T = TypeVar("T")
21
13
  P = ParamSpec("P")
@@ -37,8 +29,8 @@ class InjectingFilter(logging.Filter):
37
29
  # otherwise we would use emit within the CustomLogHandler
38
30
  def filter(self, record: logging.LogRecord) -> bool:
39
31
  ## TODO: Change how we do this to not assign to the log record
40
- record.workflow_run_id = wr.get()
41
- record.step_run_id = sr.get()
32
+ record.workflow_run_id = ctx_workflow_run_id.get()
33
+ record.step_run_id = ctx_step_run_id.get()
42
34
  return True
43
35
 
44
36