From 3ae77be73c59c4e2201408875429eeef3b9f646f Mon Sep 17 00:00:00 2001 From: zilto Date: Thu, 16 May 2024 09:24:45 -0400 Subject: [PATCH 1/5] added Builder.with_materializers() --- hamilton/driver.py | 75 ++++++++++++++------- tests/resources/test_for_materialization.py | 4 ++ tests/test_hamilton_driver.py | 59 +++++++++++++++- 3 files changed, 114 insertions(+), 24 deletions(-) diff --git a/hamilton/driver.py b/hamilton/driver.py index 9b81f2827..912a2fb7f 100644 --- a/hamilton/driver.py +++ b/hamilton/driver.py @@ -1657,6 +1657,7 @@ def __init__(self): # common fields self.config = {} self.modules = [] + self.materializers = [] self.legacy_graph_adapter = None # Standard execution fields @@ -1734,6 +1735,16 @@ def with_adapters(self, *adapters: lifecycle_base.LifecycleAdapter) -> "Builder" self.adapters.extend(adapters) return self + def with_materializers(self, *materializers) -> "Builder": + """Add materializer nodes to the `Driver` + The generated nodes can be referenced by name in `.execute()` + + :param materializers: materializers to add to the dataflow + :return: self + """ + self.materializers.extend(materializers) + return self + def with_execution_manager(self, execution_manager: executors.ExecutionManager) -> "Builder": """Sets the execution manager to use. Note that this cannot be used if local_executor or remote_executor are also set @@ -1807,30 +1818,47 @@ def build(self) -> Driver: if self.legacy_graph_adapter is not None: adapter.append(self.legacy_graph_adapter) - if not self.v2_executor: - return Driver( - self.config, *self.modules, adapter=adapter, _use_legacy_adapter=False - ) # TODO -- validate that this is backwards compatible - execution_manager = self.execution_manager - if execution_manager is None: - local_executor = self.local_executor or executors.SynchronousLocalTaskExecutor() - remote_executor = self.remote_executor or executors.MultiThreadingExecutor(max_tasks=10) - execution_manager = executors.DefaultExecutionManager( - local_executor=local_executor, remote_executor=remote_executor + if self.v2_executor: + execution_manager = self.execution_manager + if execution_manager is None: + local_executor = self.local_executor or executors.SynchronousLocalTaskExecutor() + remote_executor = self.remote_executor or executors.MultiThreadingExecutor(max_tasks=10) + execution_manager = executors.DefaultExecutionManager( + local_executor=local_executor, remote_executor=remote_executor + ) + grouping_strategy = self.grouping_strategy or grouping.GroupByRepeatableBlocks() + graph_executor = TaskBasedGraphExecutor( + execution_manager=execution_manager, + grouping_strategy=grouping_strategy, + adapter=lifecycle_base.LifecycleAdapterSet(*adapter), ) - grouping_strategy = self.grouping_strategy or grouping.GroupByRepeatableBlocks() - graph_executor = TaskBasedGraphExecutor( - execution_manager=execution_manager, - grouping_strategy=grouping_strategy, - adapter=lifecycle_base.LifecycleAdapterSet(*adapter), - ) - return Driver( - self.config, - *self.modules, - adapter=adapter, - _graph_executor=graph_executor, - _use_legacy_adapter=False, - ) + dr = Driver( + self.config, + *self.modules, + adapter=adapter, + _graph_executor=graph_executor, + _use_legacy_adapter=False, + ) + else: + dr = Driver(self.config, *self.modules, adapter=adapter, _use_legacy_adapter=False) + + if len(self.materializers) > 0: + try: # logic adapted from `Driver.materialize()`; could be deduplicated for maintenance + materializer_factories, extractor_factories = dr._process_materializers(self.materializers) + augmented_fn_graph = materialization.modify_graph(dr.graph, materializer_factories, extractor_factories) + Driver._perform_graph_validations(dr.adapter, augmented_fn_graph, self.modules) + dr.graph = augmented_fn_graph + if dr.adapter.does_hook("post_graph_construct", is_async=False): + dr.adapter.call_all_lifecycle_hooks_sync( + "post_graph_construct", + graph=augmented_fn_graph, + modules=self.modules, + config=augmented_fn_graph.config, + ) + except BaseException: + raise + + return dr def copy(self) -> "Builder": """Creates a copy of the current state of this Builder. @@ -1843,6 +1871,7 @@ def copy(self) -> "Builder": new_builder.modules = self.modules.copy() new_builder.legacy_graph_adapter = self.legacy_graph_adapter new_builder.adapters = self.adapters.copy() + new_builder.materializers = self.materializers.copy() new_builder.execution_manager = self.execution_manager new_builder.local_executor = self.local_executor new_builder.remote_executor = self.remote_executor diff --git a/tests/resources/test_for_materialization.py b/tests/resources/test_for_materialization.py index 58bbc9bcc..2045ca298 100644 --- a/tests/resources/test_for_materialization.py +++ b/tests/resources/test_for_materialization.py @@ -10,3 +10,7 @@ def json_to_save_2() -> dict: "key3": "value3", "key4": "value4", } + + +def expects_loader(external: dict) -> dict: + return external \ No newline at end of file diff --git a/tests/test_hamilton_driver.py b/tests/test_hamilton_driver.py index 1ea359736..d30beb639 100644 --- a/tests/test_hamilton_driver.py +++ b/tests/test_hamilton_driver.py @@ -12,7 +12,7 @@ Variable, ) from hamilton.execution import executors -from hamilton.io.materialization import to +from hamilton.io.materialization import to, from_ import tests.resources.cyclic_functions import tests.resources.dummy_functions @@ -20,6 +20,7 @@ import tests.resources.tagging import tests.resources.test_default_args import tests.resources.very_simple_dag +import tests.resources.test_for_materialization """This file tests driver capabilities. Anything involving execution is tested for multiple executors/driver configuration. @@ -531,6 +532,62 @@ def test_builder_copy(): # assert attr_value_copy is not attr_value +def test_builder_with_loader_materializer(): + loader_target = "external" + loader = from_.json(target=loader_target, path="/my/file.json") + dr = ( + Builder() + .with_modules(tests.resources.test_for_materialization) + .with_materializers(loader) + .build() + ) + + assert any(n.name == f"load_data.{loader_target}" for n in dr.graph.get_nodes()) + + +def test_builder_with_saver_materializer(): + saver_id = "saver_node" + saver = to.json( + id=saver_id, + dependencies=["expects_loader"], + path="/my/file.json", + ) + dr = ( + Builder() + .with_modules(tests.resources.test_for_materialization) + .with_materializers(saver) + .build() + ) + + assert any(n.name == saver_id for n in dr.graph.get_nodes()) + + +def test_builder_materializer_and_execution_materializer(tmp_path): + static_saver = to.json( + id="static_saver", + dependencies=["json_to_save_1"], + path=f"{tmp_path}/file.json", + ) + dynamic_saver = to.json( + id="dynamic_saver", + dependencies=["json_to_save_2"], + path=f"{tmp_path}/file2.json", + ) + dr = ( + Builder() + .with_modules(tests.resources.test_for_materialization) + .with_materializers(static_saver) + .build() + ) + metadata, additional = dr.materialize( + dynamic_saver, + additional_vars=["static_saver"] + ) + + assert "dynamic_saver" in metadata.keys() + assert "static_saver" in additional.keys() + + def test_materialize_checks_required_input(tmp_path): dr = Builder().with_modules(tests.resources.dummy_functions).build() From 85b7634a0d7b29a315cd4d920dd348a448a8c2fe Mon Sep 17 00:00:00 2001 From: zilto Date: Sat, 18 May 2024 20:08:20 -0400 Subject: [PATCH 2/5] moved logic to Driver __init__ --- hamilton/driver.py | 40 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/hamilton/driver.py b/hamilton/driver.py index 912a2fb7f..7d4bfa31d 100644 --- a/hamilton/driver.py +++ b/hamilton/driver.py @@ -22,6 +22,7 @@ from hamilton.io.materialization import ExtractorFactory, MaterializerFactory from hamilton.lifecycle import base as lifecycle_base + SLACK_ERROR_MESSAGE = ( "-------------------------------------------------------------------\n" "Oh no an error! Need help with Hamilton?\n" @@ -356,6 +357,7 @@ def __init__( adapter: Optional[ Union[lifecycle_base.LifecycleAdapter, List[lifecycle_base.LifecycleAdapter]] ] = None, + materializers = None, _graph_executor: GraphExecutor = None, _use_legacy_adapter: bool = True, ): @@ -382,6 +384,9 @@ def __init__( self.graph_modules = modules try: self.graph = graph.FunctionGraph.from_modules(*modules, config=config, adapter=adapter) + if materializers: + materializer_factories, extractor_factories = self._process_materializers(materializers) + self.graph = materialization.modify_graph(self.graph, materializer_factories, extractor_factories) Driver._perform_graph_validations(adapter, graph=self.graph, graph_modules=modules) if adapter.does_hook("post_graph_construct", is_async=False): adapter.call_all_lifecycle_hooks_sync( @@ -1818,6 +1823,7 @@ def build(self) -> Driver: if self.legacy_graph_adapter is not None: adapter.append(self.legacy_graph_adapter) + graph_executor = None if self.v2_executor: execution_manager = self.execution_manager if execution_manager is None: @@ -1832,33 +1838,15 @@ def build(self) -> Driver: grouping_strategy=grouping_strategy, adapter=lifecycle_base.LifecycleAdapterSet(*adapter), ) - dr = Driver( - self.config, - *self.modules, - adapter=adapter, - _graph_executor=graph_executor, - _use_legacy_adapter=False, - ) - else: - dr = Driver(self.config, *self.modules, adapter=adapter, _use_legacy_adapter=False) - - if len(self.materializers) > 0: - try: # logic adapted from `Driver.materialize()`; could be deduplicated for maintenance - materializer_factories, extractor_factories = dr._process_materializers(self.materializers) - augmented_fn_graph = materialization.modify_graph(dr.graph, materializer_factories, extractor_factories) - Driver._perform_graph_validations(dr.adapter, augmented_fn_graph, self.modules) - dr.graph = augmented_fn_graph - if dr.adapter.does_hook("post_graph_construct", is_async=False): - dr.adapter.call_all_lifecycle_hooks_sync( - "post_graph_construct", - graph=augmented_fn_graph, - modules=self.modules, - config=augmented_fn_graph.config, - ) - except BaseException: - raise - return dr + return Driver( + self.config, + *self.modules, + adapter=adapter, + materializers=self.materializers, + _graph_executor=graph_executor, + _use_legacy_adapter=False + ) def copy(self) -> "Builder": """Creates a copy of the current state of this Builder. From 7fa7ef40ce68ef4154c19c4f8d6214b2dc644fa8 Mon Sep 17 00:00:00 2001 From: zilto Date: Sat, 18 May 2024 20:09:56 -0400 Subject: [PATCH 3/5] added type annotation --- hamilton/driver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hamilton/driver.py b/hamilton/driver.py index 7d4bfa31d..d8acdea19 100644 --- a/hamilton/driver.py +++ b/hamilton/driver.py @@ -357,7 +357,7 @@ def __init__( adapter: Optional[ Union[lifecycle_base.LifecycleAdapter, List[lifecycle_base.LifecycleAdapter]] ] = None, - materializers = None, + materializers: typing.Sequence[Union[ExtractorFactory, MaterializerFactory]] = None, _graph_executor: GraphExecutor = None, _use_legacy_adapter: bool = True, ): @@ -1740,7 +1740,7 @@ def with_adapters(self, *adapters: lifecycle_base.LifecycleAdapter) -> "Builder" self.adapters.extend(adapters) return self - def with_materializers(self, *materializers) -> "Builder": + def with_materializers(self, *materializers: typing.Sequence[Union[ExtractorFactory, MaterializerFactory]]) -> "Builder": """Add materializer nodes to the `Driver` The generated nodes can be referenced by name in `.execute()` From 0b664fcef4bb9bb904b8d26934739eb3bca6b7a5 Mon Sep 17 00:00:00 2001 From: zilto Date: Tue, 21 May 2024 11:51:47 -0400 Subject: [PATCH 4/5] made materializers an internal attribute --- hamilton/driver.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/hamilton/driver.py b/hamilton/driver.py index d8acdea19..3d9f8d535 100644 --- a/hamilton/driver.py +++ b/hamilton/driver.py @@ -22,7 +22,6 @@ from hamilton.io.materialization import ExtractorFactory, MaterializerFactory from hamilton.lifecycle import base as lifecycle_base - SLACK_ERROR_MESSAGE = ( "-------------------------------------------------------------------\n" "Oh no an error! Need help with Hamilton?\n" @@ -357,7 +356,7 @@ def __init__( adapter: Optional[ Union[lifecycle_base.LifecycleAdapter, List[lifecycle_base.LifecycleAdapter]] ] = None, - materializers: typing.Sequence[Union[ExtractorFactory, MaterializerFactory]] = None, + _materializers: typing.Sequence[Union[ExtractorFactory, MaterializerFactory]] = None, _graph_executor: GraphExecutor = None, _use_legacy_adapter: bool = True, ): @@ -368,6 +367,7 @@ def __init__( :param modules: Python module objects you want to inspect for Hamilton Functions. :param adapter: Optional. A way to wire in another way of "executing" a hamilton graph. Defaults to using original Hamilton adapter which is single threaded in memory python. + :param _materializers: Not public facing, do not use this parameter. This is injected by the builder. :param _graph_executor: Not public facing, do not use this parameter. This is injected by the builder. If you need to tune execution, use the builder to do so. :param _use_legacy_adapter: Not public facing, do not use this parameter. @@ -384,9 +384,13 @@ def __init__( self.graph_modules = modules try: self.graph = graph.FunctionGraph.from_modules(*modules, config=config, adapter=adapter) - if materializers: - materializer_factories, extractor_factories = self._process_materializers(materializers) - self.graph = materialization.modify_graph(self.graph, materializer_factories, extractor_factories) + if _materializers: + materializer_factories, extractor_factories = self._process_materializers( + _materializers + ) + self.graph = materialization.modify_graph( + self.graph, materializer_factories, extractor_factories + ) Driver._perform_graph_validations(adapter, graph=self.graph, graph_modules=modules) if adapter.does_hook("post_graph_construct", is_async=False): adapter.call_all_lifecycle_hooks_sync( @@ -1740,7 +1744,9 @@ def with_adapters(self, *adapters: lifecycle_base.LifecycleAdapter) -> "Builder" self.adapters.extend(adapters) return self - def with_materializers(self, *materializers: typing.Sequence[Union[ExtractorFactory, MaterializerFactory]]) -> "Builder": + def with_materializers( + self, *materializers: typing.Sequence[Union[ExtractorFactory, MaterializerFactory]] + ) -> "Builder": """Add materializer nodes to the `Driver` The generated nodes can be referenced by name in `.execute()` @@ -1828,7 +1834,9 @@ def build(self) -> Driver: execution_manager = self.execution_manager if execution_manager is None: local_executor = self.local_executor or executors.SynchronousLocalTaskExecutor() - remote_executor = self.remote_executor or executors.MultiThreadingExecutor(max_tasks=10) + remote_executor = self.remote_executor or executors.MultiThreadingExecutor( + max_tasks=10 + ) execution_manager = executors.DefaultExecutionManager( local_executor=local_executor, remote_executor=remote_executor ) @@ -1843,9 +1851,9 @@ def build(self) -> Driver: self.config, *self.modules, adapter=adapter, - materializers=self.materializers, + _materializers=self.materializers, _graph_executor=graph_executor, - _use_legacy_adapter=False + _use_legacy_adapter=False, ) def copy(self) -> "Builder": From 99f7e897928a2d801d636322a2e94d62be0d71e6 Mon Sep 17 00:00:00 2001 From: zilto Date: Tue, 21 May 2024 17:00:23 -0400 Subject: [PATCH 5/5] updated docs; pre-commit hook; typing bugfix --- docs/concepts/_snippets/config_when copy.png | Bin 0 -> 15007 bytes docs/concepts/_snippets/config_when.png | Bin 0 -> 15007 bytes docs/concepts/_snippets/decorator_ctx.png | Bin 0 -> 45585 bytes docs/concepts/_snippets/decorator_ctx.py | 33 +++++ .../_snippets/dynamic_materializer_ctx.png | Bin 0 -> 19003 bytes ...zer_ctx.py => dynamic_materializer_ctx.py} | 20 +-- docs/concepts/_snippets/materializers.png | Bin 0 -> 29435 bytes docs/concepts/_snippets/node_ctx.py | 1 - .../_snippets/static_materializer_ctx.png | Bin 0 -> 30138 bytes .../_snippets/static_materializer_ctx.py | 34 +++++ docs/concepts/builder.rst | 52 +++++++ docs/concepts/materialization.rst | 130 ++++++++++-------- hamilton/driver.py | 2 +- tests/resources/test_for_materialization.py | 2 +- tests/test_hamilton_driver.py | 9 +- 15 files changed, 205 insertions(+), 78 deletions(-) create mode 100644 docs/concepts/_snippets/config_when copy.png create mode 100644 docs/concepts/_snippets/config_when.png create mode 100644 docs/concepts/_snippets/decorator_ctx.png create mode 100644 docs/concepts/_snippets/decorator_ctx.py create mode 100644 docs/concepts/_snippets/dynamic_materializer_ctx.png rename docs/concepts/_snippets/{materializer_ctx.py => dynamic_materializer_ctx.py} (51%) create mode 100644 docs/concepts/_snippets/materializers.png create mode 100644 docs/concepts/_snippets/static_materializer_ctx.png create mode 100644 docs/concepts/_snippets/static_materializer_ctx.py diff --git a/docs/concepts/_snippets/config_when copy.png b/docs/concepts/_snippets/config_when copy.png new file mode 100644 index 0000000000000000000000000000000000000000..d339834f4e5b4096449546314b68bd62a88da6db GIT binary patch literal 15007 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 literal 0 HcmV?d00001 diff --git a/docs/concepts/_snippets/config_when.png b/docs/concepts/_snippets/config_when.png new file mode 100644 index 0000000000000000000000000000000000000000..d339834f4e5b4096449546314b68bd62a88da6db GIT binary patch literal 15007 zcmdtJXEdB)*e?1KLUe-YAzF+m(W8%Eg2Cu5(R+;^T@VSPN1v#J=sgTcghUOZM~N1l z5H0F{eBVC%eCw>e&N^rR*=L;}){OUk=IPIUU-xxi&r5`+GBE)i0R%zBswxUP5QHTL z{=)FF!IPj#@lf!A<)x!62UQL-tV0kpq^bZ%`eAM7;29#nPJf-}`Pe)1D)b&5J+(p& zbc8)lBW!7q!sZi!D-+Q|$L^!eOZSlMiyB1)?i+Txf^ND%N2Rqbc}rmBfJ0w>QwCIFzyo_9qmY zob2^HE zjg3u(xxzixl=}%2I^rhr{^22&kak6Y`#tH2N1=P5>=cTGPDlm2FKL~!$AT+^mn7Zh zF&56)TXD;u(WuygL|7JSxUr3n7e85eZ&=I-Q5Z_CM-)d-&DBHP04G)Vkv~ZkYX3pC zU*{|Q(anjf8?uImZ~ex~{A}q(U2#IUe>NG+L`F#A!|?8@3F3tJ#<}6cM8z#r2*9Yg z$-4U;amF5hs&4TIgZ;g(r|pE(aL=YX_cm@Jj-!#$)al{o&qkNcU%ybp_SiRWBqt^H z_V%L5LeBTT7OEzAXC)8M9U&M1+h2JlZUNk=Da&5SP zn#Z{Mn42>8oqTld?jDjZGF0613%-Pegb!LXM_k)UgEmIwcVj5Zv%DlSU-jlFk4YmW zTW34qvpW}byfceO%ETc3yxITnZ^-@p#3SnI;?fe<$NBlIk6|~uy1GQ&=3d9gb5sv! z)ui5JL1BOV^73|-MTfbLj_20oLm3$v=v!YOYx_vp(eAv3sp&qQt^fpI zQ8nC)P9;K{wfg9lB4w`51xH%-b%uTZ{CV<5O{$Lgg>*ky3y z(&V3oPTD9BU*F@y!!g~EmEKh6U>LEStXZw~Es4RM*;XuOF0PlzG>M^DD(?SFU1ibH zcxU;EX7MpG%&74)J$0=hssH$~#^cA=r zv|8!MuYKMVsy5p5?HfKRsjs(pLqkJ_Nz+a!){WZQ+FYqXrK34apzr6%TVv%0CA;!( zWkRGOup8d;R3={cVeSvV|GxG9U@QuS+S+pN^wqCcUM_nrn%oSQ-$MZhwmO}fmUa^t zmy0rXKIAIr@2_wi1q5!Q8Bs(p!RXt!Zx0R*z~V)d-pvIy-@CWiwNURgQE5q*nf!HjOiTwQhjfpBZJG<5O z^+)zYDD`ySo*qRUJx$ll25O@)$MN2xNg8-+zEgHTEqq0x8%7Y0(oY;(q`wKL%HyQ^ zjt{Req|exeBZ{o~!(r@AVmaL5y`|Zbc=8B;YQwe#a^zlsm6#yT7~(c}>2)cg|6QfZ zpP*cmmo~x4%l=Ssmk2&)@Wrta_lDlCgsyd6#+x@EE%uqw=MLQa-{itG6BAiHz56rG zm6UG&^C~$0A#98LaE=BpT$%9t_#QetLtPeZU>qpdP!aLSOWiSy3^~ZaiV`5|yjzYB zvvXDD0cJA8iV}JGUkg}8^xpojuLDr~pJ1Szi4I{T%JVl3mhe)Cg;n=!tkB6xL(M2S z;!&b@EF4sA+8T7`V{dP-NE-Qw_9j>cC9a!rgum~=pOnc(7INgPqT~Q&oK#;ta^xI) zV>lKWa!x}D;h#Q9a8nlHFA5gU%#xc1n2cF#Q0isH69{}H|Bu4I7doSV6l;|0m8Z$Q zgF+V<7eV{buZ@ykawPY!m}B;P=>GRcb(5qr;3vr>c>ESvIH?XJg)i&s{-$ypsHv+D z+^ODY@>8fVWp&oh01j&QNrh$kB^VL zyZ!73Z%4{eNf9>oY`L=*|ias`}fN zE`W99(8yd~St%N}H*Rn&`mrwJI)e}WY;r5<)I$P4qjPgp!$5Wvm6n=H%qWgbxC>1- zx!vvL*UFW2m~Y3-nZJ-r%+7v1t|iO(&@wV7hq2YDL^DU_#%*}e!J76fYMx1QN&?&r znsCQ}vZs9p+Muf9q9Ux&*498ZwO-I;CP_c{-~AaiR$rOKpT#_V8ySfM{rUU%$NID5 z-T4mhO?^|-Y@O0kd!df=eZtQ3vls&CHm7D=@I`>CY7h9;`T04-%*U5d_IqoKfr3I# zls0O(b(8+?T~g@s@)Fp#`@FpE{(o(sJri1>hTCjL*<4bR38TS;7Jk)|zx7R7PcvKW z)0}YdxgT`0oUC>KKPR#3Us8_bJo)EQ-x&qSogy61k_fxSs^72x91v zT3T8L1{1O4y9)~oJ3Ez4qMV#~knKRGB7E#Dw`R80_lB&F4#l=17kX!Br@g(sUW)6z zX$u8>><5nMKV8{rRDhq~nyMAPqow7&R^k?SvpQSi3L)H4uo%Nfh1Hf_1UPzHw`Z2M zu(Tu8G=spLSnQnklr6 zq@<4@WshVDUReJ5^$B{DlT*QoUf_^c79PcM>emBHFF>TLub-BgNk>OlHs+XSj)Adl zMj+g{@`0g48aORu@4JvjOQ3@&6!QzYy|dj81M&Oe#-$3AC>>m~Q8o5}DP|514k;;2 zbv5wN(W~F{5@KTfJjAxA8_KzI$dS6p&m!FbEyivB-NjUOT#BF3Q9%q043lm*MJRCL z!uLpa1hfIhV|!O=)=Eu9rPmQ01ZGZ4>pPedvk%XD3Ui4fP?`5BMFjJqa7P6OS05Ji z%$ohIHn1-j%Ky*p%l|85^uJb^jAE2l(AK#weBw2&Ak_b(CI1Wk!Et#$#*ohnw_lx^ z(^jc<$nftS<#D!-R`#dW$!rjbs|71`~UxyB7`;yloO5sgm<2*y>**| zh*A7m^?M6|)N=Tre%@c{JKp|z44|^XV;r1AD%=#;V;Jt?!TOKH#Ka?G9te3{Iwe^j zbjB1Uez3v46tW$7;J`H9{y|Fvs-AFUWo7-I{m9DW12dXho%1kW4`(As!dEGtt_X3T zPRK>X|1wDt;3XY4xvG* z7Cdl*?cI~au9oU(-pf(`V(7~YK9#e_Mf!YZA4XGVKpAb-G#5XtL*Vz+iagJ=aXC23 zQ$^`z?80Yx{7q*4 z-4yrpJi|z#gYHsk6pC&_xjXRD&BNuMWT@J4)z#|<8{&O^ukYK$PjiMu0o@*7mNd;Nn zP9Bis1GhWydA?t^*a2i$ETzh-I0-sGaakf3- zO(%rSN6;lCC?g*-vkX4MCoL;@<<$9+x?AvV0tUZKOpwA(uM%4>eA?|m;=R{gde@x1 z@(MMjBA+Q16p&}hp6Zo+dBI42xAKe<>c-q{5@6*-X`pk8GYjg}o3J7pb3gPK8~Yv} z9TZ(TPm6=L(KQny`1r5Q$vU_@+(+w;0_1PVy229|O5JSd;>dTqPsS_Utel*7e@#l( zUxe3xUnAqtjA#@!|H$gt@+dZys8fZ0ls5=(a&o#hkel0@NdCXO@aFg`OEA>krk{E){H*VC+&V-fU&HKR|J&BuqWDUuO%_S+oX4f{V*IQW9;roIJ8KgP zJ)>)ZVcnRV^oM?}>w1~@7Q5J0xr+M*d})11P&6_=$a^vQZzlX6cG;dob-HcaYm|OU zu_2!eB4XOtw&d~)DK(aCf303&t-~f6WLW={yOo$H3)`{aHxJem8zqwMR1?Ig(>E2! z&-arj!F3&HbbPF6d@uqq0S_P6O_knF&m@6v%NQ!aa%N{3v$Lm&Cv9|Py*<6Xo_W|D z_wwA&Om8EH?#*Q=z=frWF*`~k#Ccye-?oxLL~a~4e{-{AG|!)~ zlrG~SISoXQ$X3F|cP_+y=rIn}zO?_n*%KiIAfv}H*!wzp{a`QjC&P@(!e!-`Z7SKQj+PeQ@I(wRu^Hn zZ_LqYxneTC71%|Svu9@t?tF0*e`fpjpHsQd&dwuioR*A~+A}gph}5OyaG71LiV3}J zn`AIx?2+f}?=QGMl%cJArTh*#4R(SX2MgM?h_$;^1?ypWCYZU(K(`}#@>I3iijc?_ zWhB__+kx$a@$$XPM>?99T07=*TbQWt+I4)h&J3fOvr#FBOy8ORkIt&V2gHJk0>AJp z-!T9uRf)%#M}1y9}mG{^>HDQ=cqT9!PzPxR%Ij4MQv z7*m6pCRHqE*d1`iZjKEM#Lba6JTR(gnOOd(J~K1(dKsAc|Fk!wX%)uAF?4nV)tKJt zvjp0oA2jIyi^cIiKiSg=H^!*d8hg!iz$>~F?z*upkRe$YzXjg;irSfJ(Jj>pI^FI| z2wjk=qg0XcF)u~h+S<~ois6jKi0#jHJw&s!v-9#+2IO%(yum#2L{ zf0kUW`9An&P`*9Fx9@wpt5G=o3Cxrdn%DR&t(ZMx4i1=YJfh~l8N9I$a^g!zi^RJ zPcP5^YK2@W5oZa{dt7ExE!r(6`DS;FR~!9}y1Wdz{8z#Syi;a=(u1FWaKT7KU1$FM z`QtqK{t=Tw--=IP;x3(t)6T+z|57*cZ7)K&u=f3b!b{}H&t)AUtXjKteweba8yo#n zxpTtj7xWpLT%G**W#{-p9*XDTYJ*{&ovr1g8^MWgOM+Q5;rV=;tH-aNn-u9sNBfHI zy-p8!iPGVEU-3}_AncU1G!UEl2Lya*ZcTWZq0#Q5vWss;oVEUS5d6EnxU&PoiL^A^ z-dR@6GX=yUPEGDW8I+3l-<`vN=-0qNv`A;FxvTGZM=Q5u857x1XHfL*WIphg6X$@5 zB(tOG5_Lt2T2XnqiLMMXu&=4H&C zPqv@dma@1vIv?)O9%W=?Brr(@q^8IrEEJ_wk1HGz>^y0|l9ds-O|s})%xF0-x1MJF zGWN2bd{RJ}(32y%TDsOv$&s2X%a3Tnd+Rw~ar*u| zoB};OJhcJ!{3^Mb#E3f-)jMr&nr#Yb-IYbQO8Id>=+-vgxx;w}(Aq6xFE`&b0N(rk zc2`%AA{LGwujZoo|EwBjTEyS|eH|U0{DK1II67fNl!3MN@@T29u8z+B{=Rh%)ye}+ zqEkX^3DM5>i=&^r9aookES~ReR&}HmxY_7g;vF{42fupt>TiV}AyiKiuVK=7YkeFe zU)3IdP6)m@^obbbDMNk_i<+C8=Y!8G1YQ(jERd&!mf{8xjXR?al_6FQ(@LetMyCmB zDXEL8I=f=dXg_$+>4AfsTzGPFU3u$Qlt=Py->q>o3{O-Zde{t;(`&YXaAfrK^n|6FP!JauU6o7Re-hcO)!6(Y5J$u< zcypX*sf9%xA#-_Rm8v%8^zGYL-wSM*{rJ#SI$v^HoR{<)LWbgBoG; zkY5TpQE_dUAYO}hXEv!XxQ?2d@j9gp!Y*l(qe3>)07vP-Qj?@B)r>=QB`NH(fU_5I zq1rNNZA=VIzK4AXqJ{W)O{sfPFLgC^ZO7~k3-vddhd1?T*W6;27oF=4M{VZCy-gY% zFF-`=nNV(tb-4bc^qtThhyG%h%Xg zJK}@qikd{65`}NV>%flnUk(}#j<;^zTIuDPR-0OLn_*1c%{lobM{jbyW#;&`6g(6z zwwva`dPQsG@_I6AWY2*~2f=)tsT<8SvvXYkf#@&?xya8{3&WD91?@re{QY>^q2eKz z&kcM@ho$+1Cx&zUC%}>e_KIkQ+V1)X6MWhY31`-X5X~GGHPdKqt-!o-!_By?2QZAe zhd@NPM&-&Xr{;r0TXyvei(zT3*3HVf$^`>EKph2$+#+=}H)n#_b|!gH;@PA*%@^uW zv-?7L?##WJl%mUQdgD|q)^5$pc~qEr89u3_r)TZ>CvCRfrIlXN*|DF-cwbXeD5Y8y zo4M34TK2Wyi2VoX+?m7l4N@<_bHu$Jf#o{UXWLv%Fy`bu4HRSyI9v(*(#>P6ktdx~ z(ID1Gz1H!p&!>!y3ypN-G-%@B`Pp_cXSzP;^p>J2<-ynCuGHNFeu|JE?*ySgVD4&f zEBPUm;~8kDE20zqy=t9~$|!wYrzWRuv)ezbF2{yPKHFrXrR^Cv&bA>u5XDKYaEY)T zU_a$IWkcDTos&a8+tXc|Gz)M>z@AYt%!jwOofl%C9qxp$P(AEc4T?4o0)$WhfBxJq zLR^%KrwL^b01&|a`TLjOd)YLF-Y+uW!vY`z9}*A{n7n00g$nio9;1lE0aNwk zDHGstt|?EFJDn&+!ddwNB~Hcs5RRe)Y6_ie2bEr^_Qz zu8P$+fis7}yGHvjWVV!MXpjAS|AY;6!QpI!7FL*nh{moUgLD15TXp(Co`K7wEjD-U^T61cT2|Bc{21u+3kO!KS-4H?1JBPl-q;pjxy7MIIu49EOSaTS zy$EB|oadA%pYOeJ;BK_d;>|PSskn`V@QECbr}hYu+`6)H>KdH!&CP*3y&?ZzV2w5$ zNu2`v!p7o5Y0X5Ei0+*V)TjC9PS~Z+Ow+l@TDZoyq2V;JnKUb1Art~u!l$Y-8<-Nb zQ#O$v+RKy@G$s|G^nmuD>zHX<>^R63P98WkmSK@-|FF*>*;Xwz_Cc%^?UGIT4L#qL zv}i5j{5uzCC9R&XaxadU#Cw~qsdpPF5Itt|z_#z@*|qrVbLuqh%nUr3eP-b5|L-)h z{jZJ)=Gnl{XlLHJ6GgBN8*>HLb@^90rnBE-QfM3E;&ys@j)W$Ui?C#ibk4@yCIQeq z&ET27?wydx!;C27=1W2GxP;oHT6jZELy@KdSZlZ=7|Ls_PpxVw86 zmnfz7+UegY$4z}%F zBX)W#99|hTZVgR!%|^^l%=QItmi9T%;iL1Odq;|7VPG$!jNj<9pbxSi0kZ%om1#ye zPyT+Y2|hjl+gLsAg^L88zU{NvRDJx7KU{+7Z&{>y?AVzgxftLWss}-I8rNC?6T|^n zKSz3c$j^h#d8~%;rJ#rp)2+|aggLZViR4bNLwk1`0J{J-mg>u#y5B25o?zPf@G5Mz zY$+Zd$=s!*y)Br9q{{`dTq@La6q@Gy+pOe{Nb+x<;={tBVs)BCiO9TTMI$Qo^613a z(tOhovl*#%hmx1qCdoTE2=a(LcXP5+CVBRyN>mA>xB*_vocPIiXO#c!;64#M&N8~C zy8YppNyZM#p%{<;=Rq2yc!HlO2aEjidedqh-<`_A^H_k9or%vzvQ)?FN&Y5@?uZ>< z`m%-OpF?3p0@@Oy6H;%wD$HP@l-jRSx0)1?l-dk#5tKLUm zwF&k-L!^g%NWO^$#bS9_gM;_mMWUL%y6=Fcg2j0h0Z#H3Z@nodiqC(@a&qEiDdV-^ zie&D)&V|yd()TwTb|3fuDW8ASt4z3ge78hjMsYSA1ZoKKV`%ZtTqgM%m>M%Xs}b0j-yQgZioqjwl~wlgu~HE}kTZ zDtSub^$w%;J^DVijw+BM&P0c-D6r z<3|@~d-7+YrKN@Hf`To)_pTeYvJ#?#1h88F&X%l5NJt8l;(B{}{4dWRNo6m3Azv9S zHh|v)>(tcpb9x#(w4`JeAY>3#sJVBs!m7R*Q8U0B91({x6-IToAWEq+$CUjd9OCH% z@!|`VJ{Nkd40Ap$vDd+nZMG=WS8ohcXm-bGP=jv|z3UVSGw)016TQR>H*FRH5dkwZ z%A50V`>V&189a4dENDG~vd|7G`Q6iv1vOq>Ok3NU4a~eCmdq1pObxDAZgF`KpPZ}< zKzn=884zo%#aVzUEb4jko_Run{D_!zIi^gfl#<{D1``Ad8CV1XzEe=1bGfVoe@`$- z%}=2rpS3I}$L#9z!gp(OZ*7=WUfO{;+)-RcZ=3?&K#c(4cXzf`*+EE07Ew+l^8-%~ zQGttOwcgkU6zXb8ukk%Jei>8AiQWUfbc$lHc$9t)_vg9CBVTJ4T_p1}2encg>Io1% zy90mrR*r4|MWQm<4O-$Rf-YQs?qC?qOsNQP&%*f6>=^sD?;om_Lt|CJ@_ZIDb?-Nr z9uh%oqQzNbP5IsUj*7i+4fBH5ixVsuMe+)_7IZtm;1>%mbq45m3(s}KWQBR*ErFZD zx7-i^Jn&8LO2seh8cB@zN*;kwR(Zi&OC<>w+e3fMt7tN2b4WL*_IS8i(JQ6}widi> zGKY2<#Z-rKAamLM<+PpE;_|u4emb7RSp_E_mhAGuvHbuywO{Qel^vkL*g_B`&zp&$ z-|TK|mWg@JSy?HKj;lN4IjvI?VjukV6a4J1Ih{gy-a^5lW~H-mrokO?fSBM7hTUr~ zxDOsXjKIWz1r6@Cg-+aOON92vb<^?UvbN5Qr2zGc$GPe=8yK=bM*cw*fCU!$39BLZ z`45LK!gP0hzmMm9s64m4aO;+#ewqv#Fo1ZWJj&n6!vN`v8`?CV<@ER1uDJB# znu?bEcRHW~H{4*EuXhs?7n$wmzlJ$_D=6BO0{*u0p6hfn(+9h9m%Hs-b| z+~1ZEfc@)>FuD_BdVJXQ$*lE;?A1dn)hwHJ3Goi;Cks*En;4|hgIcZ|xcK@SH}sfd zJn5#S(f8as8=H)XOz%DA24js7xTN52lv`grd($prxxl7UKNY0F%w-YidsC_Cp^5=(V3{l*l)p`_s?<2thc& zvcg0e#RUNYv3>J0)A8sardQhkcTd1k$^-5`hS5kOM(B0ssfK#|=&#DzDNAy` zd74A&(*B;S(#-L9zc$p&C%G5oB&!a47K7V!wNm;>OFCe2gj zI?xl_bLBz-kF)fskhfFYJiM$Rv2&bnwW(?5;_L#L#6r0319hBGT!%bBkOz5)gCu;3 zVxf_#<7r>V=nvcV%9;H#BXt)mL4knZUt8w4#EvmL8{2oHXT*u=hgaRtjkH#sJ>b7l z0JB8~)rGYwt}o5FO$H>Er+8c3Wlde0OSlorp4C+I(_Kdi!IP%#d)Np_c0d?Ukq3~z z!h!aq>HxsQV&*SoZ2dIYwt1L|bDuIcMJhVELlPeXP|Zai;Wk^!UApi+Ks>(-HzUHh zHB1IG^~MB&Yz^xe?|FI7Rh0Bkl+@GHyC6%RL*d3XL46QLJbM_6i!)}41@Now{Ox-l z8^X8H=AG#&-Ue>1&4Hi|aLNP*O=mqWr#~O&jX6<35GLw%KhJ%&Yt9#dO}3S-?;vR4 zv|xNmBavZRN|HgQ5&p+c>g5&es*iRQgqf)@D$JCuh>7>s=W%Y3`EaDq5;6O1Z{ut! zfO&=$+M??4Y=WDb4EYK-yZs;!B0Kw@;DF<4Q5}>ZT_0sSQH{9~3QP?Mp)Di>0AFEN zr7C!2?#s6uTriIn3Nl)9*UjY=cHusX_+aTI0=)*r*(giaK{rL|>nq7urkw~}FdEVv zv9ZpQD4il~2=mu^x<4E{+%e3)P<{7Z0rUj8Xotv??*dc5lTK;-C^k<-^?y#<=VW6~ zM-H8lSnqa6l*oNJtoC((=5dUFS4`+)(%*CcaaF)cU9RTO#qexkjwo}*@1Z4cIT1f4 zAN8Eu>`UImhiQE_mNx_*Wb6w6QLwvd(WjkJ4V!zrO<6GrfmbD?je%9*$+{Iw zyGzT4Ta8D!?@X!%J|yA0W6eoeEsDdX^_HInZHD)0Y~eXJ&$Q25F^_!aq)cJ7>EY_k z5R6bodcgPBN36z#&k-L$rgI}Z*tVk=M_BqzNt4#|=aME)9%(CO9Ui`4+R`PffuxT^ z;%OwF_k4R0RJ}e#`_Rd5XGVlk{00P62v$x!gS8J1|APp)D-qZZc$zaiWtu2*rcvbt zrYWa}XhF*J?=D+!%a7Jp(FGc&v=w^-2eJ zTDzJ7coD$u3=a>l5b(TIQ{DW2BSTBOXo>D{MbQC*Woh*R2r#v^wXgHO`3-D7s+u7WmqWX}CVh?^Rho8u=mu8vLR=-K z6!2gfJ0nHvscJTLBJb|nY&RIbfUTWqkI&h<$fc^SfLC#ZJQWz7;SPcaza8*F-GXWC z9}JGm%x5m)80zeWg46^do)%<*JZW3`(7e#t{MvXu0+uAUl zpFjT|%n95W}j(|AcBYFSz5u2i!H?6QC$Wjd=CV3++*W6P6(_^hm- zsB7h$E?}2)+ZW6J_f+GD<4xawX%cn=kK~)+OLx6=3~+B5WYZ{wEl>qe<9^iC2fVtm;?Zry?c2ApAft+V!!|-h;E62-IHqv@ zaupAM;s_wZ%%enFH^fq=Pw?Y!sLLcLC->kMwzf)WUDm=C_5CS~x}TCGpUNug+YnLR zRhLJUQ;xpA3XY={ya`!aS^cY~w(3>I!fA+|Y5VWu!i4a0dkV4F)>J@D^T_M>n=c?* z5jK$x=vv#{JUjpA74;M-{eWbdfr$yEaNg;c&(6*U9&P}gUGMhF&dw#+vw$W)J3WP^ z_P&4r-tK#Dg=vd`nAqghlu?EL*}s1;qNA$}YZk%r;2>X%*>bu*pH(JJl|Z!$1Z9p5 z(_>=|R$t@6zkp`vVoaZ{sjq(qBwSUPqf8Jd$jW}4ZgA50_s|pLWI#tth=xqCSl}1y zC3#2~)zd?8o}L-Au&@;Fcaq9j(-apLvPnv|%~%ZsMUXvT@9V5A6A(-|oPn-+EOifS z_`Cx>ER@(*hX?)n1qX@-KxRD>6Fa%QIE!KgNe9KBZ$4)VJFOkSU?Ato6>;c{8;O(p z^sVTeU&QhOkofo16A%!n#2Ht#d##dm^6QppvI|Q~O9Qg&;rO zPfwo!!b+N`45>ho(w8wYPiTIFwm_wY96UU6gMol-F03_aaxF|E-T*Hg)T5qCN=ibq zrAXJw>IE>-TV4j3b{ymp3dq^>-%<0KKfVbR&OE%lwlWh~p+Mx3m}b)G%rXEb--t5q ziJ>flSojm2JTA`7!>+DXrz(unh;hjeAYnc|Jxv<8I6D?3^4gvjt~DCIVq#*l)}ut4 znEV_W(K*tw8_4Y1snjpe5^)g+a=t84w~a$~R4txOhuL*HPG}H#sVUU-)YLHd2vEBZ z+eX?QSs(OX#|_O!mDt!3z5M-PE>w+`6%-H@bVByB*DTUNun)Xo_{|G##QHk7nY)9- zI=Dsl z`*Xx;Rmer(`1nzm$(W0a3)u6DI@!StuY;(8g> z%C&DW7Dzsx3rz5_06O_nA<08UM5&tLFL|s5VAZcf{R1)_h%PRQVeMOZK|w)}IUipY z7#)yV?gFu1@AvOuTYSIPvj-eHj`RcZe&EsTSFa@e_m{zIAY=RimOvZPlTq>1B0#Wb zc=#bFXGwFj9{?oaFj;db?Z`bOicm&M1Bts;EcPg?a(p$sh=$CKN%}brHFdY^M_!;f zs@Fkmj+KAik?R9ma1K&qBDnCS-)}H8N;ee|6$~)xG`?$a;4od!wXzCsCj_X2bg_n( z7U!LeWn$8>urMH3?in29<>GqynP^JS(6HIEE0z~1nSj*4Lf@%jIy5viL(uN>bhAq7 z^jdc%EZhJj=9QI|z({m;6#*3)5ZP~Kbw@=mk?Vbzm|0f9stN7wu`Hy)**#SoZYawW z3^zW^yqbu?-w5dmqo)GbUi6tC>*=wg^@{52zrqo?N2GA7N$4d}0*b(yG8|ToMM291 zp4`XLdvb$KKoUIB#xMJLQ^nd3JSiYUh7rH{2K0R&l~}H&eu+S$9u_Buz!j0b*Lf}e zdjYBmRfqzKSP_E%&sLoLI^b-(2`eZYa%#|EIo1-DQN+7si!8Ib_jV}RPL!PuoUir8D6v6J7%#s;h)$kC2C zHUON0abLjR?&9wU+ZZSoE-o&F>Eg=EuT}RTMH@KsWTa5_V4_^*9nb_9KaOIYSE2n6 zA!mGzq%5qD*Rovo}2bV>n^ z{VwuysMcdi$%nm@A6$2FnW_a}3Jc=m2Ci*(7O(_#(8gH#@z=W{L%na`(m>6Ad-po| zcQMCvujFk^O&MnU`1^q}9YTS?qp;SX%A79l_ukUidX1vwcRbuc?Ukw)Nem(rMl`Tl zINw&}wfllx)Kjol6~5nuZ3F_a;+*fHmA-s3Lt+hw(NR(1LNe!nZsdTu0Du%L z6tq#teb`&L1nw(L9%5l(2|JBjPqa1H28HdIxM8~ZiMV{Xr&DKtfr4;2oZv-13KjbF z?UyeK(DkV8>2*2e=?#p*C4y9Z`k1{@QD)|kKYr+b&jJ|Y5fDv_iM%L2JO?fpb#73l3O{+R7Uid3)kcejovX-ehK~a6PBA^xU7qJ5ZzB{2)Kky2{(>+{wkoIdn%%p;bM=r&;>pKQ zfH;zF;;>C~aC5(UO0$&wl+?h1_FC-Y%>03M0|+PxA#mvdHjxE%Qa}s@E@#*aWd;E6 zr=YO*OxzR2%GjgFzOBwQmn|KXInGb?1KhS#R#rYG11L!&O9c1k&FSgs56#Wb0P2^L z%9(8k_df=+AAzH*Q;LM%|1{T@iH0Zpn4qKCLkW(i^atqCdv%lILoe0NO z!4QVSyz$CyFYz76)=U@f<2$|>`9+cjL|^JeEV1}yWM%4ZkAYeL{=Z<>zw@Ery?Yn2 zXM+qI8bUx!W^YuO7KOOAL%&i!%*<>4+d+x`!an12NC2G0nA0n46m~Xt<8 literal 0 HcmV?d00001 diff --git a/docs/concepts/_snippets/decorator_ctx.png b/docs/concepts/_snippets/decorator_ctx.png new file mode 100644 index 0000000000000000000000000000000000000000..b69a2a486eb2413163f1fb8c86d50edfa891e273 GIT binary patch literal 45585 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 J?=D#d{~vL~mt+6{ literal 0 HcmV?d00001 diff --git a/docs/concepts/_snippets/decorator_ctx.py b/docs/concepts/_snippets/decorator_ctx.py new file mode 100644 index 000000000..797f82cbc --- /dev/null +++ b/docs/concepts/_snippets/decorator_ctx.py @@ -0,0 +1,33 @@ +import pandas as pd +import xgboost + +from hamilton.function_modifiers import load_from, save_to, source + + +# source("data_path") allows to read the input value for `data_path` +@load_from.parquet(path=source("data_path")) +def preprocessed_df(raw_df: pd.DataFrame) -> pd.DataFrame: + """preprocess raw data""" + return ... + + +@save_to.json(path=source("model_path")) +def model(preprocessed_df: pd.DataFrame) -> xgboost.XGBModel: + """Train model on preprocessed data""" + return ... + + +if __name__ == "__main__": + import __main__ + + from hamilton import driver + + dr = driver.Builder().with_modules(__main__).build() + + data_path = "..." + model_path = "..." + inputs = dict(data_path=data_path, model_path=model_path) + final_vars = ["save.model", "model"] + results = dr.execute(final_vars, inputs=inputs) + # results["model"] <- the model + # results["save.model"] <- metadata from saving the model diff --git a/docs/concepts/_snippets/dynamic_materializer_ctx.png b/docs/concepts/_snippets/dynamic_materializer_ctx.png new file mode 100644 index 0000000000000000000000000000000000000000..33db0a5176dfc5218d23bc1c3fae0417300632e4 GIT binary patch literal 19003 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 NSxH5Saxnw{{{}t7P~89k literal 0 HcmV?d00001 diff --git a/docs/concepts/_snippets/materializer_ctx.py b/docs/concepts/_snippets/dynamic_materializer_ctx.py similarity index 51% rename from docs/concepts/_snippets/materializer_ctx.py rename to docs/concepts/_snippets/dynamic_materializer_ctx.py index 12e2e4eef..a6777b232 100644 --- a/docs/concepts/_snippets/materializer_ctx.py +++ b/docs/concepts/_snippets/dynamic_materializer_ctx.py @@ -18,16 +18,18 @@ def model(preprocessed_df: pd.DataFrame) -> xgboost.XGBModel: from hamilton import driver from hamilton.io.materialization import from_, to - # this registers DataSaver and DataLoader objects - from hamilton.plugins import pandas_extensions, xgboost_extensions # noqa: F401 - - dr = driver.Builder().with_modules(__main__).build() - data_path = "..." model_dir = "..." materializers = [ - from_.parquet(path=data_path, target="raw_df"), - to.json(path=f"{model_dir}/model.json", dependencies=["model"], id="model__json"), + from_.parquet(target="raw_df", path=data_path), + to.json( + id="model__json", # name of the DataSaver node + dependencies=["model"], + path=f"{model_dir}/model.json", + ), ] - - dr.materialize(*materializers) + dr = driver.Builder().with_modules(__main__).build() + # executes all `to.` materializers; use `additional_vars` to execute other nodes + metadata, results = dr.materialize(*materializers, additional_vars=["model"]) + # results["model"] <- the model + # metadata["model__json"] <- metadata from saving the model diff --git a/docs/concepts/_snippets/materializers.png b/docs/concepts/_snippets/materializers.png new file mode 100644 index 0000000000000000000000000000000000000000..5b9389982a827fcf49de493b901142acb6105a38 GIT binary patch literal 29435 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 literal 0 HcmV?d00001 diff --git a/docs/concepts/_snippets/node_ctx.py b/docs/concepts/_snippets/node_ctx.py index c6bab9ebd..563522788 100644 --- a/docs/concepts/_snippets/node_ctx.py +++ b/docs/concepts/_snippets/node_ctx.py @@ -33,6 +33,5 @@ def save_model(model: xgboost.XGBModel, model_dir: str) -> None: model_dir = "..." inputs = dict(data_path=data_path, model_dir=model_dir) final_vars = ["save_model"] - results = dr.execute(final_vars, inputs=inputs) # results["save_model"] == None diff --git a/docs/concepts/_snippets/static_materializer_ctx.png b/docs/concepts/_snippets/static_materializer_ctx.png new file mode 100644 index 0000000000000000000000000000000000000000..a7602f13332c97b51cf1e80101123230ea768afe GIT binary patch literal 30138 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 pd.DataFrame: + """preprocess raw data""" + return ... + + +def model(preprocessed_df: pd.DataFrame) -> xgboost.XGBModel: + """Train model on preprocessed data""" + return ... + + +if __name__ == "__main__": + import __main__ + + from hamilton import driver + from hamilton.io.materialization import from_, to + + data_path = "..." + model_dir = "..." + materializers = [ + from_.parquet(target="raw_df", path=data_path), + to.json( + id="model__json", # name of the DataSaver node + dependencies=["model"], + path=f"{model_dir}/model.json", + ), + ] + dr = driver.Builder().with_modules(__main__).with_materializers(*materializers).build() + results = dr.execute(["model", "model__json"]) + # results["model"] <- the model + # results["model__json"] <- metadata from saving the model diff --git a/docs/concepts/builder.rst b/docs/concepts/builder.rst index eb3a0dd2e..6c079d213 100644 --- a/docs/concepts/builder.rst +++ b/docs/concepts/builder.rst @@ -111,6 +111,58 @@ This is directly related to the ``@config`` function decorator (see :ref:`config .build() ) + dr.display_all_functions("dag.png") + + +.. image:: ./_snippets/config_when.png + :align: center + + +with_materializers() +____________________ + +Adds `DataSaver` and `DataLoader` nodes to your dataflow. This allows to visualize these nodes using ``Driver.display_all_functions()`` and be executed by name with ``Driver.execute()``. More details on the :doc:`materialization` documentation page. + +.. code-block:: python + + # my_dataflow.py + import pandas as pd + from hamilton.function_modifiers import config + + def clean_df(raw_df: pd.DataFrame) -> pd.DataFrame: + return ... + + def features_df(clean_df: pd.DataFrame) -> pd.DataFrame: + return ... + +.. code-block:: python + + # run.py + from hamilton import driver + from hamilton.io.materialization import from_, to + import my_dataflow + + loader = from_.parquet(target="raw_df", path="/my/raw_file.parquet") + saver = to.parquet( + id="features__parquet", + dependencies=["features_df"], + path="/my/feature_file.parquet" + ) + + dr = ( + driver.Builder() + .with_modules(my_dataflow) + .with_materializers(loader, saver) + .build() + ) + dr.display_all_functions("dag.png") + + dr.execute(["features__parquet"]) + +.. image:: ./_snippets/materializers.png + :align: center + + with_adapters() --------------- diff --git a/docs/concepts/materialization.rst b/docs/concepts/materialization.rst index 2a26892c4..73758492d 100644 --- a/docs/concepts/materialization.rst +++ b/docs/concepts/materialization.rst @@ -6,102 +6,117 @@ So far, we executed our dataflow using the ``Driver.execute()`` method, which ca On this page, you'll learn: -- The difference between ``.execute()`` and ``.materialize()`` +- How to load and data in Hamilton - Why use materialization -- What are DataSaver and DataLoader objects +- What are ``DataSaver`` and ``DataLoader`` objects +- The difference between ``.execute()`` and ``.materialize()`` - The basics to write your own materializer Different ways to write the same dataflow ----------------------------------------- -Below are 3 ways to write a dataflow that: +Below are 5 ways to write a dataflow that: 1. loads a dataframe from a parquet file 2. preprocesses the dataframe 3. trains a machine learning model 4. saves the trained model -The first two options use ``Driver.execute()`` and the latter ``Driver.materialize()``. Notice where in the code data is loaded and saved and how it affects the dataflow. +The first two options don't use the concept of materialization and the next three do. + +Without materialization +----------------------- -.. table:: Model training +.. table:: :align: left - +----------------------------------------------+-----------------------------------------------+--------------------------------------------------------+ - | Nodes / dataflow context | Driver context | Materialization | - +==============================================+===============================================+========================================================+ - | .. literalinclude:: _snippets/node_ctx.py | .. literalinclude:: _snippets/driver_ctx.py | .. literalinclude:: _snippets/materializer_ctx.py | - | | | | - +----------------------------------------------+-----------------------------------------------+--------------------------------------------------------+ - | .. image:: _snippets/node_ctx.png | .. image:: _snippets/driver_ctx.png | .. image:: _snippets/materializer_ctx.png | - | :width: 500px | :width: 500px | :width: 500px | - +----------------------------------------------+-----------------------------------------------+--------------------------------------------------------+ + +----------------------------------------------+-----------------------------------------------+ + | 1) From nodes | 2) From ``Driver`` | + +==============================================+===============================================+ + | .. literalinclude:: _snippets/node_ctx.py | .. literalinclude:: _snippets/driver_ctx.py | + | | | + +----------------------------------------------+-----------------------------------------------+ + | .. image:: _snippets/node_ctx.png | .. image:: _snippets/driver_ctx.png | + | :width: 500px | :width: 500px | + +----------------------------------------------+-----------------------------------------------+ -As explained previously, ``Driver.execute()`` walks the graph to compute the list of nodes you requested by name. For ``Driver.materialize()``, you give it a list of data savers (``from_``) and data loaders (``to``). Each one will add a node to the dataflow before execution. +Observations: -.. note:: +1. These two approaches load and save data using ``pandas`` and ``xgboost`` without any Hamilton constructs. These methods are transparent and simple to get started, but as the number of node grows (or across projects) defining one node per parquet file to load introduces a lot of boilerplate. +2. Using **1) from nodes** improves visibility by including loading & saving in the dataflow (as illustrated). +3. Using **2) from ``Driver``** facilitates modifying loading & saving before code execution when executing the code, without modifying the dataflow itself. It is particularly useful when moving from development to production. - ``Driver.materialize()`` can do everything ``Driver.execute()`` does, and more. It can receive ``inputs`` and ``overrides``. Instead of using ``final_vars``, you can use ``additional_vars`` to request nodes that you don't want to materialize/save. +Limitations +~~~~~~~~~~~~ -Why use materialization ------------------------ +Materializations aims to solve 3 limitations: + +1. **Redundancy**: deduplicate loading & saving code to improve maintainability and debugging +2. **Observability**: include loading & saving in the dataflow for full observability and allow hooks +3. **Flexibility**: change the loading & saving behavior without editing the dataflow -Let's compare the benefits of the 3 different approaches -Nodes / dataflow context -~~~~~~~~~~~~~~~~~~~~~~~~ +With materialization +-------------------- -This approach defines data loading and saving as part of the dataflow and uses ``Driver.execute()``. It is usually the simplest approach and the one you should start with. +.. table:: + :align: left -Benefits + +-------------------------------------------------------------+-------------------------------------------------------------+-------------------------------------------------+ + | 3) Static materializers | 4) Dynamic materializers | 5) Function modifiers | + +=============================================================+=============================================================+=================================================+ + | .. literalinclude:: _snippets/static_materializer_ctx.py | .. literalinclude:: _snippets/dynamic_materializer_ctx.py | .. literalinclude:: _snippets/decorator_ctx.py | + | | | | + +-------------------------------------------------------------+-------------------------------------------------------------+-------------------------------------------------+ + | .. image:: _snippets/static_materializer_ctx.png | .. image:: _snippets/dynamic_materializer_ctx.png | .. image:: _snippets/decorator_ctx.png | + | :width: 500px | :width: 500px | :width: 500px | + +-------------------------------------------------------------+-------------------------------------------------------------+-------------------------------------------------+ -- the functions ``raw_df()`` and ``save_model()`` are transparent as to how they load/save data -- can easily change data location using the strings ``data_path`` and ``model_dir`` as inputs -- all operations are part of the dataflow -Limitations +Static materializers +~~~~~~~~~~~~~~~~~~~~ -- need to write a unique function for each loaded parquet file and saved model. To reduce code duplication, one could write a utility function ``_load_parquet()`` -- can be too restrictive as to how to load data. Using ``override`` in the ``.execute()`` call can add flexibility. +Passing ``from_`` and ``to`` Hamilton objects to ``Builder().with_materializers()`` injects into the dataflow standardized nodes to load and save data. It solves the 3 limitations highlighted in the previous section: -Driver context -~~~~~~~~~~~~~~ +1. Redundancy ✅: Using the ``from_`` and ``to`` Hamilton constructs reduces the boilerplate to load and save data from common formats (JSON, parquet, CSV, etc.) and to interact with 3rd party libraries (pandas, matplotlib, xgboost, dlt, etc.) +2. Observability ✅: Loaders and savers are part of the dataflow. You can view them with ``Driver.display_all_functions()`` and execute nodes by requesting them with ``Driver.execute()``. +3. Flexibility ✅: The loading and saving behavior is decoupled from the dataflow and can modified easily when creating the ``Driver`` and executing code. -This approach loads and saves data outside the dataflow and uses ``Driver.execute()``. Since the Driver is responsible for executing your dataflow, it makes sense to handle data loading/saving in the context of the "driver code" (e.g., ``run.py``) if they change often. -Benefits +Dynamic materializers +~~~~~~~~~~~~~~~~~~~~~ -- Driver users is responsible for loading/saving data -- fewer dataflow functions to define and maintain -- the functions for ``raw_df()`` and ``save_model()`` can live in another Python module that you can optionally build the Driver with. +The dataflow is executed by passing ``from_`` and ``to`` objects to ``Driver.materialize()`` instead of the regular ``Driver.execute()``. This approach ressembles **2) from Driver**: -Limitations +.. note:: -- add complexity to the "driver code". -- lose the benefits of Hamilton for loading and saving operations (visualize, lifecycle hook, etc.) -- to add flexibility to data loading/saving, one can adopt the **nodes/dataflow context** approach and add functions with ``@config`` for alternative implementations (see :ref:`config-decorators`). + ``Driver.materialize()`` can receive data savers (``from_``) and loaders (``to``) and will execute all ``to`` passed. Like ``Driver.execute()``, it can receive ``inputs``, and ``overrides``, but instead of ``final_vars`` it receives ``additional_vars``. +1. Redundancy ✅: Uses ``from_`` and ``to`` Hamilton constructs. +2. Observability 🚸: Materializers are visible with ``Driver.visualize_materialization()``, but can't be introspected otherwise. Also, you need to rely on ``Driver.materialize()`` which has a different call signature. +3. Flexibility ✅: Loading and saving is decoupled from the dataflow. -Materialization -~~~~~~~~~~~~~~~ +.. note:: -This approach tries to strike a balance between the two previous methods and uses ``Driver.materialize()``. + Using static materializers is typically preferrable. Static and dynamic materializers can be used together with ``dr = Builder.with_materializers().build()`` and later ``dr.materialize()``. -Unique benefits +Function modifiers +~~~~~~~~~~~~~~~~~~ -- Use the Hamilton logic to combine nodes (more on that later) -- Get tested code for common data loading and saving out-of-the-box (e.g., JSON, CSV, Parquet, pickle) -- Easily save the same node to multiple formats +By adding ``@load_from`` and ``@save_to`` function modifiers (:ref:`loader-saver-decorators`) to Hamilton functions, materializers are generated when using ``Builder.with_modules()``. This approach ressembles **1) from Driver**: -Benefits +.. note:: -- Flexibility for Driver users to change data location -- Less dataflow functions to define and maintain -- All operations are part of the dataflow + Under the hood, the ``@load_from`` modifier uses the same code as ``from_`` to load data, same for ``@save_to`` and ``to``. -Limitations +1. Redundancy 🚸: Using ``@load_from`` and ``@save_to`` reduces redundancy. However, to make available to multiple nodes a loaded table, you would need to decorate each node with the same ``@save_to``. Also, it might be impractical to decorate dynamically generated nodes (e.g., when using the ``@parameterize`` function modifier). +2. Observability ✅: Loaders and savers are part of the dataflow. +3. Flexibility 🚸: You can modify the path and materializer kwargs at runtime using ``source()`` in the decorator definition, but you can't change the format itself (e.g., from parquet to CSV). + +.. note:: + + It can be desirable to couple loading and saving to the dataflow using function modifiers. It makes it clear when reading the dataflow definition which nodes should load or save data using external sources. -- Writing a custom DataSaver or DataLoader requires more effort than adding a function to the dataflow. -- Adds *some* complexity to the Driver (e.g., ``run.py``). DataLoader and DataSaver ------------------------ @@ -118,8 +133,3 @@ Here are simplified snippets for saving and loading an XGBoost model to/from JSO +----------------------------------------------+-----------------------------------------------+ To define your own DataSaver and DataLoader, the Hamilton `XGBoost extension `_ provides a good example - -``@load_from`` and ``@save_to`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Also, the data loaders and savers power the ``@load_from`` and ``@save_to`` :ref:`loader-saver-decorators` diff --git a/hamilton/driver.py b/hamilton/driver.py index 3d9f8d535..30d6c7537 100644 --- a/hamilton/driver.py +++ b/hamilton/driver.py @@ -1745,7 +1745,7 @@ def with_adapters(self, *adapters: lifecycle_base.LifecycleAdapter) -> "Builder" return self def with_materializers( - self, *materializers: typing.Sequence[Union[ExtractorFactory, MaterializerFactory]] + self, *materializers: Union[ExtractorFactory, MaterializerFactory] ) -> "Builder": """Add materializer nodes to the `Driver` The generated nodes can be referenced by name in `.execute()` diff --git a/tests/resources/test_for_materialization.py b/tests/resources/test_for_materialization.py index 2045ca298..df39b7ab4 100644 --- a/tests/resources/test_for_materialization.py +++ b/tests/resources/test_for_materialization.py @@ -13,4 +13,4 @@ def json_to_save_2() -> dict: def expects_loader(external: dict) -> dict: - return external \ No newline at end of file + return external diff --git a/tests/test_hamilton_driver.py b/tests/test_hamilton_driver.py index d30beb639..b199ef43d 100644 --- a/tests/test_hamilton_driver.py +++ b/tests/test_hamilton_driver.py @@ -12,15 +12,15 @@ Variable, ) from hamilton.execution import executors -from hamilton.io.materialization import to, from_ +from hamilton.io.materialization import from_, to import tests.resources.cyclic_functions import tests.resources.dummy_functions import tests.resources.dynamic_parallelism.parallel_linear_basic import tests.resources.tagging import tests.resources.test_default_args -import tests.resources.very_simple_dag import tests.resources.test_for_materialization +import tests.resources.very_simple_dag """This file tests driver capabilities. Anything involving execution is tested for multiple executors/driver configuration. @@ -579,10 +579,7 @@ def test_builder_materializer_and_execution_materializer(tmp_path): .with_materializers(static_saver) .build() ) - metadata, additional = dr.materialize( - dynamic_saver, - additional_vars=["static_saver"] - ) + metadata, additional = dr.materialize(dynamic_saver, additional_vars=["static_saver"]) assert "dynamic_saver" in metadata.keys() assert "static_saver" in additional.keys()