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.
Files changed (151) hide show
  1. apache_hamilton-1.90.0.dev0.dist-info/METADATA +407 -0
  2. apache_hamilton-1.90.0.dev0.dist-info/RECORD +151 -0
  3. apache_hamilton-1.90.0.dev0.dist-info/WHEEL +4 -0
  4. apache_hamilton-1.90.0.dev0.dist-info/entry_points.txt +9 -0
  5. apache_hamilton-1.90.0.dev0.dist-info/licenses/DISCLAIMER +10 -0
  6. apache_hamilton-1.90.0.dev0.dist-info/licenses/LICENSE +228 -0
  7. apache_hamilton-1.90.0.dev0.dist-info/licenses/NOTICE +5 -0
  8. hamilton/__init__.py +24 -0
  9. hamilton/ad_hoc_utils.py +132 -0
  10. hamilton/async_driver.py +465 -0
  11. hamilton/base.py +466 -0
  12. hamilton/caching/__init__.py +16 -0
  13. hamilton/caching/adapter.py +1475 -0
  14. hamilton/caching/cache_key.py +70 -0
  15. hamilton/caching/fingerprinting.py +287 -0
  16. hamilton/caching/stores/__init__.py +16 -0
  17. hamilton/caching/stores/base.py +242 -0
  18. hamilton/caching/stores/file.py +140 -0
  19. hamilton/caching/stores/memory.py +297 -0
  20. hamilton/caching/stores/sqlite.py +282 -0
  21. hamilton/caching/stores/utils.py +40 -0
  22. hamilton/cli/__init__.py +16 -0
  23. hamilton/cli/__main__.py +328 -0
  24. hamilton/cli/commands.py +126 -0
  25. hamilton/cli/logic.py +338 -0
  26. hamilton/common/__init__.py +76 -0
  27. hamilton/contrib/__init__.py +41 -0
  28. hamilton/data_quality/__init__.py +16 -0
  29. hamilton/data_quality/base.py +198 -0
  30. hamilton/data_quality/default_validators.py +560 -0
  31. hamilton/data_quality/pandera_validators.py +121 -0
  32. hamilton/dataflows/__init__.py +726 -0
  33. hamilton/dataflows/template/README.md +25 -0
  34. hamilton/dataflows/template/__init__.py +54 -0
  35. hamilton/dataflows/template/author.md +28 -0
  36. hamilton/dataflows/template/requirements.txt +0 -0
  37. hamilton/dataflows/template/tags.json +7 -0
  38. hamilton/dataflows/template/valid_configs.jsonl +1 -0
  39. hamilton/dev_utils/__init__.py +16 -0
  40. hamilton/dev_utils/deprecation.py +204 -0
  41. hamilton/driver.py +2112 -0
  42. hamilton/execution/__init__.py +16 -0
  43. hamilton/execution/debugging_utils.py +56 -0
  44. hamilton/execution/executors.py +502 -0
  45. hamilton/execution/graph_functions.py +421 -0
  46. hamilton/execution/grouping.py +430 -0
  47. hamilton/execution/state.py +539 -0
  48. hamilton/experimental/__init__.py +27 -0
  49. hamilton/experimental/databackend.py +61 -0
  50. hamilton/experimental/decorators/__init__.py +16 -0
  51. hamilton/experimental/decorators/parameterize_frame.py +233 -0
  52. hamilton/experimental/h_async.py +29 -0
  53. hamilton/experimental/h_cache.py +413 -0
  54. hamilton/experimental/h_dask.py +28 -0
  55. hamilton/experimental/h_databackends.py +174 -0
  56. hamilton/experimental/h_ray.py +28 -0
  57. hamilton/experimental/h_spark.py +32 -0
  58. hamilton/function_modifiers/README +40 -0
  59. hamilton/function_modifiers/__init__.py +121 -0
  60. hamilton/function_modifiers/adapters.py +900 -0
  61. hamilton/function_modifiers/base.py +859 -0
  62. hamilton/function_modifiers/configuration.py +310 -0
  63. hamilton/function_modifiers/delayed.py +202 -0
  64. hamilton/function_modifiers/dependencies.py +246 -0
  65. hamilton/function_modifiers/expanders.py +1230 -0
  66. hamilton/function_modifiers/macros.py +1634 -0
  67. hamilton/function_modifiers/metadata.py +434 -0
  68. hamilton/function_modifiers/recursive.py +908 -0
  69. hamilton/function_modifiers/validation.py +289 -0
  70. hamilton/function_modifiers_base.py +31 -0
  71. hamilton/graph.py +1153 -0
  72. hamilton/graph_types.py +264 -0
  73. hamilton/graph_utils.py +41 -0
  74. hamilton/htypes.py +450 -0
  75. hamilton/io/__init__.py +32 -0
  76. hamilton/io/data_adapters.py +216 -0
  77. hamilton/io/default_data_loaders.py +224 -0
  78. hamilton/io/materialization.py +500 -0
  79. hamilton/io/utils.py +158 -0
  80. hamilton/lifecycle/__init__.py +67 -0
  81. hamilton/lifecycle/api.py +833 -0
  82. hamilton/lifecycle/base.py +1130 -0
  83. hamilton/lifecycle/default.py +802 -0
  84. hamilton/log_setup.py +47 -0
  85. hamilton/models.py +92 -0
  86. hamilton/node.py +449 -0
  87. hamilton/plugins/README.md +48 -0
  88. hamilton/plugins/__init__.py +16 -0
  89. hamilton/plugins/dask_extensions.py +47 -0
  90. hamilton/plugins/dlt_extensions.py +161 -0
  91. hamilton/plugins/geopandas_extensions.py +49 -0
  92. hamilton/plugins/h_dask.py +331 -0
  93. hamilton/plugins/h_ddog.py +522 -0
  94. hamilton/plugins/h_diskcache.py +163 -0
  95. hamilton/plugins/h_experiments/__init__.py +22 -0
  96. hamilton/plugins/h_experiments/__main__.py +62 -0
  97. hamilton/plugins/h_experiments/cache.py +39 -0
  98. hamilton/plugins/h_experiments/data_model.py +68 -0
  99. hamilton/plugins/h_experiments/hook.py +219 -0
  100. hamilton/plugins/h_experiments/server.py +375 -0
  101. hamilton/plugins/h_kedro.py +152 -0
  102. hamilton/plugins/h_logging.py +454 -0
  103. hamilton/plugins/h_mcp/__init__.py +28 -0
  104. hamilton/plugins/h_mcp/__main__.py +33 -0
  105. hamilton/plugins/h_mcp/_helpers.py +129 -0
  106. hamilton/plugins/h_mcp/_templates.py +417 -0
  107. hamilton/plugins/h_mcp/server.py +328 -0
  108. hamilton/plugins/h_mlflow.py +335 -0
  109. hamilton/plugins/h_narwhals.py +134 -0
  110. hamilton/plugins/h_openlineage.py +400 -0
  111. hamilton/plugins/h_opentelemetry.py +167 -0
  112. hamilton/plugins/h_pandas.py +257 -0
  113. hamilton/plugins/h_pandera.py +117 -0
  114. hamilton/plugins/h_polars.py +304 -0
  115. hamilton/plugins/h_polars_lazyframe.py +282 -0
  116. hamilton/plugins/h_pyarrow.py +56 -0
  117. hamilton/plugins/h_pydantic.py +127 -0
  118. hamilton/plugins/h_ray.py +242 -0
  119. hamilton/plugins/h_rich.py +142 -0
  120. hamilton/plugins/h_schema.py +493 -0
  121. hamilton/plugins/h_slack.py +100 -0
  122. hamilton/plugins/h_spark.py +1380 -0
  123. hamilton/plugins/h_threadpool.py +125 -0
  124. hamilton/plugins/h_tqdm.py +122 -0
  125. hamilton/plugins/h_vaex.py +129 -0
  126. hamilton/plugins/huggingface_extensions.py +236 -0
  127. hamilton/plugins/ibis_extensions.py +93 -0
  128. hamilton/plugins/jupyter_magic.py +622 -0
  129. hamilton/plugins/kedro_extensions.py +117 -0
  130. hamilton/plugins/lightgbm_extensions.py +99 -0
  131. hamilton/plugins/matplotlib_extensions.py +108 -0
  132. hamilton/plugins/mlflow_extensions.py +216 -0
  133. hamilton/plugins/numpy_extensions.py +105 -0
  134. hamilton/plugins/pandas_extensions.py +1763 -0
  135. hamilton/plugins/plotly_extensions.py +150 -0
  136. hamilton/plugins/polars_extensions.py +71 -0
  137. hamilton/plugins/polars_implementations.py +25 -0
  138. hamilton/plugins/polars_lazyframe_extensions.py +302 -0
  139. hamilton/plugins/polars_post_1_0_0_extensions.py +877 -0
  140. hamilton/plugins/polars_pre_1_0_0_extension.py +836 -0
  141. hamilton/plugins/pydantic_extensions.py +98 -0
  142. hamilton/plugins/pyspark_pandas_extensions.py +47 -0
  143. hamilton/plugins/sklearn_plot_extensions.py +129 -0
  144. hamilton/plugins/spark_extensions.py +105 -0
  145. hamilton/plugins/vaex_extensions.py +51 -0
  146. hamilton/plugins/xgboost_extensions.py +91 -0
  147. hamilton/plugins/yaml_extensions.py +89 -0
  148. hamilton/registry.py +254 -0
  149. hamilton/settings.py +18 -0
  150. hamilton/telemetry.py +50 -0
  151. 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