From 794c2c4fc259238b6e83cd027b7999e835efa588 Mon Sep 17 00:00:00 2001 From: Stefan Krawczyk Date: Fri, 12 Jul 2024 17:22:47 -0600 Subject: [PATCH] Adds loader and saver decorators They enable one to annotate a function as loading or saving data and then having that metadata available for capture. This also removes older code -- hopefully all of it... --- examples/materialization/using_types/run.py | 23 ++++ .../using_types/simple_etl.png | Bin 30787 -> 37494 bytes .../materialization/using_types/simple_etl.py | 33 ++---- hamilton/execution/graph_functions.py | 13 --- hamilton/function_modifiers/__init__.py | 2 + hamilton/function_modifiers/adapters.py | 1 - hamilton/function_modifiers/macros.py | 105 ++++++++++++++++++ hamilton/graph.py | 4 +- hamilton/htypes.py | 72 ------------ hamilton/node.py | 26 +---- .../src/hamilton_sdk/tracking/pandas_stats.py | 4 +- ui/sdk/src/hamilton_sdk/tracking/stats.py | 13 +-- 12 files changed, 149 insertions(+), 147 deletions(-) create mode 100644 examples/materialization/using_types/run.py diff --git a/examples/materialization/using_types/run.py b/examples/materialization/using_types/run.py new file mode 100644 index 000000000..973c910b0 --- /dev/null +++ b/examples/materialization/using_types/run.py @@ -0,0 +1,23 @@ +import simple_etl +from hamilton_sdk import adapters + +from hamilton import driver + +tracker = adapters.HamiltonTracker( + project_id=7, # modify this as needed + username="elijah@dagworks.io", + dag_name="my_version_of_the_dag", + tags={"environment": "DEV", "team": "MY_TEAM", "version": "X"}, +) # note this slows down execution because there's 60 columns. +# 30 columns adds about a 1 second. +# 60 is therefore 2 seconds. + +dr = driver.Builder().with_config({}).with_modules(simple_etl).with_adapters(tracker).build() +dr.display_all_functions("simple_etl.png") + +import time + +start = time.time() +print(start) +dr.execute(["saved_data"], inputs={"filepath": "data.csv"}) +print(time.time() - start) diff --git a/examples/materialization/using_types/simple_etl.png b/examples/materialization/using_types/simple_etl.png index e83cef032149b7b68ea806117ec6efc102e3dc90..13c5cafd2f0162394cef58aa2ee812f3bcf75c48 100644 GIT binary patch literal 37494 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 ONkT+MxKK#P=l=k>GlkXw literal 30787 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 EA8{A5GXMYp diff --git a/examples/materialization/using_types/simple_etl.py b/examples/materialization/using_types/simple_etl.py index a9f99c791..d14c266d8 100644 --- a/examples/materialization/using_types/simple_etl.py +++ b/examples/materialization/using_types/simple_etl.py @@ -1,37 +1,24 @@ import pandas as pd from sklearn import datasets -from hamilton.htypes import DataLoaderMetadata, DataSaverMetadata +from hamilton.function_modifiers import loader, saver +from hamilton.io import utils as io_utils -def raw_data() -> tuple[pd.DataFrame, DataLoaderMetadata]: +@loader() +def raw_data() -> tuple[pd.DataFrame, dict]: data = datasets.load_digits() df = pd.DataFrame(data.data, columns=[f"feature_{i}" for i in range(data.data.shape[1])]) - return df, DataLoaderMetadata.from_dataframe(df) + metadata = io_utils.get_dataframe_metadata(df) + return df, metadata def transformed_data(raw_data: pd.DataFrame) -> pd.DataFrame: return raw_data -def saved_data(transformed_data: pd.DataFrame, filepath: str) -> DataSaverMetadata: +@saver() +def saved_data(transformed_data: pd.DataFrame, filepath: str) -> dict: transformed_data.to_csv(filepath) - return DataSaverMetadata.from_file_and_dataframe(filepath, transformed_data) - - -if __name__ == "__main__": - import __main__ as simple_etl - from hamilton_sdk import adapters - - from hamilton import driver - - tracker = adapters.HamiltonTracker( - project_id=7, # modify this as needed - username="elijah@dagworks.io", - dag_name="my_version_of_the_dag", - tags={"environment": "DEV", "team": "MY_TEAM", "version": "X"}, - ) - dr = driver.Builder().with_config({}).with_modules(simple_etl).with_adapters(tracker).build() - dr.display_all_functions("simple_etl.png") - - dr.execute(["saved_data"], inputs={"filepath": "data.csv"}) + metadata = io_utils.get_file_and_dataframe_metadata(filepath, transformed_data) + return metadata diff --git a/hamilton/execution/graph_functions.py b/hamilton/execution/graph_functions.py index edeb5fb51..3a86d8b60 100644 --- a/hamilton/execution/graph_functions.py +++ b/hamilton/execution/graph_functions.py @@ -3,7 +3,6 @@ from typing import Any, Collection, Dict, List, Optional, Set, Tuple from hamilton import node -from hamilton.htypes import DataLoaderMetadata from hamilton.lifecycle.base import LifecycleAdapterSet logger = logging.getLogger(__name__) @@ -219,18 +218,6 @@ def dfs_traverse( except Exception as e: pre_node_execute_errored = True raise e - # this is a hack - # if one of the kwargs is a tuple[Value, DataLoaderMetadata] we need to unpack it - kwargs = { - k: ( - v[0] - if isinstance(v, tuple) - and len(v) == 2 - and isinstance(v[1], DataLoaderMetadata) - else v - ) - for k, v in kwargs.items() - } if adapter.does_method("do_node_execute", is_async=False): result = adapter.call_lifecycle_method_sync( "do_node_execute", diff --git a/hamilton/function_modifiers/__init__.py b/hamilton/function_modifiers/__init__.py index 1f52edcb2..a1baf34e0 100644 --- a/hamilton/function_modifiers/__init__.py +++ b/hamilton/function_modifiers/__init__.py @@ -92,3 +92,5 @@ # materialization stuff load_from = adapters.load_from save_to = adapters.save_to +loader = macros.loader +saver = macros.saver diff --git a/hamilton/function_modifiers/adapters.py b/hamilton/function_modifiers/adapters.py index 0fe712f90..48c4d9480 100644 --- a/hamilton/function_modifiers/adapters.py +++ b/hamilton/function_modifiers/adapters.py @@ -265,7 +265,6 @@ def filter_function(_inject_parameter=inject_parameter, **kwargs): def inject_nodes( self, params: Dict[str, Type[Type]], config: Dict[str, Any], fn: Callable ) -> Tuple[Collection[node.Node], Dict[str, str]]: - pass """Generates two nodes: 1. A node that loads the data from the data source, and returns that + metadata 2. A node that takes the data from the data source, injects it into, and runs, the function. diff --git a/hamilton/function_modifiers/macros.py b/hamilton/function_modifiers/macros.py index d855448cf..89e745070 100644 --- a/hamilton/function_modifiers/macros.py +++ b/hamilton/function_modifiers/macros.py @@ -5,6 +5,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union import pandas as pd +import typing_inspect from hamilton import models, node from hamilton.dev_utils.deprecation import deprecated @@ -12,6 +13,7 @@ from hamilton.function_modifiers.configuration import ConfigResolver from hamilton.function_modifiers.delayed import resolve as delayed_resolve from hamilton.function_modifiers.dependencies import ( + InvalidDecoratorException, LiteralDependency, SingleDependency, UpstreamDependency, @@ -870,3 +872,106 @@ def optional_config(self) -> Dict[str, Any]: # # def __init__(self, *transforms: Applicable, collapse=False): # super(flow, self).__init__(*transforms, collapse=collapse, _chain=False) + + +class loader(base.NodeCreator): + """Class to capture metadata.""" + + # def __init__(self, og_function: Callable): + # self.og_function = og_function + # super(loader,self).__init__() + + def validate(self, fn: Callable): + print("called validate loader") + return_annotation = inspect.signature(fn).return_annotation + if return_annotation is inspect.Signature.empty: + raise InvalidDecoratorException( + f"Function: {fn.__qualname__} must have a return annotation." + ) + # check that the type is a tuple[TYPE, dict]: + if not typing_inspect.is_tuple_type(return_annotation): + raise InvalidDecoratorException(f"Function: {fn.__qualname__} must return a tuple.") + if len(typing_inspect.get_args(return_annotation)) != 2: + raise InvalidDecoratorException( + f"Function: {fn.__qualname__} must return a tuple of length 2." + ) + if not typing_inspect.get_args(return_annotation)[1] == dict: + raise InvalidDecoratorException( + f"Function: {fn.__qualname__} must return a tuple of type (SOME_TYPE, dict)." + ) + + def generate_nodes(self, fn: Callable, config) -> List[node.Node]: + """ + Generates two nodes. + The first one is just the fn - with a slightly different name, + the second one uses the proper function name, but only returns + the first part of the tuple that the first returns. + We have to add tags appropriately. + :param fn: + :param config: + :return: + """ + _name = "loader" + og_node = node.Node.from_fn(fn, name=_name) + new_tags = og_node.tags.copy() + new_tags.update( + { + "hamilton.data_loader": True, + "hamilton.data_loader.has_metadata": True, + "hamilton.data_loader.source": f"{fn.__name__}", + "hamilton.data_loader.classname": f"{fn.__name__}()", + "hamilton.data_loader.node": _name, + } + ) + + def filter_function(**kwargs): + return kwargs[f"{fn.__name__}.{_name}"][0] + + filter_node = node.Node( + name=fn.__name__, # use original function name + callabl=filter_function, + typ=typing_inspect.get_args(og_node.type)[0], + input_types={f"{fn.__name__}.{_name}": og_node.type}, + tags={ + "hamilton.data_loader": True, + "hamilton.data_loader.has_metadata": False, + "hamilton.data_loader.source": f"{fn.__name__}", + "hamilton.data_loader.classname": f"{fn.__name__}()", + "hamilton.data_loader.node": fn.__name__, + }, + ) + + return [og_node.copy_with(tags=new_tags, namespace=(fn.__name__,)), filter_node] + + +class saver(base.NodeCreator): + """Class to capture metadata.""" + + def validate(self, fn: Callable): + print("called validate") + return_annotation = inspect.signature(fn).return_annotation + if return_annotation is inspect.Signature.empty: + raise InvalidDecoratorException( + f"Function: {fn.__qualname__} must have a return annotation." + ) + # check that the return type is a dict + if return_annotation not in (dict, Dict): + raise InvalidDecoratorException(f"Function: {fn.__qualname__} must return a dict.") + + def generate_nodes(self, fn: Callable, config) -> List[node.Node]: + """ + All this does is add tags + :param fn: + :param config: + :return: + """ + og_node = node.Node.from_fn(fn) + new_tags = og_node.tags.copy() + new_tags.update( + { + "hamilton.data_saver": True, + "hamilton.data_saver.sink": f"{og_node.name}", + "hamilton.data_saver.classname": f"{fn.__name__}()", + } + ) + return [og_node.copy_with(tags=new_tags)] diff --git a/hamilton/graph.py b/hamilton/graph.py index 56edacff7..5847ef6a2 100644 --- a/hamilton/graph.py +++ b/hamilton/graph.py @@ -500,7 +500,9 @@ def _get_legend( node_style.update(**modifier_style) seen_node_types.add("materializer") - if n.tags.get("hamilton.data_loader") and "load_data." in n.name: + if n.tags.get("hamilton.data_loader") and ( + "load_data." in n.name or "loader" == n.tags.get("hamilton.data_loader.node") + ): materializer_type = n.tags["hamilton.data_loader.classname"] label = _get_node_label(n, type_string=materializer_type) modifier_style = _get_function_modifier_style("materializer") diff --git a/hamilton/htypes.py b/hamilton/htypes.py index 317e16ac4..2aac81920 100644 --- a/hamilton/htypes.py +++ b/hamilton/htypes.py @@ -383,75 +383,3 @@ def check_instance(obj: Any, type_: Any) -> bool: # If the type is not a generic type, just use isinstance return isinstance(obj, type_) - - -from io import BytesIO -from pathlib import Path -from typing import Any, BinaryIO, Literal, TextIO - - -class DataSaverMetadata: - - def __init__(self, metadata: dict): - self.value: dict = metadata - - @classmethod - def from_file_and_dataframe( - cls, file: Union[str, TextIO, BytesIO, Path, BinaryIO, bytes], dataframe: Any - ) -> "DataSaverMetadata": - from hamilton.io import utils as io_utils # here due to circular import - - metadata = io_utils.get_file_and_dataframe_metadata(file, dataframe) - return DataSaverMetadata(metadata) - - @classmethod - def from_file( - cls, file: Union[str, TextIO, BytesIO, Path, BinaryIO, bytes] - ) -> "DataSaverMetadata": - from hamilton.io import utils as io_utils # here due to circular import - - metadata = io_utils.get_file_metadata(file) - return DataSaverMetadata(metadata) - - @classmethod - def from_dataframe(cls, dataframe: Any) -> "DataSaverMetadata": - from hamilton.io import utils as io_utils # here due to circular import - - metadata = io_utils.get_dataframe_metadata(dataframe) - return DataSaverMetadata(metadata) - - def to_dict(self) -> dict: - return self.value - - -class DataLoaderMetadata: - def __init__(self, metadata: dict): - self.value: dict = metadata - - @classmethod - def from_file_and_dataframe( - cls, file: Union[str, TextIO, BytesIO, Path, BinaryIO, bytes], dataframe: Any - ) -> "DataLoaderMetadata": - from hamilton.io import utils as io_utils # here due to circular import - - metadata = io_utils.get_file_and_dataframe_metadata(file, dataframe) - return DataLoaderMetadata(metadata) - - @classmethod - def from_file( - cls, file: Union[str, TextIO, BytesIO, Path, BinaryIO, bytes] - ) -> "DataLoaderMetadata": - from hamilton.io import utils as io_utils # here due to circular import - - metadata = io_utils.get_file_metadata(file) - return DataLoaderMetadata(metadata) - - @classmethod - def from_dataframe(cls, dataframe: Any) -> "DataLoaderMetadata": - from hamilton.io import utils as io_utils # here due to circular import - - metadata = io_utils.get_dataframe_metadata(dataframe) - return DataLoaderMetadata(metadata) - - def to_dict(self) -> dict: - return self.value diff --git a/hamilton/node.py b/hamilton/node.py index 4cc59e056..2c6e5c73f 100644 --- a/hamilton/node.py +++ b/hamilton/node.py @@ -6,7 +6,7 @@ import typing_inspect -from hamilton.htypes import Collect, DataLoaderMetadata, DataSaverMetadata, Parallelizable +from hamilton.htypes import Collect, Parallelizable """ Module that contains the primitive components of the graph. @@ -274,30 +274,6 @@ def from_fn(fn: Callable, name: str = None) -> "Node": if typing_inspect.is_generic_type(return_type): if typing_inspect.get_origin(return_type) == Parallelizable: node_source = NodeType.EXPAND - elif return_type == DataSaverMetadata: - tags.update( - { - "hamilton.data_saver": True, - "hamilton.data_saver.sink": fn.__name__, - "hamilton.data_saver.classname": fn.__name__, - } - ) - # check for tuple[DataLoaderMetadata, Any], or Tuple[DataLoaderMetadata, Any] - elif ( - typing_inspect.get_origin(return_type) == tuple - and len(return_type.__args__) == 2 - and return_type.__args__[1] == DataLoaderMetadata - ): - tags.update( - { - "hamilton.data_loader": True, - "hamilton.data_loader.has_metadata": True, - "hamilton.data_loader.source": fn.__name__, - "hamilton.data_loader.classname": fn.__name__, - } - ) - # make return types match -- TODO: actually do the right data loader thing - return_type = return_type.__args__[0] for parameter in inspect.signature(fn).parameters.values(): hint = parameter.annotation if typing_inspect.is_generic_type(hint): diff --git a/ui/sdk/src/hamilton_sdk/tracking/pandas_stats.py b/ui/sdk/src/hamilton_sdk/tracking/pandas_stats.py index f190b8b08..e6ec8c86a 100644 --- a/ui/sdk/src/hamilton_sdk/tracking/pandas_stats.py +++ b/ui/sdk/src/hamilton_sdk/tracking/pandas_stats.py @@ -44,7 +44,9 @@ def _compute_stats(df: pd.DataFrame) -> Dict[str, Dict[str, Any]]: def execute_col( target_output: str, col: pd.Series, name: Union[str, int], position: int ) -> Dict[str, Any]: - """Get stats on a column.""" + """Get stats on a column. + TODO: profile this and see where we can speed things up. + """ try: res = dr.execute( [target_output], inputs={"col": col, "name": name, "position": position} diff --git a/ui/sdk/src/hamilton_sdk/tracking/stats.py b/ui/sdk/src/hamilton_sdk/tracking/stats.py index 5938d686b..44f5a5c3e 100644 --- a/ui/sdk/src/hamilton_sdk/tracking/stats.py +++ b/ui/sdk/src/hamilton_sdk/tracking/stats.py @@ -5,8 +5,6 @@ import pandas as pd from hamilton_sdk.tracking import sql_utils -from hamilton.htypes import DataLoaderMetadata, DataSaverMetadata - StatsType = Dict[str, Any] @@ -46,13 +44,6 @@ def compute_stats_primitives(result, node_name: str, node_tags: dict) -> StatsTy } -@compute_stats.register(DataSaverMetadata) -def compute_state_saver( - result: DataSaverMetadata, node_name: str, node_tags: dict -) -> Dict[str, Any]: - return compute_stats_dict(result.to_dict(), node_name, node_tags) - - @compute_stats.register(dict) def compute_stats_dict(result: dict, node_name: str, node_tags: dict) -> StatsType: """call summary stats on the values in the dict""" @@ -105,8 +96,8 @@ def compute_stats_dict(result: dict, node_name: str, node_tags: dict) -> StatsTy @compute_stats.register(tuple) def compute_stats_tuple(result: tuple, node_name: str, node_tags: dict) -> StatsType: if "hamilton.data_loader" in node_tags and node_tags["hamilton.data_loader"] is True: - # assumption it's a tuple -- HACK to get metadata for dataloadermetadata -- TODO: create actual nodes. - if isinstance(result[1], dict) or isinstance(result[1], DataLoaderMetadata): + # assumption it's a tuple + if isinstance(result[1], dict): try: # double check that it's JSON serializable raw_data = (