hatchet-sdk 1.0.0__py3-none-any.whl → 1.0.1__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 (73) hide show
  1. hatchet_sdk/__init__.py +32 -16
  2. hatchet_sdk/client.py +25 -63
  3. hatchet_sdk/clients/admin.py +203 -142
  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/models/workflow_runs_metrics.py +5 -1
  21. hatchet_sdk/clients/run_event_listener.py +0 -1
  22. hatchet_sdk/clients/v1/api_client.py +81 -0
  23. hatchet_sdk/context/context.py +86 -159
  24. hatchet_sdk/contracts/dispatcher_pb2_grpc.py +1 -1
  25. hatchet_sdk/contracts/events_pb2.py +2 -2
  26. hatchet_sdk/contracts/events_pb2_grpc.py +1 -1
  27. hatchet_sdk/contracts/v1/dispatcher_pb2.py +36 -0
  28. hatchet_sdk/contracts/v1/dispatcher_pb2.pyi +38 -0
  29. hatchet_sdk/contracts/v1/dispatcher_pb2_grpc.py +145 -0
  30. hatchet_sdk/contracts/v1/shared/condition_pb2.py +39 -0
  31. hatchet_sdk/contracts/v1/shared/condition_pb2.pyi +72 -0
  32. hatchet_sdk/contracts/v1/shared/condition_pb2_grpc.py +29 -0
  33. hatchet_sdk/contracts/v1/workflows_pb2.py +67 -0
  34. hatchet_sdk/contracts/v1/workflows_pb2.pyi +228 -0
  35. hatchet_sdk/contracts/v1/workflows_pb2_grpc.py +234 -0
  36. hatchet_sdk/contracts/workflows_pb2_grpc.py +1 -1
  37. hatchet_sdk/features/cron.py +91 -121
  38. hatchet_sdk/features/logs.py +16 -0
  39. hatchet_sdk/features/metrics.py +75 -0
  40. hatchet_sdk/features/rate_limits.py +45 -0
  41. hatchet_sdk/features/runs.py +221 -0
  42. hatchet_sdk/features/scheduled.py +114 -131
  43. hatchet_sdk/features/workers.py +41 -0
  44. hatchet_sdk/features/workflows.py +55 -0
  45. hatchet_sdk/hatchet.py +463 -165
  46. hatchet_sdk/opentelemetry/instrumentor.py +8 -13
  47. hatchet_sdk/rate_limit.py +33 -39
  48. hatchet_sdk/runnables/contextvars.py +12 -0
  49. hatchet_sdk/runnables/standalone.py +192 -0
  50. hatchet_sdk/runnables/task.py +144 -0
  51. hatchet_sdk/runnables/types.py +138 -0
  52. hatchet_sdk/runnables/workflow.py +771 -0
  53. hatchet_sdk/utils/aio_utils.py +0 -79
  54. hatchet_sdk/utils/proto_enums.py +0 -7
  55. hatchet_sdk/utils/timedelta_to_expression.py +23 -0
  56. hatchet_sdk/utils/typing.py +2 -2
  57. hatchet_sdk/v0/clients/rest_client.py +9 -0
  58. hatchet_sdk/v0/worker/action_listener_process.py +18 -2
  59. hatchet_sdk/waits.py +120 -0
  60. hatchet_sdk/worker/action_listener_process.py +64 -30
  61. hatchet_sdk/worker/runner/run_loop_manager.py +35 -26
  62. hatchet_sdk/worker/runner/runner.py +72 -55
  63. hatchet_sdk/worker/runner/utils/capture_logs.py +3 -11
  64. hatchet_sdk/worker/worker.py +155 -118
  65. hatchet_sdk/workflow_run.py +4 -5
  66. {hatchet_sdk-1.0.0.dist-info → hatchet_sdk-1.0.1.dist-info}/METADATA +1 -2
  67. {hatchet_sdk-1.0.0.dist-info → hatchet_sdk-1.0.1.dist-info}/RECORD +69 -43
  68. {hatchet_sdk-1.0.0.dist-info → hatchet_sdk-1.0.1.dist-info}/entry_points.txt +2 -0
  69. hatchet_sdk/clients/rest_client.py +0 -636
  70. hatchet_sdk/semver.py +0 -30
  71. hatchet_sdk/worker/runner/utils/error_with_traceback.py +0 -6
  72. hatchet_sdk/workflow.py +0 -527
  73. {hatchet_sdk-1.0.0.dist-info → hatchet_sdk-1.0.1.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):
