| # 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 |