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.
- hatchet_sdk/__init__.py +27 -16
- hatchet_sdk/client.py +13 -63
- hatchet_sdk/clients/admin.py +203 -124
- hatchet_sdk/clients/dispatcher/action_listener.py +42 -42
- hatchet_sdk/clients/dispatcher/dispatcher.py +18 -16
- hatchet_sdk/clients/durable_event_listener.py +327 -0
- hatchet_sdk/clients/rest/__init__.py +12 -1
- hatchet_sdk/clients/rest/api/log_api.py +258 -0
- hatchet_sdk/clients/rest/api/task_api.py +32 -6
- hatchet_sdk/clients/rest/api/workflow_runs_api.py +626 -0
- hatchet_sdk/clients/rest/models/__init__.py +12 -1
- hatchet_sdk/clients/rest/models/v1_log_line.py +94 -0
- hatchet_sdk/clients/rest/models/v1_log_line_level.py +39 -0
- hatchet_sdk/clients/rest/models/v1_log_line_list.py +110 -0
- hatchet_sdk/clients/rest/models/v1_task_summary.py +80 -64
- hatchet_sdk/clients/rest/models/v1_trigger_workflow_run_request.py +95 -0
- hatchet_sdk/clients/rest/models/v1_workflow_run_display_name.py +98 -0
- hatchet_sdk/clients/rest/models/v1_workflow_run_display_name_list.py +114 -0
- hatchet_sdk/clients/rest/models/workflow_run_shape_item_for_workflow_run_details.py +9 -4
- hatchet_sdk/clients/rest_client.py +21 -0
- hatchet_sdk/clients/run_event_listener.py +0 -1
- hatchet_sdk/context/context.py +85 -147
- hatchet_sdk/contracts/dispatcher_pb2_grpc.py +1 -1
- hatchet_sdk/contracts/events_pb2.py +2 -2
- hatchet_sdk/contracts/events_pb2_grpc.py +1 -1
- hatchet_sdk/contracts/v1/dispatcher_pb2.py +36 -0
- hatchet_sdk/contracts/v1/dispatcher_pb2.pyi +38 -0
- hatchet_sdk/contracts/v1/dispatcher_pb2_grpc.py +145 -0
- hatchet_sdk/contracts/v1/shared/condition_pb2.py +39 -0
- hatchet_sdk/contracts/v1/shared/condition_pb2.pyi +72 -0
- hatchet_sdk/contracts/v1/shared/condition_pb2_grpc.py +29 -0
- hatchet_sdk/contracts/v1/workflows_pb2.py +67 -0
- hatchet_sdk/contracts/v1/workflows_pb2.pyi +228 -0
- hatchet_sdk/contracts/v1/workflows_pb2_grpc.py +234 -0
- hatchet_sdk/contracts/workflows_pb2_grpc.py +1 -1
- hatchet_sdk/features/cron.py +3 -3
- hatchet_sdk/features/scheduled.py +2 -2
- hatchet_sdk/hatchet.py +427 -151
- hatchet_sdk/opentelemetry/instrumentor.py +8 -13
- hatchet_sdk/rate_limit.py +33 -39
- hatchet_sdk/runnables/contextvars.py +12 -0
- hatchet_sdk/runnables/standalone.py +194 -0
- hatchet_sdk/runnables/task.py +144 -0
- hatchet_sdk/runnables/types.py +138 -0
- hatchet_sdk/runnables/workflow.py +764 -0
- hatchet_sdk/utils/aio_utils.py +0 -79
- hatchet_sdk/utils/proto_enums.py +0 -7
- hatchet_sdk/utils/timedelta_to_expression.py +23 -0
- hatchet_sdk/utils/typing.py +2 -2
- hatchet_sdk/v0/clients/rest_client.py +9 -0
- hatchet_sdk/v0/worker/action_listener_process.py +18 -2
- hatchet_sdk/waits.py +120 -0
- hatchet_sdk/worker/action_listener_process.py +64 -30
- hatchet_sdk/worker/runner/run_loop_manager.py +35 -25
- hatchet_sdk/worker/runner/runner.py +72 -49
- hatchet_sdk/worker/runner/utils/capture_logs.py +3 -11
- hatchet_sdk/worker/worker.py +155 -118
- hatchet_sdk/workflow_run.py +4 -5
- {hatchet_sdk-1.0.0.dist-info → hatchet_sdk-1.0.0a1.dist-info}/METADATA +1 -2
- {hatchet_sdk-1.0.0.dist-info → hatchet_sdk-1.0.0a1.dist-info}/RECORD +62 -42
- {hatchet_sdk-1.0.0.dist-info → hatchet_sdk-1.0.0a1.dist-info}/entry_points.txt +2 -0
- hatchet_sdk/semver.py +0 -30
- hatchet_sdk/worker/runner/utils/error_with_traceback.py +0 -6
- hatchet_sdk/workflow.py +0 -527
- {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,
|
|
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
|
|
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
|
|
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[
|
|
53
|
-
|
|
59
|
+
event_queue: "Queue[ActionEvent]",
|
|
60
|
+
config: ClientConfig,
|
|
61
|
+
slots: int | None = None,
|
|
54
62
|
handle_kill: bool = True,
|
|
55
|
-
action_registry: dict[str,
|
|
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 =
|
|
69
|
+
self.client = Client(config)
|
|
63
70
|
self.name = self.client.config.namespace + name
|
|
64
|
-
self.
|
|
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
|
|
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=
|
|
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=
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
213
|
-
|
|
222
|
+
ctx: Context,
|
|
223
|
+
task: Task[TWorkflowInput, R],
|
|
214
224
|
action: Action,
|
|
215
225
|
run_id: str,
|
|
216
|
-
) ->
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
222
|
-
return await
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
259
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
41
|
-
record.step_run_id =
|
|
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
|
|