@@ -48,29 +55,27 @@ class WorkerStatus(Enum):
48
55
  class Runner:
49
56
  def __init__(
50
57
  self,
51
- name: str,
52
- event_queue: "Queue[Any]",
53
- max_runs: int | None = None,
58
+ event_queue: "Queue[ActionEvent]",
59
+ config: ClientConfig,
60
+ slots: int | None = None,
54
61
  handle_kill: bool = True,
55
- action_registry: dict[str, Step[T]] = {},
62
+ action_registry: dict[str, Task[TWorkflowInput, R]] = {},
56
63
  validator_registry: dict[str, WorkflowValidator] = {},
57
- config: ClientConfig = ClientConfig(),
58
64
  labels: dict[str, str | int] = {},
59
65
  ):
60
66
  # We store the config so we can dynamically create clients for the dispatcher client.
61
67
  self.config = config
62
- self.client = new_client_raw(config)
63
- self.name = self.client.config.namespace + name
64
- self.max_runs = max_runs
68
+ self.client = Client(config)
69
+ self.slots = slots
65
70
  self.tasks: dict[str, asyncio.Task[Any]] = {} # Store run ids and futures
66
71
  self.contexts: dict[str, Context] = {} # Store run ids and contexts
67
- self.action_registry: dict[str, Step[T]] = action_registry
72
+ self.action_registry = action_registry
68
73
  self.validator_registry = validator_registry
69
74
 
70
75
  self.event_queue = event_queue
71
76
 
72
77
  # The thread pool is used for synchronous functions which need to run concurrently
73
- self.thread_pool = ThreadPoolExecutor(max_workers=max_runs)
78
+ self.thread_pool = ThreadPoolExecutor(max_workers=slots)
74
79
  self.threads: Dict[str, Thread] = {} # Store run ids and threads
75
80
 
76
81
  self.killing = False
@@ -82,9 +87,10 @@ class Runner:
82
87
  self.admin_client = AdminClient(self.config)
83
88
  self.workflow_run_event_listener = RunEventListenerClient(self.config)
84
89
  self.client.workflow_listener = PooledWorkflowRunListener(self.config)
90
+ self.durable_event_listener = DurableEventListener(self.config)
85
91
 
86
92
  self.worker_context = WorkerContext(
87
- labels=labels, client=new_client_raw(config).dispatcher
93
+ labels=labels, client=Client(config=config).dispatcher
88
94
  )
89
95
 
90
96
  def create_workflow_run_url(self, action: Action) -> str:
@@ -130,7 +136,7 @@ class Runner:
130
136
  ActionEvent(
131
137
  action=action,
132
138
  type=STEP_EVENT_TYPE_FAILED,
133
- payload=str(errorWithTraceback(f"{e}", e)),
139
+ payload=str(pretty_format_exception(f"{e}", e)),
134
140
  )
135
141
  )
136
142
 
@@ -172,7 +178,7 @@ class Runner:
172
178
  ActionEvent(
173
179
  action=action,
174
180
  type=GROUP_KEY_EVENT_TYPE_FAILED,
175
- payload=str(errorWithTraceback(f"{e}", e)),
181
+ payload=str(pretty_format_exception(f"{e}", e)),
176
182
  )
177
183
  )
178
184
 
@@ -195,7 +201,9 @@ class Runner:
195
201
 
196
202
  return inner_callback
197
203
 
198
- def thread_action_func(self, context: Context, step: Step[T], action: Action) -> T:
204
+ def thread_action_func(
205
+ self, ctx: Context, task: Task[TWorkflowInput, R], action: Action
206
+ ) -> R:
199
207
  if action.step_run_id is not None and action.step_run_id != "":
200
208
  self.threads[action.step_run_id] = current_thread()
201
209
  elif (
@@ -204,22 +212,23 @@ class Runner:
204
212
  ):
205
213
  self.threads[action.get_group_key_run_id] = current_thread()
206
214
 
207
- return step.call(context)
215
+ return task.call(ctx)
208
216
 
209
217
  # We wrap all actions in an async func
210
218
  async def async_wrapped_action_func(
211
219
  self,
212
- context: Context,
213
- step: Step[T],
220
+ ctx: Context,
221
+ task: Task[TWorkflowInput, R],
214
222
  action: Action,
215
223
  run_id: str,
216
- ) -> T:
217
- wr.set(context.workflow_run_id)
218
- sr.set(context.step_run_id)
224
+ ) -> R:
225
+ ctx_step_run_id.set(action.step_run_id)
226
+ ctx_workflow_run_id.set(action.workflow_run_id)
227
+ ctx_worker_id.set(action.worker_id)
219
228
 
