apache-hamilton 1.90.0.dev0__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.
- apache_hamilton-1.90.0.dev0.dist-info/METADATA +407 -0
- apache_hamilton-1.90.0.dev0.dist-info/RECORD +151 -0
- apache_hamilton-1.90.0.dev0.dist-info/WHEEL +4 -0
- apache_hamilton-1.90.0.dev0.dist-info/entry_points.txt +9 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/DISCLAIMER +10 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/LICENSE +228 -0
- apache_hamilton-1.90.0.dev0.dist-info/licenses/NOTICE +5 -0
- hamilton/__init__.py +24 -0
- hamilton/ad_hoc_utils.py +132 -0
- hamilton/async_driver.py +465 -0
- hamilton/base.py +466 -0
- hamilton/caching/__init__.py +16 -0
- hamilton/caching/adapter.py +1475 -0
- hamilton/caching/cache_key.py +70 -0
- hamilton/caching/fingerprinting.py +287 -0
- hamilton/caching/stores/__init__.py +16 -0
- hamilton/caching/stores/base.py +242 -0
- hamilton/caching/stores/file.py +140 -0
- hamilton/caching/stores/memory.py +297 -0
- hamilton/caching/stores/sqlite.py +282 -0
- hamilton/caching/stores/utils.py +40 -0
- hamilton/cli/__init__.py +16 -0
- hamilton/cli/__main__.py +328 -0
- hamilton/cli/commands.py +126 -0
- hamilton/cli/logic.py +338 -0
- hamilton/common/__init__.py +76 -0
- hamilton/contrib/__init__.py +41 -0
- hamilton/data_quality/__init__.py +16 -0
- hamilton/data_quality/base.py +198 -0
- hamilton/data_quality/default_validators.py +560 -0
- hamilton/data_quality/pandera_validators.py +121 -0
- hamilton/dataflows/__init__.py +726 -0
- hamilton/dataflows/template/README.md +25 -0
- hamilton/dataflows/template/__init__.py +54 -0
- hamilton/dataflows/template/author.md +28 -0
- hamilton/dataflows/template/requirements.txt +0 -0
- hamilton/dataflows/template/tags.json +7 -0
- hamilton/dataflows/template/valid_configs.jsonl +1 -0
- hamilton/dev_utils/__init__.py +16 -0
- hamilton/dev_utils/deprecation.py +204 -0
- hamilton/driver.py +2112 -0
- hamilton/execution/__init__.py +16 -0
- hamilton/execution/debugging_utils.py +56 -0
- hamilton/execution/executors.py +502 -0
- hamilton/execution/graph_functions.py +421 -0
- hamilton/execution/grouping.py +430 -0
- hamilton/execution/state.py +539 -0
- hamilton/experimental/__init__.py +27 -0
- hamilton/experimental/databackend.py +61 -0
- hamilton/experimental/decorators/__init__.py +16 -0
- hamilton/experimental/decorators/parameterize_frame.py +233 -0
- hamilton/experimental/h_async.py +29 -0
- hamilton/experimental/h_cache.py +413 -0
- hamilton/experimental/h_dask.py +28 -0
- hamilton/experimental/h_databackends.py +174 -0
- hamilton/experimental/h_ray.py +28 -0
- hamilton/experimental/h_spark.py +32 -0
- hamilton/function_modifiers/README +40 -0
- hamilton/function_modifiers/__init__.py +121 -0
- hamilton/function_modifiers/adapters.py +900 -0
- hamilton/function_modifiers/base.py +859 -0
- hamilton/function_modifiers/configuration.py +310 -0
- hamilton/function_modifiers/delayed.py +202 -0
- hamilton/function_modifiers/dependencies.py +246 -0
- hamilton/function_modifiers/expanders.py +1230 -0
- hamilton/function_modifiers/macros.py +1634 -0
- hamilton/function_modifiers/metadata.py +434 -0
- hamilton/function_modifiers/recursive.py +908 -0
- hamilton/function_modifiers/validation.py +289 -0
- hamilton/function_modifiers_base.py +31 -0
- hamilton/graph.py +1153 -0
- hamilton/graph_types.py +264 -0
- hamilton/graph_utils.py +41 -0
- hamilton/htypes.py +450 -0
- hamilton/io/__init__.py +32 -0
- hamilton/io/data_adapters.py +216 -0
- hamilton/io/default_data_loaders.py +224 -0
- hamilton/io/materialization.py +500 -0
- hamilton/io/utils.py +158 -0
- hamilton/lifecycle/__init__.py +67 -0
- hamilton/lifecycle/api.py +833 -0
- hamilton/lifecycle/base.py +1130 -0
- hamilton/lifecycle/default.py +802 -0
- hamilton/log_setup.py +47 -0
- hamilton/models.py +92 -0
- hamilton/node.py +449 -0
- hamilton/plugins/README.md +48 -0
- hamilton/plugins/__init__.py +16 -0
- hamilton/plugins/dask_extensions.py +47 -0
- hamilton/plugins/dlt_extensions.py +161 -0
- hamilton/plugins/geopandas_extensions.py +49 -0
- hamilton/plugins/h_dask.py +331 -0
- hamilton/plugins/h_ddog.py +522 -0
- hamilton/plugins/h_diskcache.py +163 -0
- hamilton/plugins/h_experiments/__init__.py +22 -0
- hamilton/plugins/h_experiments/__main__.py +62 -0
- hamilton/plugins/h_experiments/cache.py +39 -0
- hamilton/plugins/h_experiments/data_model.py +68 -0
- hamilton/plugins/h_experiments/hook.py +219 -0
- hamilton/plugins/h_experiments/server.py +375 -0
- hamilton/plugins/h_kedro.py +152 -0
- hamilton/plugins/h_logging.py +454 -0
- hamilton/plugins/h_mcp/__init__.py +28 -0
- hamilton/plugins/h_mcp/__main__.py +33 -0
- hamilton/plugins/h_mcp/_helpers.py +129 -0
- hamilton/plugins/h_mcp/_templates.py +417 -0
- hamilton/plugins/h_mcp/server.py +328 -0
- hamilton/plugins/h_mlflow.py +335 -0
- hamilton/plugins/h_narwhals.py +134 -0
- hamilton/plugins/h_openlineage.py +400 -0
- hamilton/plugins/h_opentelemetry.py +167 -0
- hamilton/plugins/h_pandas.py +257 -0
- hamilton/plugins/h_pandera.py +117 -0
- hamilton/plugins/h_polars.py +304 -0
- hamilton/plugins/h_polars_lazyframe.py +282 -0
- hamilton/plugins/h_pyarrow.py +56 -0
- hamilton/plugins/h_pydantic.py +127 -0
- hamilton/plugins/h_ray.py +242 -0
- hamilton/plugins/h_rich.py +142 -0
- hamilton/plugins/h_schema.py +493 -0
- hamilton/plugins/h_slack.py +100 -0
- hamilton/plugins/h_spark.py +1380 -0
- hamilton/plugins/h_threadpool.py +125 -0
- hamilton/plugins/h_tqdm.py +122 -0
- hamilton/plugins/h_vaex.py +129 -0
- hamilton/plugins/huggingface_extensions.py +236 -0
- hamilton/plugins/ibis_extensions.py +93 -0
- hamilton/plugins/jupyter_magic.py +622 -0
- hamilton/plugins/kedro_extensions.py +117 -0
- hamilton/plugins/lightgbm_extensions.py +99 -0
- hamilton/plugins/matplotlib_extensions.py +108 -0
- hamilton/plugins/mlflow_extensions.py +216 -0
- hamilton/plugins/numpy_extensions.py +105 -0
- hamilton/plugins/pandas_extensions.py +1763 -0
- hamilton/plugins/plotly_extensions.py +150 -0
- hamilton/plugins/polars_extensions.py +71 -0
- hamilton/plugins/polars_implementations.py +25 -0
- hamilton/plugins/polars_lazyframe_extensions.py +302 -0
- hamilton/plugins/polars_post_1_0_0_extensions.py +877 -0
- hamilton/plugins/polars_pre_1_0_0_extension.py +836 -0
- hamilton/plugins/pydantic_extensions.py +98 -0
- hamilton/plugins/pyspark_pandas_extensions.py +47 -0
- hamilton/plugins/sklearn_plot_extensions.py +129 -0
- hamilton/plugins/spark_extensions.py +105 -0
- hamilton/plugins/vaex_extensions.py +51 -0
- hamilton/plugins/xgboost_extensions.py +91 -0
- hamilton/plugins/yaml_extensions.py +89 -0
- hamilton/registry.py +254 -0
- hamilton/settings.py +18 -0
- hamilton/telemetry.py +50 -0
- hamilton/version.py +18 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from hamilton.execution.grouping import NodeGroup, NodeGroupPurpose, TaskSpec
|
|
20
|
+
|
|
21
|
+
"""A set of utilities for debugging/printing out data"""
|
|
22
|
+
group_purpose_icons = {
|
|
23
|
+
NodeGroupPurpose.EXPAND_UNORDERED: "⫳",
|
|
24
|
+
NodeGroupPurpose.GATHER: "⋃",
|
|
25
|
+
NodeGroupPurpose.EXECUTE_BLOCK: "᠅",
|
|
26
|
+
NodeGroupPurpose.EXECUTE_SINGLE: "•",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def print_node_groups(node_groups: list[NodeGroup]):
|
|
31
|
+
"""Prints out the node groups in a clean, tree-like format.
|
|
32
|
+
|
|
33
|
+
:param node_groups:
|
|
34
|
+
:return:
|
|
35
|
+
"""
|
|
36
|
+
for group in node_groups:
|
|
37
|
+
node_icon = group_purpose_icons[group.purpose]
|
|
38
|
+
print(f"{node_icon} {group.base_id}")
|
|
39
|
+
for node_ in group.nodes:
|
|
40
|
+
print(f" • {node_.name} [ƒ({','.join(map(lambda n: n.name, node_.dependencies))})]")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def print_tasks(tasks: list[TaskSpec]):
|
|
44
|
+
"""Prints out the node groups in a clean, tree-like format.
|
|
45
|
+
|
|
46
|
+
:param tasks:
|
|
47
|
+
:return:
|
|
48
|
+
"""
|
|
49
|
+
print()
|
|
50
|
+
for task in tasks:
|
|
51
|
+
node_icon = group_purpose_icons[task.purpose]
|
|
52
|
+
print(f"{node_icon} {task.base_id} [ƒ({', '.join(task.base_dependencies)})]")
|
|
53
|
+
for node_ in task.nodes:
|
|
54
|
+
print(
|
|
55
|
+
f" • {node_.name}"
|
|
56
|
+
) # ƒ({', '.join(map(lambda n: n.name, node_.dependencies))})")
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
+
# or more contributor license agreements. See the NOTICE file
|
|
3
|
+
# distributed with this work for additional information
|
|
4
|
+
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
+
# to you under the Apache License, Version 2.0 (the
|
|
6
|
+
# "License"); you may not use this file except in compliance
|
|
7
|
+
# with the License. You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing,
|
|
12
|
+
# software distributed under the License is distributed on an
|
|
13
|
+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
+
# KIND, either express or implied. See the License for the
|
|
15
|
+
# specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
17
|
+
|
|
18
|
+
import abc
|
|
19
|
+
import dataclasses
|
|
20
|
+
import functools
|
|
21
|
+
import logging
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
if sys.platform in ("emscripten", "wasi"):
|
|
25
|
+
ProcessPoolExecutor = None
|
|
26
|
+
else:
|
|
27
|
+
from concurrent.futures.process import ProcessPoolExecutor
|
|
28
|
+
|
|
29
|
+
from collections.abc import Callable
|
|
30
|
+
from concurrent.futures import Executor, Future, ThreadPoolExecutor
|
|
31
|
+
from typing import Any, Protocol
|
|
32
|
+
|
|
33
|
+
from hamilton import node
|
|
34
|
+
from hamilton.execution.graph_functions import execute_subdag
|
|
35
|
+
from hamilton.execution.grouping import NodeGroupPurpose, TaskImplementation
|
|
36
|
+
from hamilton.execution.state import ExecutionState, GraphState, TaskState
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TaskFuture(Protocol):
|
|
42
|
+
"""Simple representation of a future. TODO -- add cancel().
|
|
43
|
+
This a clean wrapper over a python future, and we may end up just using that at some point."""
|
|
44
|
+
|
|
45
|
+
def get_state(self) -> TaskState:
|
|
46
|
+
"""Returns the state of the task."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
def get_result(self) -> Any:
|
|
50
|
+
"""Returns the result of the task."""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TaskExecutor(abc.ABC):
|
|
55
|
+
"""Abstract class for a task executor. All this does is submit a task and return a future.
|
|
56
|
+
It also tells us if it can do that"""
|
|
57
|
+
|
|
58
|
+
@abc.abstractmethod
|
|
59
|
+
def init(self):
|
|
60
|
+
"""Initializes the task executor, provisioning any necessary resources."""
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
@abc.abstractmethod
|
|
64
|
+
def finalize(self):
|
|
65
|
+
"""Tears down the task executor, freeing up any provisioned resources.
|
|
66
|
+
Will be called in a finally block."""
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
@abc.abstractmethod
|
|
70
|
+
def submit_task(self, task: TaskImplementation) -> TaskFuture:
|
|
71
|
+
"""Submits a task to the executor. Returns a task ID that can be used to query the status.
|
|
72
|
+
Effectively a future.
|
|
73
|
+
|
|
74
|
+
:param task: Task implementation (bound with arguments) to submit
|
|
75
|
+
:return: The future representing the task's computation.
|
|
76
|
+
"""
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
@abc.abstractmethod
|
|
80
|
+
def can_submit_task(self) -> bool:
|
|
81
|
+
"""Returns whether or not we can submit a task to the executor.
|
|
82
|
+
For instance, if the maximum parallelism is reached, we may not be able to submit a task.
|
|
83
|
+
|
|
84
|
+
TODO -- consider if this should be a "parallelism" value instead of a boolean, forcing
|
|
85
|
+
the ExecutionState to store the state prior to executing a task.
|
|
86
|
+
|
|
87
|
+
:return: whether or not we can submit a task.
|
|
88
|
+
"""
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def new_callable(*args, _callable=None, **kwargs):
|
|
93
|
+
return list(_callable(*args, **kwargs))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _modify_callable(node_source: node.NodeType, callabl: Callable):
|
|
97
|
+
"""This is a bit of a shortcut -- we modify the callable here as
|
|
98
|
+
we want to allow `Parallelizable[]` nodes to return a generator
|
|
99
|
+
|
|
100
|
+
:param node_source:
|
|
101
|
+
:param callabl:
|
|
102
|
+
:return:
|
|
103
|
+
"""
|
|
104
|
+
if node_source == node.NodeType.EXPAND:
|
|
105
|
+
return functools.partial(new_callable, _callable=callabl)
|
|
106
|
+
return callabl
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def base_execute_task(task: TaskImplementation) -> dict[str, Any]:
|
|
110
|
+
"""This is a utility function to execute a base task. In an ideal world this would be recursive,
|
|
111
|
+
(as in we can use the same task execution/management system as we would otherwise)
|
|
112
|
+
but for now we just call out to good old DFS. Note that this only returns the result that
|
|
113
|
+
a task is required to output, and does not return anything else. It also returns
|
|
114
|
+
any overrides.
|
|
115
|
+
|
|
116
|
+
We should probably have a simple way of doing this for single-node tasks, as they're
|
|
117
|
+
going to be common.
|
|
118
|
+
|
|
119
|
+
:param task: task to execute.
|
|
120
|
+
:return: a diciontary of the results of all the nodes in that task's nodes to compute.
|
|
121
|
+
"""
|
|
122
|
+
# We do this as an edge case to force the callable to return a list if it is an expand,
|
|
123
|
+
# and would normally return a generator. That said, we will likely remove this in the future --
|
|
124
|
+
# its an implementation detail, and a true queuing system between nodes/controller would mean
|
|
125
|
+
# we wouldn't need to do this, and instead could just use the generator aspect.
|
|
126
|
+
# Furthermore, in most cases the user wouldn't be calling an expand on a "remote" node,
|
|
127
|
+
# but it is a supported use-case.
|
|
128
|
+
for node_ in task.nodes:
|
|
129
|
+
if not getattr(node_, "callable_modified", False):
|
|
130
|
+
node_._callable = _modify_callable(node_.node_role, node_.callable)
|
|
131
|
+
node_.callable_modified = True
|
|
132
|
+
if task.adapter.does_hook("pre_task_execute", is_async=False):
|
|
133
|
+
task.adapter.call_all_lifecycle_hooks_sync(
|
|
134
|
+
"pre_task_execute",
|
|
135
|
+
run_id=task.run_id,
|
|
136
|
+
task_id=task.task_id,
|
|
137
|
+
nodes=task.nodes,
|
|
138
|
+
inputs=task.dynamic_inputs,
|
|
139
|
+
overrides=task.overrides,
|
|
140
|
+
spawning_task_id=task.spawning_task_id,
|
|
141
|
+
purpose=task.purpose,
|
|
142
|
+
)
|
|
143
|
+
error = None
|
|
144
|
+
success = True
|
|
145
|
+
results = None
|
|
146
|
+
try:
|
|
147
|
+
results = execute_subdag(
|
|
148
|
+
nodes=task.nodes,
|
|
149
|
+
inputs=task.dynamic_inputs,
|
|
150
|
+
adapter=task.adapter,
|
|
151
|
+
overrides={**task.dynamic_inputs, **task.overrides},
|
|
152
|
+
run_id=task.run_id,
|
|
153
|
+
task_id=task.task_id,
|
|
154
|
+
)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
logger.exception(task.task_id)
|
|
157
|
+
error = e
|
|
158
|
+
success = False
|
|
159
|
+
logger.exception(
|
|
160
|
+
f"Exception executing task {task.task_id}, with nodes: {[item.name for item in task.nodes]}"
|
|
161
|
+
)
|
|
162
|
+
raise e
|
|
163
|
+
finally:
|
|
164
|
+
if task.adapter.does_hook("post_task_execute", is_async=False):
|
|
165
|
+
task.adapter.call_all_lifecycle_hooks_sync(
|
|
166
|
+
"post_task_execute",
|
|
167
|
+
run_id=task.run_id,
|
|
168
|
+
task_id=task.task_id,
|
|
169
|
+
nodes=task.nodes,
|
|
170
|
+
results=results,
|
|
171
|
+
success=success,
|
|
172
|
+
error=error,
|
|
173
|
+
spawning_task_id=task.spawning_task_id,
|
|
174
|
+
purpose=task.purpose,
|
|
175
|
+
)
|
|
176
|
+
# This selection is for GC
|
|
177
|
+
# We also need to get the override values
|
|
178
|
+
# This way if its overridden we can ensure it gets passed to the right one
|
|
179
|
+
final_retval = {
|
|
180
|
+
key: value
|
|
181
|
+
for key, value in results.items()
|
|
182
|
+
if key in task.outputs_to_compute or key in task.overrides
|
|
183
|
+
}
|
|
184
|
+
return final_retval
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclasses.dataclass
|
|
188
|
+
class TaskFutureWrappingFunction(TaskFuture):
|
|
189
|
+
"""Wraps a python function call in a TaskFuture."""
|
|
190
|
+
|
|
191
|
+
def __init__(self, function: Callable[[], Any]):
|
|
192
|
+
self.function = function
|
|
193
|
+
self._results = None
|
|
194
|
+
self._done = False
|
|
195
|
+
self._exception = None
|
|
196
|
+
|
|
197
|
+
def get_state(self):
|
|
198
|
+
if self._exception is not None:
|
|
199
|
+
return TaskState.FAILED
|
|
200
|
+
if not self._done:
|
|
201
|
+
try:
|
|
202
|
+
self._results = self.function()
|
|
203
|
+
except Exception as e:
|
|
204
|
+
logger.exception("Task failed")
|
|
205
|
+
self._exception = e
|
|
206
|
+
return TaskState.FAILED
|
|
207
|
+
finally:
|
|
208
|
+
self._done = True
|
|
209
|
+
return TaskState.SUCCESSFUL
|
|
210
|
+
|
|
211
|
+
def get_result(self):
|
|
212
|
+
if self._exception is not None:
|
|
213
|
+
raise self._exception
|
|
214
|
+
if not self._done:
|
|
215
|
+
self._results = self.function()
|
|
216
|
+
self._done = True
|
|
217
|
+
return self._results
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class SynchronousLocalTaskExecutor(TaskExecutor):
|
|
221
|
+
"""Basic synchronous/local task executor that runs tasks
|
|
222
|
+
in the same process, at submit time."""
|
|
223
|
+
|
|
224
|
+
def submit_task(self, task: TaskImplementation) -> TaskFuture:
|
|
225
|
+
"""Submitting a task is literally just running it.
|
|
226
|
+
|
|
227
|
+
:param task: Task to submit
|
|
228
|
+
:return: Future associated with this task
|
|
229
|
+
"""
|
|
230
|
+
return TaskFutureWrappingFunction(functools.partial(base_execute_task, task))
|
|
231
|
+
|
|
232
|
+
def can_submit_task(self) -> bool:
|
|
233
|
+
"""We can always submit a task as the task submission is blocking!
|
|
234
|
+
|
|
235
|
+
:return: True
|
|
236
|
+
"""
|
|
237
|
+
return True
|
|
238
|
+
|
|
239
|
+
def init(self):
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
def finalize(self):
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@dataclasses.dataclass
|
|
247
|
+
class TaskFutureWrappingPythonFuture(TaskFuture):
|
|
248
|
+
"""Wraps a python future in a TaskFuture"""
|
|
249
|
+
|
|
250
|
+
def __init__(self, future: Future):
|
|
251
|
+
self.future = future
|
|
252
|
+
|
|
253
|
+
def get_state(self):
|
|
254
|
+
"""Gets the state. This is non-blocking."""
|
|
255
|
+
|
|
256
|
+
if self.future.done():
|
|
257
|
+
try:
|
|
258
|
+
self.future.result()
|
|
259
|
+
except Exception:
|
|
260
|
+
logger.exception("Task failed")
|
|
261
|
+
return TaskState.FAILED
|
|
262
|
+
return TaskState.SUCCESSFUL
|
|
263
|
+
else:
|
|
264
|
+
return TaskState.RUNNING
|
|
265
|
+
|
|
266
|
+
def get_result(self):
|
|
267
|
+
"""Gets the result. This is non-blocking.
|
|
268
|
+
|
|
269
|
+
:return: None if there is no result, else the result
|
|
270
|
+
"""
|
|
271
|
+
if not self.future.done():
|
|
272
|
+
return None
|
|
273
|
+
out = self.future.result()
|
|
274
|
+
return out
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class PoolExecutor(TaskExecutor, abc.ABC):
|
|
278
|
+
"""Base class for a pool-based executor (threadpool executor/multiprocessing executor).
|
|
279
|
+
Handles common logic, stores active future, and manages max tasks.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
def __init__(self, max_tasks: int):
|
|
283
|
+
self.active_futures = []
|
|
284
|
+
self.initialized = False
|
|
285
|
+
self.pool = None
|
|
286
|
+
self.max_tasks = max_tasks # TODO -- allow infinite/no max.
|
|
287
|
+
|
|
288
|
+
def _prune_active_futures(self):
|
|
289
|
+
self.active_futures = [f for f in self.active_futures if not f.done()]
|
|
290
|
+
|
|
291
|
+
@abc.abstractmethod
|
|
292
|
+
def create_pool(self) -> Executor:
|
|
293
|
+
"""Creates a pool to submit tasks to.
|
|
294
|
+
|
|
295
|
+
:return: The executor to handle all the tasks in the pool
|
|
296
|
+
"""
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
def init(self):
|
|
300
|
+
"""Creates/initializes pool"""
|
|
301
|
+
if not self.initialized:
|
|
302
|
+
self.pool = self.create_pool()
|
|
303
|
+
self.initialized = True
|
|
304
|
+
else:
|
|
305
|
+
raise RuntimeError("Cannot initialize an already initialized executor")
|
|
306
|
+
|
|
307
|
+
def finalize(self):
|
|
308
|
+
"""Finalizes pool, freeing up resources"""
|
|
309
|
+
if self.initialized:
|
|
310
|
+
self.pool.shutdown()
|
|
311
|
+
self.initialized = False
|
|
312
|
+
else:
|
|
313
|
+
raise RuntimeError("Cannot finalize an uninitialized executor")
|
|
314
|
+
|
|
315
|
+
def submit_task(self, task: TaskImplementation) -> TaskFuture:
|
|
316
|
+
"""Submitting a task is literally just running it.
|
|
317
|
+
|
|
318
|
+
:param task: Task to submit
|
|
319
|
+
:return: The future associated with the task
|
|
320
|
+
"""
|
|
321
|
+
# First submit it
|
|
322
|
+
# Then we need to wrap it in a future
|
|
323
|
+
future = self.pool.submit(base_execute_task, task)
|
|
324
|
+
self.active_futures.append(future)
|
|
325
|
+
return TaskFutureWrappingPythonFuture(future)
|
|
326
|
+
|
|
327
|
+
def can_submit_task(self) -> bool:
|
|
328
|
+
"""Tells if the pool is full or not.
|
|
329
|
+
|
|
330
|
+
:return:
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
self._prune_active_futures()
|
|
334
|
+
return len(self.active_futures) < self.max_tasks
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class MultiThreadingExecutor(PoolExecutor):
|
|
338
|
+
"""Basic synchronous/local task executor that runs tasks
|
|
339
|
+
in the same process, at submit time."""
|
|
340
|
+
|
|
341
|
+
def create_pool(self) -> ThreadPoolExecutor:
|
|
342
|
+
return ThreadPoolExecutor(max_workers=self.max_tasks)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class MultiProcessingExecutor(PoolExecutor):
|
|
346
|
+
"""Basic synchronous/local task executor that runs tasks
|
|
347
|
+
in the same process, at submit time. Note that this is
|
|
348
|
+
not yet augmented to handle the right serialization,
|
|
349
|
+
so use at your own risk. We will be fixing shortly,
|
|
350
|
+
but the dask/ray parallelism and the multithreading
|
|
351
|
+
parallelism executors serialize correctly."""
|
|
352
|
+
|
|
353
|
+
def create_pool(self) -> ProcessPoolExecutor:
|
|
354
|
+
return ProcessPoolExecutor(max_workers=self.max_tasks)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class ExecutionManager(abc.ABC):
|
|
358
|
+
"""Manages execution per task. This enables you to have different executors for different
|
|
359
|
+
tasks/task types. Note that, currently, it just uses the task information, but we could
|
|
360
|
+
theoretically add metadata in a task as well.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
def __init__(self, executors: list[TaskExecutor]):
|
|
364
|
+
"""Initializes the execution manager. Note this does not start it up/claim resources --
|
|
365
|
+
you need to call init() to do that.
|
|
366
|
+
|
|
367
|
+
:param executors:
|
|
368
|
+
"""
|
|
369
|
+
self.executors = executors
|
|
370
|
+
|
|
371
|
+
def init(self):
|
|
372
|
+
"""Initializes each of the executors."""
|
|
373
|
+
for executor in self.executors:
|
|
374
|
+
executor.init()
|
|
375
|
+
|
|
376
|
+
def finalize(self):
|
|
377
|
+
"""Finalizes each of the executors."""
|
|
378
|
+
for executor in self.executors:
|
|
379
|
+
executor.finalize()
|
|
380
|
+
|
|
381
|
+
@abc.abstractmethod
|
|
382
|
+
def get_executor_for_task(self, task: TaskImplementation) -> TaskExecutor:
|
|
383
|
+
"""Selects the executor for the task. This enables us to set the appropriate executor
|
|
384
|
+
for specific tasks (so that we can run some locally, some remotely, etc...).
|
|
385
|
+
|
|
386
|
+
Note that this is the power-user case -- in all likelihood, people will use the default
|
|
387
|
+
ExecutionManager.
|
|
388
|
+
|
|
389
|
+
:param task: Task to choose execution manager for
|
|
390
|
+
:return: The executor to use for this task
|
|
391
|
+
"""
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class DefaultExecutionManager(ExecutionManager):
|
|
396
|
+
def __init__(self, local_executor=None, remote_executor=None):
|
|
397
|
+
"""Instantiates a BasicExecutionManager with a local/remote executor.
|
|
398
|
+
These enable us to run certain tasks locally (simple transformations, generating sets of files),
|
|
399
|
+
and certain tasks remotely (processing files in large datasets, etc...)
|
|
400
|
+
|
|
401
|
+
:param local_executor: Executor to use for running tasks locally
|
|
402
|
+
:param remote_executor: Executor to use for running tasks remotely
|
|
403
|
+
"""
|
|
404
|
+
if local_executor is None:
|
|
405
|
+
local_executor = SynchronousLocalTaskExecutor()
|
|
406
|
+
if remote_executor is None:
|
|
407
|
+
remote_executor = MultiProcessingExecutor(max_tasks=5)
|
|
408
|
+
super().__init__([local_executor, remote_executor])
|
|
409
|
+
self.local_executor = local_executor
|
|
410
|
+
self.remote_executor = remote_executor
|
|
411
|
+
|
|
412
|
+
def get_executor_for_task(self, task: TaskImplementation) -> TaskExecutor:
|
|
413
|
+
"""Simple implementation that returns the local executor for single task executions,
|
|
414
|
+
|
|
415
|
+
:param task: Task to get executor for
|
|
416
|
+
:return: A local task if this is a "single-node" task, a remote task otherwise
|
|
417
|
+
"""
|
|
418
|
+
|
|
419
|
+
if task.purpose == NodeGroupPurpose.EXECUTE_BLOCK:
|
|
420
|
+
return self.remote_executor
|
|
421
|
+
return self.local_executor
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def run_graph_to_completion(
|
|
425
|
+
execution_state: ExecutionState,
|
|
426
|
+
execution_manager: ExecutionManager,
|
|
427
|
+
):
|
|
428
|
+
"""Blocking call to run the graph until it is complete. Note that this employs a while loop.
|
|
429
|
+
With the way we handle futures, we should be able to have this event-driven, allowing us
|
|
430
|
+
to only query when the state is updated, and use that to trigger a new update.
|
|
431
|
+
|
|
432
|
+
For now, the while loop is fine.
|
|
433
|
+
|
|
434
|
+
:return: Nothing, the execution state/result cache can give us the data
|
|
435
|
+
"""
|
|
436
|
+
task_futures = {}
|
|
437
|
+
execution_manager.init()
|
|
438
|
+
try:
|
|
439
|
+
while not GraphState.is_terminal(execution_state.get_graph_state()):
|
|
440
|
+
# Get the next task from the queue
|
|
441
|
+
next_task = execution_state.release_next_task()
|
|
442
|
+
if next_task is not None:
|
|
443
|
+
task_executor = execution_manager.get_executor_for_task(next_task)
|
|
444
|
+
if task_executor.can_submit_task():
|
|
445
|
+
if next_task.adapter.does_hook("pre_task_submission", is_async=False):
|
|
446
|
+
next_task.adapter.call_all_lifecycle_hooks_sync(
|
|
447
|
+
"pre_task_submission",
|
|
448
|
+
run_id=next_task.run_id,
|
|
449
|
+
task_id=next_task.task_id,
|
|
450
|
+
nodes=next_task.nodes,
|
|
451
|
+
inputs=next_task.dynamic_inputs,
|
|
452
|
+
overrides=next_task.overrides,
|
|
453
|
+
spawning_task_id=next_task.spawning_task_id,
|
|
454
|
+
purpose=next_task.purpose,
|
|
455
|
+
)
|
|
456
|
+
try:
|
|
457
|
+
submitted = task_executor.submit_task(next_task)
|
|
458
|
+
except Exception as e:
|
|
459
|
+
logger.exception(
|
|
460
|
+
f"Exception submitting task {next_task.task_id}, with nodes: "
|
|
461
|
+
f"{[item.name for item in next_task.nodes]}"
|
|
462
|
+
)
|
|
463
|
+
raise e
|
|
464
|
+
task_futures[next_task] = submitted
|
|
465
|
+
else:
|
|
466
|
+
# Whoops, back on the queue. We should probably wait a bit here, but for
|
|
467
|
+
# now we're going to keep burning through
|
|
468
|
+
execution_state.reject_task(task_to_reject=next_task)
|
|
469
|
+
|
|
470
|
+
# Update all the tasks in flight (copy so we can modify)
|
|
471
|
+
for task, task_future in task_futures.copy().items():
|
|
472
|
+
result, error = None, None
|
|
473
|
+
state = task_future.get_state()
|
|
474
|
+
try:
|
|
475
|
+
result = task_future.get_result()
|
|
476
|
+
except Exception as e:
|
|
477
|
+
logger.exception(
|
|
478
|
+
f"Exception resolving task {task.task_id}, with nodes: "
|
|
479
|
+
f"{[item.name for item in task.nodes]}"
|
|
480
|
+
)
|
|
481
|
+
error = e
|
|
482
|
+
finally:
|
|
483
|
+
execution_state.update_task_state(task.task_id, state, result)
|
|
484
|
+
if TaskState.is_terminal(state):
|
|
485
|
+
if task.adapter.does_hook("post_task_return", is_async=False):
|
|
486
|
+
task.adapter.call_all_lifecycle_hooks_sync(
|
|
487
|
+
"post_task_return",
|
|
488
|
+
run_id=task.run_id,
|
|
489
|
+
task_id=task.task_id,
|
|
490
|
+
nodes=task.nodes,
|
|
491
|
+
success=state == TaskState.SUCCESSFUL,
|
|
492
|
+
error=error,
|
|
493
|
+
result=result,
|
|
494
|
+
spawning_task_id=task.spawning_task_id,
|
|
495
|
+
purpose=task.purpose,
|
|
496
|
+
)
|
|
497
|
+
del task_futures[task]
|
|
498
|
+
if error:
|
|
499
|
+
raise error
|
|
500
|
+
logger.info(f"Graph is done, graph state is {execution_state.get_graph_state()}")
|
|
501
|
+
finally:
|
|
502
|
+
execution_manager.finalize()
|