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,430 @@
|
|
|
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 enum
|
|
21
|
+
from collections import defaultdict
|
|
22
|
+
from collections.abc import Collection
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from hamilton import node
|
|
26
|
+
from hamilton.execution import graph_functions
|
|
27
|
+
from hamilton.execution.graph_functions import get_node_levels, topologically_sort_nodes
|
|
28
|
+
from hamilton.lifecycle import base as lifecycle_base
|
|
29
|
+
from hamilton.node import Node, NodeType
|
|
30
|
+
|
|
31
|
+
"""General utilities for grouping nodes in a DAG"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class NodeGroupPurpose(enum.Enum):
|
|
35
|
+
"""Node groups have a purpose. The standard type is "EXECUTE_BLOCK",
|
|
36
|
+
which just executes a subdag. The other types are for creating "expanders",
|
|
37
|
+
which can dictate how subsequent nodes expand into a subdag.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
EXPAND_UNORDERED = "expand_unordered" # DAG that ends in a parameter for the next subdag to
|
|
42
|
+
GATHER = "gather" # DAG that begins in the collection of a parallel block
|
|
43
|
+
EXECUTE_BLOCK = "execute_block" # DAG that is a standard block of nodes
|
|
44
|
+
EXECUTE_SINGLE = "execute_single" # DAG that is just a single node
|
|
45
|
+
|
|
46
|
+
def is_expander(self):
|
|
47
|
+
return self.value == "expand_unordered"
|
|
48
|
+
|
|
49
|
+
def is_gatherer(self):
|
|
50
|
+
return self.value == "gather"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclasses.dataclass
|
|
54
|
+
class NodeGroup:
|
|
55
|
+
"""Represents a simple grouping of nodes into a single unit.
|
|
56
|
+
This has some utility functions to determine inputs, outputs, etc...
|
|
57
|
+
Note that this has the following properties:
|
|
58
|
+
1. ID is unique across all node groups
|
|
59
|
+
2. Nodes are unique across all node groups (one node to group)
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
base_id: str # Unique ID for node group.
|
|
63
|
+
spawning_task_base_id: str | None
|
|
64
|
+
nodes: list[Node]
|
|
65
|
+
purpose: NodeGroupPurpose # TODO -- derive this (or not?)
|
|
66
|
+
# set of available nodes by this task for querying
|
|
67
|
+
available_nodes: set[str] = dataclasses.field(init=False)
|
|
68
|
+
|
|
69
|
+
def __post_init__(self):
|
|
70
|
+
self.available_nodes = {node_.name for node_ in self.nodes}
|
|
71
|
+
|
|
72
|
+
def __hash__(self):
|
|
73
|
+
return hash(self.base_id)
|
|
74
|
+
|
|
75
|
+
def __eq__(self, other):
|
|
76
|
+
return self.base_id == other.base_id
|
|
77
|
+
|
|
78
|
+
def get_expander_node(self) -> Node | None:
|
|
79
|
+
"""Returns the expander node for this node group, if it exists"""
|
|
80
|
+
candidates = [n for n in self.nodes if n.node_role == NodeType.EXPAND]
|
|
81
|
+
if candidates:
|
|
82
|
+
return candidates[0]
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
def get_collector_node(self) -> Node | None:
|
|
86
|
+
"""Returns the collector node for this node group, if it exists"""
|
|
87
|
+
candidates = [n for n in self.nodes if n.node_role == NodeType.COLLECT]
|
|
88
|
+
if candidates:
|
|
89
|
+
return candidates[0]
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
def produces(self, node_name: str) -> bool:
|
|
93
|
+
return node_name in self.available_nodes
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclasses.dataclass
|
|
97
|
+
class TaskSpec(NodeGroup):
|
|
98
|
+
"""Represents the spec for a task. This has no actual input/output data associated with it,
|
|
99
|
+
but has information needed to know how one might execute.
|
|
100
|
+
|
|
101
|
+
You can think of this as a "template" for a task, where the inputs are not yet known.
|
|
102
|
+
Furthermore, there could be multiple instances of this task spec, each with different inputs.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
outputs_to_compute: Collection[str] # list of output names to compute
|
|
106
|
+
overrides: dict[str, Any] # overrides for the task, fixed at the time of creation
|
|
107
|
+
adapter: lifecycle_base.LifecycleAdapterSet
|
|
108
|
+
base_dependencies: list[str] # list of tasks that must be completed before this task can run
|
|
109
|
+
|
|
110
|
+
def get_input_vars(self) -> tuple[list[str], list[str]]:
|
|
111
|
+
"""Returns the node-level dependencies for this node group.
|
|
112
|
+
This is all of the sources in the subdag.
|
|
113
|
+
|
|
114
|
+
:return: A tuple consisting of (a) the list of required inputs variables and (b)
|
|
115
|
+
the list of optional input variables
|
|
116
|
+
"""
|
|
117
|
+
all_node_names = {n.name for n in self.nodes}
|
|
118
|
+
required_variables = set()
|
|
119
|
+
optional_variables = set()
|
|
120
|
+
for node_ in self.nodes:
|
|
121
|
+
if node_.name in self.overrides:
|
|
122
|
+
continue
|
|
123
|
+
if node_.user_defined:
|
|
124
|
+
# if the node is user-defined then that
|
|
125
|
+
# means we need it for the execution
|
|
126
|
+
required_variables.add(node_.name)
|
|
127
|
+
for dependency in node_.dependencies:
|
|
128
|
+
if dependency.name not in self.overrides:
|
|
129
|
+
if dependency.user_defined or dependency.name not in all_node_names:
|
|
130
|
+
if node_.requires(dependency.name):
|
|
131
|
+
optional_variables.discard(dependency.name)
|
|
132
|
+
required_variables.add(dependency.name)
|
|
133
|
+
else:
|
|
134
|
+
optional_variables.add(dependency.name)
|
|
135
|
+
|
|
136
|
+
return list(required_variables), list(optional_variables)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclasses.dataclass
|
|
140
|
+
class TaskImplementation(TaskSpec):
|
|
141
|
+
"""Represents the "execution ready" spec for a task. This contains two essential pieces of information:
|
|
142
|
+
|
|
143
|
+
1. The inputs/outputs
|
|
144
|
+
2. The "spawning" task that spawned this
|
|
145
|
+
|
|
146
|
+
Thus the task_id is unique (as it contains the spawning task's ID + the group ID), but the base_id is not, as
|
|
147
|
+
we could have multiple versions of this task running in parallel.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
# task whose result spawned these tasks
|
|
151
|
+
# If this is none, it means the graph itself spawned these tasks
|
|
152
|
+
group_id: str | None
|
|
153
|
+
realized_dependencies: dict[str, list[str]] # realized dependencies are the actual dependencies
|
|
154
|
+
# Note that these are lists as we have "gather" operations
|
|
155
|
+
spawning_task_id: str | None # task that spawned this task
|
|
156
|
+
task_id: str = dataclasses.field(init=False)
|
|
157
|
+
dynamic_inputs: dict[str, Any] = dataclasses.field(default_factory=dict)
|
|
158
|
+
run_id: str = dataclasses.field(default_factory=str)
|
|
159
|
+
|
|
160
|
+
def bind(self, dynamic_inputs: dict[str, Any]) -> "TaskImplementation":
|
|
161
|
+
"""Binds dynamic inputs to the task spec, returning a new task spec"""
|
|
162
|
+
return dataclasses.replace(self, dynamic_inputs={**dynamic_inputs, **self.dynamic_inputs})
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def determine_task_id(base_id: str, spawning_task: str | None, group_id: str | None):
|
|
166
|
+
return ".".join(
|
|
167
|
+
filter(lambda i: i is not None, [spawning_task, group_id, base_id])
|
|
168
|
+
) # This will do for now...
|
|
169
|
+
|
|
170
|
+
def __post_init__(self):
|
|
171
|
+
super(TaskImplementation, self).__post_init__()
|
|
172
|
+
self.task_id = self.determine_task_id(self.base_id, self.spawning_task_id, self.group_id)
|
|
173
|
+
|
|
174
|
+
def __hash__(self) -> int:
|
|
175
|
+
return hash(self.task_id)
|
|
176
|
+
|
|
177
|
+
def __eq__(self, other: object) -> bool:
|
|
178
|
+
if not isinstance(other, TaskImplementation):
|
|
179
|
+
return False
|
|
180
|
+
return self.task_id == other.task_id
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class GroupingStrategy(abc.ABC):
|
|
184
|
+
"""Base class for grouping nodes"""
|
|
185
|
+
|
|
186
|
+
@abc.abstractmethod
|
|
187
|
+
def group_nodes(self, nodes: list[node.Node]) -> list[NodeGroup]:
|
|
188
|
+
"""Groups nodes into a list of node groups"""
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class GroupByRepeatableBlocks(GroupingStrategy):
|
|
193
|
+
"""Groups nodes by repeatable blocks. For every set of nodes betweeen
|
|
194
|
+
a "parallel[]" and a "collect[]", we place them in a "block", which is its own group.
|
|
195
|
+
Note that this is specifically built to allow parallel/remote execution of blocks, with local
|
|
196
|
+
execution of non-parallel groups (that might generate/read from the blocks).
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def nodes_after_last_expand_block(
|
|
201
|
+
collect_node: node.Node,
|
|
202
|
+
) -> tuple[node.Node, list[node.Node]]:
|
|
203
|
+
"""Utility function to yield all nodes between a start and an end node.
|
|
204
|
+
This returns all nodes for which the following conditions are met:
|
|
205
|
+
|
|
206
|
+
1. It contains the start node as an ancestor
|
|
207
|
+
2. It contains a node that matches the terminate condition as a dependent
|
|
208
|
+
|
|
209
|
+
:param collect_node: Collector node to trace from
|
|
210
|
+
:return: The nodes in between start and end
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
def is_expander(node_: node.Node) -> bool:
|
|
214
|
+
return node_.node_role == NodeType.EXPAND
|
|
215
|
+
|
|
216
|
+
return graph_functions.nodes_between(collect_node, is_expander)
|
|
217
|
+
|
|
218
|
+
def group_nodes(self, nodes: list[node.Node]) -> list[NodeGroup]:
|
|
219
|
+
"""Groups nodes into blocks. This works as follows:
|
|
220
|
+
1. Fina all the Parallelizable[] nodes in the DAG
|
|
221
|
+
2. For each of those, do a DFS until the next Collect[] node
|
|
222
|
+
3. Create a group for each of those
|
|
223
|
+
4. Add all other nodes to their own group
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
:param nodes:
|
|
227
|
+
:return:
|
|
228
|
+
"""
|
|
229
|
+
collectors = [node_ for node_ in nodes if node_.node_role == NodeType.COLLECT]
|
|
230
|
+
groups = []
|
|
231
|
+
visited = set()
|
|
232
|
+
# We have to do this as we may have overrides to handle
|
|
233
|
+
node_names = {node_.name for node_ in nodes}
|
|
234
|
+
for collector in collectors:
|
|
235
|
+
expander, nodes_in_block = self.nodes_after_last_expand_block(collector)
|
|
236
|
+
# TODO -- add error message for conflicting groups...
|
|
237
|
+
# if expander in visited:
|
|
238
|
+
# raise ValueError(f"Multiple collect nodes cannot trace "
|
|
239
|
+
# f"back to the same expander. ")
|
|
240
|
+
expander_name = f"expand-{expander.name}"
|
|
241
|
+
if expander.name in node_names:
|
|
242
|
+
groups.append(
|
|
243
|
+
NodeGroup(
|
|
244
|
+
base_id=f"expand-{expander.name}",
|
|
245
|
+
spawning_task_base_id=None,
|
|
246
|
+
nodes=[expander],
|
|
247
|
+
purpose=NodeGroupPurpose.EXPAND_UNORDERED,
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
# In thie case of a strange override, we may end up with this
|
|
251
|
+
# breaking, as a node in the block could be missing
|
|
252
|
+
# This is an undefined case, but we'll likely end up with an error
|
|
253
|
+
groups.append(
|
|
254
|
+
NodeGroup(
|
|
255
|
+
base_id=f"block-{expander.name}",
|
|
256
|
+
spawning_task_base_id=expander_name,
|
|
257
|
+
nodes=nodes_in_block,
|
|
258
|
+
purpose=NodeGroupPurpose.EXECUTE_BLOCK,
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
visited.update(nodes_in_block + [expander])
|
|
262
|
+
groups.append(
|
|
263
|
+
NodeGroup(
|
|
264
|
+
base_id=f"collect-{expander.name}",
|
|
265
|
+
spawning_task_base_id=expander_name,
|
|
266
|
+
nodes=[collector],
|
|
267
|
+
purpose=NodeGroupPurpose.GATHER,
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
visited.update([collector])
|
|
271
|
+
|
|
272
|
+
remaining_nodes = [node_ for node_ in nodes if node_ not in visited]
|
|
273
|
+
for node_ in remaining_nodes:
|
|
274
|
+
groups.append(
|
|
275
|
+
NodeGroup(
|
|
276
|
+
base_id=node_.name,
|
|
277
|
+
spawning_task_base_id=None, # Nothing spawns this task
|
|
278
|
+
nodes=[node_],
|
|
279
|
+
purpose=NodeGroupPurpose.EXECUTE_SINGLE,
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
return groups
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def convert_node_type_to_group_purpose(node_type: NodeType) -> NodeGroupPurpose:
|
|
286
|
+
"""Utility type to map node type to group purpose. Note its not 1:1, but its pretty close."""
|
|
287
|
+
if node_type == NodeType.EXPAND:
|
|
288
|
+
return NodeGroupPurpose.EXPAND_UNORDERED
|
|
289
|
+
if node_type == NodeType.COLLECT:
|
|
290
|
+
return NodeGroupPurpose.GATHER
|
|
291
|
+
return NodeGroupPurpose.EXECUTE_BLOCK
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class GroupNodesIndividually(GroupingStrategy):
|
|
295
|
+
"""Groups nodes into individual blocks."""
|
|
296
|
+
|
|
297
|
+
def group_nodes(self, nodes: list[node.Node]):
|
|
298
|
+
return [
|
|
299
|
+
NodeGroup(
|
|
300
|
+
base_id=node_.name,
|
|
301
|
+
nodes=[node_],
|
|
302
|
+
purpose=convert_node_type_to_group_purpose(node_.node_role),
|
|
303
|
+
spawning_task_base_id=None,
|
|
304
|
+
)
|
|
305
|
+
for node_ in nodes
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class GroupNodesAllAsOne(GroupingStrategy):
|
|
310
|
+
"""Groups nodes all into one block. TODO -- add validation."""
|
|
311
|
+
|
|
312
|
+
def group_nodes(self, nodes: list[node.Node]):
|
|
313
|
+
return [
|
|
314
|
+
NodeGroup(
|
|
315
|
+
base_id="root",
|
|
316
|
+
spawning_task_base_id=None,
|
|
317
|
+
nodes=nodes,
|
|
318
|
+
purpose=convert_node_type_to_group_purpose(nodes[0].node_role),
|
|
319
|
+
)
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class GroupNodesByLevel(GroupingStrategy):
|
|
324
|
+
def group_nodes(self, nodes: list[node.Node]) -> list[NodeGroup]:
|
|
325
|
+
in_order = topologically_sort_nodes(nodes)
|
|
326
|
+
node_levels = get_node_levels(in_order)
|
|
327
|
+
nodes_by_level = defaultdict(list)
|
|
328
|
+
nodes_by_name = {node_.name: node_ for node_ in nodes}
|
|
329
|
+
for node_name, level in node_levels.items():
|
|
330
|
+
nodes_by_level[level].append(nodes_by_name[node_name])
|
|
331
|
+
out = []
|
|
332
|
+
for i in range(len(nodes_by_level)):
|
|
333
|
+
nodes = nodes_by_level[i]
|
|
334
|
+
out.append(
|
|
335
|
+
NodeGroup(
|
|
336
|
+
base_id=f"level_{i}",
|
|
337
|
+
spawning_task_base_id=None,
|
|
338
|
+
nodes=nodes,
|
|
339
|
+
purpose=NodeGroupPurpose.EXECUTE_BLOCK,
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
return out
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def create_task_plan(
|
|
346
|
+
node_groups: list[NodeGroup],
|
|
347
|
+
outputs: list[str],
|
|
348
|
+
overrides: dict[str, Any],
|
|
349
|
+
adapter: lifecycle_base.LifecycleAdapterSet,
|
|
350
|
+
) -> list[TaskSpec]:
|
|
351
|
+
"""Creates tasks from node groups. This occurs after we group and after execute() is called in
|
|
352
|
+
the driver. Knowing what the user wants, we can finally create the tasks.
|
|
353
|
+
|
|
354
|
+
This does the following:
|
|
355
|
+
|
|
356
|
+
1. Determines any tasks that are not in the critical path, deletes them
|
|
357
|
+
2. For every task in the critical path
|
|
358
|
+
- Sets the outputs required from that task (all user-specified outputs that reside in that task)
|
|
359
|
+
- Instantiates and returns the task
|
|
360
|
+
|
|
361
|
+
:param node_groups: Groups of nodes to form tasks
|
|
362
|
+
:param outputs: output nodes the user wants
|
|
363
|
+
:param overrides: Overrides that short-circuit the execution
|
|
364
|
+
:param adapter: LifecycleAdapterSet to use for execution
|
|
365
|
+
:return: A list of task specs that we will execute later
|
|
366
|
+
"""
|
|
367
|
+
|
|
368
|
+
outputs_set = set(outputs)
|
|
369
|
+
out = []
|
|
370
|
+
node_to_task_map = {}
|
|
371
|
+
for node_group in node_groups:
|
|
372
|
+
outputs = []
|
|
373
|
+
nodes_in_group = set([node_.name for node_ in node_group.nodes])
|
|
374
|
+
for node_ in node_group.nodes:
|
|
375
|
+
# If this is overridden we can skip it
|
|
376
|
+
# Note there may be a strange bug with grouping/overrides (E.G. downstream of overrides)
|
|
377
|
+
if node_.name in overrides:
|
|
378
|
+
continue
|
|
379
|
+
node_to_task_map[node_.name] = node_group
|
|
380
|
+
is_output = False
|
|
381
|
+
if node_.name in outputs_set:
|
|
382
|
+
is_output = True
|
|
383
|
+
for depended_on_by in node_.depended_on_by:
|
|
384
|
+
if (
|
|
385
|
+
depended_on_by.name not in overrides
|
|
386
|
+
and depended_on_by.name not in nodes_in_group
|
|
387
|
+
):
|
|
388
|
+
is_output = True
|
|
389
|
+
break
|
|
390
|
+
if is_output:
|
|
391
|
+
outputs.append(node_.name)
|
|
392
|
+
|
|
393
|
+
task_spec = TaskSpec(
|
|
394
|
+
base_id=node_group.base_id,
|
|
395
|
+
spawning_task_base_id=node_group.spawning_task_base_id,
|
|
396
|
+
nodes=node_group.nodes,
|
|
397
|
+
purpose=node_group.purpose,
|
|
398
|
+
outputs_to_compute=outputs,
|
|
399
|
+
adapter=adapter,
|
|
400
|
+
overrides={
|
|
401
|
+
node_.name: overrides[node_.name]
|
|
402
|
+
for node_ in node_group.nodes
|
|
403
|
+
if node_.name in overrides
|
|
404
|
+
},
|
|
405
|
+
base_dependencies=[],
|
|
406
|
+
)
|
|
407
|
+
out.append(task_spec)
|
|
408
|
+
# Now we need to go through and add dependencies to each task
|
|
409
|
+
# We should be able to do this in the single pass if we guarenteed topological order,
|
|
410
|
+
# but for now we're doing this in the second pass
|
|
411
|
+
for task_spec in out:
|
|
412
|
+
task_dependencies = set()
|
|
413
|
+
nodes_in_group = set([node_.name for node_ in task_spec.nodes])
|
|
414
|
+
for node_ in task_spec.nodes:
|
|
415
|
+
if node_.name in overrides:
|
|
416
|
+
continue
|
|
417
|
+
for dependency in node_.dependencies:
|
|
418
|
+
if dependency.name not in overrides and dependency.name not in nodes_in_group:
|
|
419
|
+
task_containing_dependency = node_to_task_map.get(dependency.name)
|
|
420
|
+
# If its optional, we don't need to add it as a dependency
|
|
421
|
+
if task_containing_dependency is None and not node_.requires(dependency.name):
|
|
422
|
+
continue
|
|
423
|
+
if task_containing_dependency is None:
|
|
424
|
+
raise ValueError(
|
|
425
|
+
f"Dependency is None and requires {dependency.name}."
|
|
426
|
+
f"We're somehow in a bad state. Please reach out on slack."
|
|
427
|
+
)
|
|
428
|
+
task_dependencies.add(task_containing_dependency.base_id)
|
|
429
|
+
task_spec.base_dependencies = list(task_dependencies)
|
|
430
|
+
return out
|