220
229
  try:
221
- if step.is_async_function:
222
- return await step.aio_call(context)
230
+ if task.is_async_function:
231
+ return await task.aio_call(ctx)
223
232
  else:
224
233
  pfunc = functools.partial(
225
234
  # we must copy the context vars to the new thread, as only asyncio natively supports
@@ -227,8 +236,8 @@ class Runner:
227
236
  copy_context_vars,
228
237
  contextvars.copy_context().items(),
229
238
  self.thread_action_func,
230
- context,
231
- step,
239
+ ctx,
240
+ task,
232
241
  action,
233
242
  )
234
243
 
@@ -236,7 +245,7 @@ class Runner:
236
245
  return await loop.run_in_executor(self.thread_pool, pfunc)
237
246
  except Exception as e:
238
247
  logger.error(
239
- errorWithTraceback(
248
+ pretty_format_exception(
240
249
  f"exception raised in action ({action.action_id}, retry={action.retry_count}):\n{e}",
241
250
  e,
242
251
  )
@@ -255,17 +264,28 @@ class Runner:
255
264
  if run_id in self.contexts:
256
265
  del self.contexts[run_id]
257
266
 
258
- def create_context(self, action: Action) -> Context:
259
- return Context(
267
+ @overload
268
+ def create_context(
269
+ self, action: Action, is_durable: Literal[True] = True
270
+ ) -> DurableContext: ...
271
+
272
+ @overload
273
+ def create_context(
274
+ self, action: Action, is_durable: Literal[False] = False
275
+ ) -> Context: ...
276
+
277
+ def create_context(
278
+ self, action: Action, is_durable: bool = True
279
+ ) -> Context | DurableContext:
280
+ constructor = DurableContext if is_durable else Context
281
+
282
+ return constructor(
260
283
  action,
261
284
  self.dispatcher_client,
262
285
  self.admin_client,
263
286
  self.client.event,
264
- self.client.rest,
265
- self.client.workflow_listener,
266
- self.workflow_run_event_listener,
287
+ self.durable_event_listener,
267
288
  self.worker_context,
268
- self.client.config.namespace,
269
289
  validator_registry=self.validator_registry,
270
290
  )
271
291
 
@@ -276,11 +296,12 @@ class Runner:
276
296
  # Find the corresponding action function from the registry
277
297
  action_func = self.action_registry.get(action_name)
278
298
 
279
- context = self.create_context(action)
280
-
281
- self.contexts[action.step_run_id] = context
282
-
283
299
  if action_func:
300
+ context = self.create_context(
301
+ action, True if action_func.is_durable else False
302
+ )
303
+
304
+ self.contexts[action.step_run_id] = context
284
305
  self.event_queue.put(
285
306
  ActionEvent(action=action, type=STEP_EVENT_TYPE_STARTED, payload="")
286
307
  )
@@ -301,20 +322,16 @@ class Runner:
301
322
  # do nothing, this should be caught in the callback
302
323
  pass
303
324
 
325
+ ## Once the step run completes, we need to remove the workflow spawn index
326
+ ## so we don't leak memory
327
+ if action.workflow_run_id in workflow_spawn_indices:
328
+ async with spawn_index_lock:
329
+ workflow_spawn_indices.pop(action.workflow_run_id)
330
+
304
331
  ## IMPORTANT: Keep this method's signature in sync with the wrapper in the OTel instrumentor
305
332
  async def handle_start_group_key_run(self, action: Action) -> Exception | None:
306
333
  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
- )
334
+ context = self.create_context(action)
318
335
 
319
336
  self.contexts[action.get_group_key_run_id] = context
320
337
 
@@ -412,7 +429,7 @@ class Runner:
412
429
 
413
430
  if output is not None:
414
431
  try:
415
- return json.dumps(output)
432
+ return json.dumps(output, default=str)
416
433
  except Exception as e:
417
434
  logger.error(f"Could not serialize output: {e}")
418
435
  return str(output)
@@ -427,6 +444,6 @@ class Runner:
427
444
  running = len(self.tasks.keys())
428
445
 
429
446
 
430
- def errorWithTraceback(message: str, e: Exception) -> str:
447
+ def pretty_format_exception(message: str, e: Exception) -> str:
431
448
  trace = "".join(traceback.format_exception(type(e), e, e.__traceback__))
432
449
  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