blob: 252c84a4cb49534a72f77a9ce3e82aad8cbf1144 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import dataclasses
import functools
import inspect
from collections.abc import Callable
from types import ModuleType
from typing import Any
from hamilton import node
from hamilton.execution.grouping import NodeGroupPurpose
from hamilton.graph import FunctionGraph
from hamilton.lifecycle.base import (
BaseDoBuildResult,
BaseDoNodeExecute,
BaseDoRemoteExecute,
BaseDoValidateInput,
BasePostGraphConstruct,
BasePostGraphExecute,
BasePostNodeExecute,
BasePostTaskExecute,
BasePostTaskExpand,
BasePostTaskGroup,
BasePostTaskReturn,
BasePreDoAnythingHook,
BasePreGraphExecute,
BasePreNodeExecute,
BasePreTaskExecute,
BasePreTaskSubmission,
BaseValidateGraph,
BaseValidateNode,
LifecycleAdapterSet,
)
from hamilton.node import Node
@dataclasses.dataclass
class HookCall:
sequence_number: int
name: str
fn: Callable
bound_kwargs: dict[str, Any]
result: Any
class SentinelException(Exception):
pass
# Allows us to track the order in which multiple lifecycle hooks are called
# This ensures that we can get order of call across operations
# We'll want to test for order, not absolute position, as it is global
sequence_number = 0
# This is slightly hacky, but its a quick way to decorate the lifecycle hooks
class ExtendToTrackCalls:
def __init__(self, name: str):
# We just pass in self, cause why not?
adapter_set = LifecycleAdapterSet(self)
for lifecycle_step, _ in {
**adapter_set.sync_methods,
**adapter_set.async_methods,
**adapter_set.sync_hooks,
**adapter_set.async_hooks,
**adapter_set.sync_validators,
}.items():
# We know there's just one in this case
setattr(self, lifecycle_step, self._wrap_fn(getattr(self, lifecycle_step)))
self._calls = []
self._name = name
def _wrap_fn(self, fn: Callable):
@functools.wraps(fn)
def wrapped(*args, **kwargs):
global sequence_number
sequence_number += 1
sig = inspect.signature(fn)
bound = sig.bind(*args, **kwargs)
out = fn(*args, **kwargs)
self._calls.append(HookCall(sequence_number, self._name, fn, bound.arguments, out))
return out
return wrapped
@property
def calls(self) -> list[HookCall]:
return self._calls
@property
def name(self):
return self._name
class TrackingPreDoAnythingHook(BasePreDoAnythingHook, ExtendToTrackCalls):
def pre_do_anything(self):
pass
class TrackingPostGraphConstructHook(ExtendToTrackCalls, BasePostGraphConstruct):
def post_graph_construct(
self, graph: FunctionGraph, modules: list[ModuleType], config: dict[str, Any]
):
pass
class TrackingPreGraphExecuteHook(ExtendToTrackCalls, BasePreGraphExecute):
def pre_graph_execute(
self,
run_id: str,
graph: FunctionGraph,
final_vars: list[str],
inputs: dict[str, Any],
overrides: dict[str, Any],
):
pass
class TrackingPostTaskGroupHook(ExtendToTrackCalls, BasePostTaskGroup):
def post_task_group(self, run_id: str, task_ids: list[str]):
pass
class TrackingPreTaskExecuteHook(ExtendToTrackCalls, BasePreTaskExecute):
def pre_task_execute(
self,
run_id: str,
task_id: str,
nodes: list[node.Node],
inputs: dict[str, Any],
overrides: dict[str, Any],
spawning_task_id: str | None,
purpose: NodeGroupPurpose,
):
pass
class TrackingPreNodeExecuteHook(ExtendToTrackCalls, BasePreNodeExecute):
def pre_node_execute(
self, run_id: str, node_: Node, kwargs: dict[str, Any], task_id: str | None = None
):
pass
class TrackingPostNodeExecuteHook(ExtendToTrackCalls, BasePostNodeExecute):
def post_node_execute(
self,
run_id: str,
node_: Node,
kwargs: dict[str, Any],
success: bool,
error: Exception | None,
result: Any,
task_id: str | None = None,
):
pass
class TrackingPreTaskSubmissionHook(ExtendToTrackCalls, BasePreTaskSubmission):
def pre_task_submission(
self,
*,
run_id: str,
task_id: str,
nodes: list[Node],
inputs: dict[str, Any],
overrides: dict[str, Any],
spawning_task_id: str | None,
purpose: NodeGroupPurpose,
):
pass
class TrackingPostTaskReturnHook(ExtendToTrackCalls, BasePostTaskReturn):
def post_task_return(
self,
*,
run_id: str,
task_id: str,
nodes: list[Node],
result: Any,
success: bool,
error: Exception,
spawning_task_id: str | None,
purpose: NodeGroupPurpose,
):
pass
class TrackingPostTaskExecuteHook(ExtendToTrackCalls, BasePostTaskExecute):
def post_task_execute(
self,
run_id: str,
task_id: str,
nodes: list[node.Node],
results: dict[str, Any] | None,
success: bool,
error: Exception,
spawning_task_id: str | None,
purpose: NodeGroupPurpose,
):
pass
class TrackingPostTaskExpandHook(ExtendToTrackCalls, BasePostTaskExpand):
def post_task_expand(self, run_id: str, task_id: str, parameters: dict[str, Any]):
pass
class TrackingPostGraphExecuteHook(ExtendToTrackCalls, BasePostGraphExecute):
def post_graph_execute(
self,
run_id: str,
graph: FunctionGraph,
success: bool,
error: Exception | None,
results: dict[str, Any] | None,
):
pass
class TrackingDoValidateInputMethod(ExtendToTrackCalls, BaseDoValidateInput):
def __init__(self, name: str, valid: bool = True):
super().__init__(name)
self._valid = valid
def do_validate_input(self, node_type: type, input_value: Any) -> bool:
return self._valid
class TrackingDoNodeExecuteHook(ExtendToTrackCalls, BaseDoNodeExecute):
def __init__(self, name: str, additional_value: int):
super().__init__(name)
self._additional_value = additional_value
def do_node_execute(
self, run_id: str, node_: node.Node, kwargs: dict[str, Any], task_id: str | None = None
) -> Any:
if node_.type == int and node_.name != "n_iters":
return node_(**kwargs) + self._additional_value
return node_(**kwargs)
class TrackingDoRemoteExecuteHook(ExtendToTrackCalls, BaseDoRemoteExecute):
def __init__(self, name: str, additional_value: int):
super().__init__(name)
self._additional_value = additional_value
def do_remote_execute(
self,
node: "node.Node",
execute_lifecycle_for_node: Callable,
**kwargs: dict[str, Any],
) -> Any:
node_ = node
if node_.type == int and node_.name != "n_iters":
return execute_lifecycle_for_node(**kwargs) + self._additional_value
return execute_lifecycle_for_node(**kwargs)
class TrackingDoBuildResultMethod(ExtendToTrackCalls, BaseDoBuildResult):
def __init__(self, name: str, result: Any):
super().__init__(name)
self._result = result
def do_build_result(self, outputs: dict[str, Any]) -> Any:
return self._result
class TrackingValidateNodeValidator(ExtendToTrackCalls, BaseValidateNode):
def __init__(self, name: str, valid: bool, message: str | None):
super().__init__(name)
self._valid = valid
self._message = message
def validate_node(self, *, created_node: node.Node) -> tuple[bool, str | None]:
return self._valid, self._message
class TrackingValidateGraphValidator(ExtendToTrackCalls, BaseValidateGraph):
def __init__(self, name: str, valid: bool, message: str | None):
super().__init__(name)
self._valid = valid
self._message = message
def validate_graph(
self, *, graph: "FunctionGraph", modules: list[ModuleType], config: dict[str, Any]
) -> tuple[bool, str | None]:
return self._valid, self._message