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,726 @@
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
+ """
19
+ This module houses functions and tools to interact with off-the-shelf
20
+ dataflows.
21
+
22
+ TODO: expect this to have a CLI interface in the future.
23
+ """
24
+
25
+ import importlib
26
+ import json
27
+ import logging
28
+ import os
29
+ import shutil
30
+ import sys
31
+ import time
32
+ import urllib.error
33
+ import urllib.request
34
+ from collections.abc import Callable
35
+ from types import ModuleType
36
+ from typing import TYPE_CHECKING, Dict, List, NamedTuple, Optional, Tuple, Type, Union
37
+
38
+ from hamilton import driver
39
+
40
+ if TYPE_CHECKING:
41
+ import builtins
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+ """
46
+ Paths that we care about.
47
+ Assumptions:
48
+ - ~/.hamilton/dataflows/ is where we store dataflows. This is fine because we should be able to save locally.
49
+ - This will act as a cache for dataflows.
50
+ - People can modify this location globally.
51
+ - Directory should mirror github (e.g. contrib/hamilton/contrib/user/{user}/{dataflow})
52
+ - We don't have to deal with the python path because this module knows how to import it.
53
+
54
+ TODOs:
55
+ - finish init
56
+ - make sure it works on windows
57
+ - make functions more robust (see comments in functions).
58
+ """
59
+ COMMON_PATH = "{commit_ish}/contrib/hamilton/contrib"
60
+ BASE_URL = f"https://raw.githubusercontent.com/apache/hamilton/{COMMON_PATH}"
61
+ DATAFLOW_FOLDER = os.path.expanduser("~/.hamilton/dataflows")
62
+ USER_PATH = DATAFLOW_FOLDER + "/" + COMMON_PATH + "/user/{user}/{dataflow}"
63
+ OFFICIAL_PATH = DATAFLOW_FOLDER + "/" + COMMON_PATH + "/dagworks/{dataflow}"
64
+
65
+
66
+ def _track_function_call(call_fn: Callable) -> Callable:
67
+ """No-op decorator kept for backwards compatibility.
68
+
69
+ :param call_fn: the function.
70
+ :return: the same function, unwrapped.
71
+ """
72
+ return call_fn
73
+
74
+
75
+ def _track_download(is_official: bool, user: str | None, dataflow_name: str, version: str):
76
+ """No-op. Telemetry has been removed."""
77
+ pass
78
+
79
+
80
+ def _get_request(url: str) -> tuple[int, str]:
81
+ """Makes a GET request to the given URL and returns the status code and response data.
82
+
83
+ :param url: the url to make the request to.
84
+ :return: tuple of status code and text response decoded as utf-8.
85
+ """
86
+ try:
87
+ with urllib.request.urlopen(url) as response:
88
+ data = response.read().decode("utf-8")
89
+ return response.status, data
90
+
91
+ except urllib.error.HTTPError as e:
92
+ return e.code, e.reason
93
+
94
+
95
+ @_track_function_call
96
+ def clear_storage():
97
+ """Clears all the data under DATAFLOW_FOLDER. By default its "~/.hamilton/dataflows"."""
98
+ if os.path.exists(DATAFLOW_FOLDER):
99
+ shutil.rmtree(DATAFLOW_FOLDER)
100
+ logger.info(f"Cleared storage at {DATAFLOW_FOLDER}")
101
+ else:
102
+ logger.info(f"Folder {DATAFLOW_FOLDER} does not exist.")
103
+
104
+
105
+ # want TTL cache on this call to not get rate limited.
106
+ last_time_called = None
107
+ last_resolve_value = None
108
+
109
+
110
+ @_track_function_call
111
+ def resolve_latest_branch_commit(branch: str = "main") -> str:
112
+ """Resolves https://api.github.com/repos/DAGWorks-Inc/hamilton/git/refs/heads/{branch}
113
+
114
+ :return: commit for what "main" is.
115
+ """
116
+ global last_time_called
117
+ global last_resolve_value
118
+ if last_time_called is not None and last_resolve_value is not None:
119
+ if (last_time_called + 60.0) > time.time():
120
+ logger.info("using cached resolve_latest")
121
+ return last_resolve_value
122
+ url = f"https://api.github.com/repos/DAGWorks-Inc/hamilton/git/refs/heads/{branch}"
123
+ status_code, text = _get_request(url)
124
+ # response = requests.get(url)
125
+ if status_code != 200:
126
+ raise ValueError(
127
+ f"Failed to resolve latest commit for branch {branch}:\n{status_code}\n{text}"
128
+ )
129
+ commit_sha = json.loads(text)["object"]["sha"]
130
+ last_time_called = time.time()
131
+ last_resolve_value = commit_sha
132
+ return commit_sha
133
+
134
+
135
+ @_track_function_call
136
+ def latest_commit(dataflow: str, user: str = None) -> str:
137
+ """Determines the latest commit for a dataflow.
138
+
139
+ This is useful to know if you want to pull the latest version of a dataflow.
140
+
141
+ :param dataflow: the string name of the dataflow
142
+ :param user: the name of the user. None if official.
143
+ :return: the commit sha.
144
+ """
145
+ if user:
146
+ url = f"https://hub.dagworks.io/commits/Users/{user}/{dataflow}/commit.txt"
147
+ else:
148
+ url = f"https://hub.dagworks.io/commits/DAGWorks/{dataflow}/commit.txt"
149
+ status_code, text = _get_request(url)
150
+ if status_code != 200:
151
+ raise ValueError(
152
+ f"Failed to resolve latest commit [{url}] for {user}/{dataflow}:\n{status_code}\n{text}"
153
+ )
154
+ chunk_index = text.find("[commit::")
155
+ if chunk_index < 0:
156
+ raise ValueError("No commit found")
157
+ # Gets the latest commit from potentially multiple.
158
+ commit = text[chunk_index + len("[commit::") :]
159
+ commit_sha = commit[: commit.find("]")]
160
+ return commit_sha
161
+
162
+
163
+ @_track_function_call
164
+ def pull_module(dataflow: str, user: str = None, version: str = "latest", overwrite: bool = False):
165
+ """Pulls a dataflow module.
166
+
167
+ Saves to hamilton.dataflow.USER_PATH. An import should just work right after doing this.
168
+
169
+ It performs the following:
170
+
171
+ 1. Creates a URL to pull from github.
172
+ 2. Pulls the code for the dataflow.
173
+ 3. Save to the local location based on hamilton.dataflow.USER_PATH.
174
+
175
+ :param dataflow: the dataflow name.
176
+ :param user: the user's github handle.
177
+ :param version: the commit version. "latest" will resolve to the most recent commit, else pass \
178
+ a commit SHA.
179
+ :param overwrite: whether to overwrite. Default is False.
180
+ """
181
+ if version == "latest":
182
+ version = latest_commit(dataflow, user)
183
+ if user:
184
+ _track_download(False, user, dataflow, version)
185
+ logger.info(f"pulling dataflow {user}/{dataflow} with version {version}")
186
+ local_file_path = USER_PATH.format(commit_ish=version, user=user, dataflow=dataflow)
187
+ else:
188
+ _track_download(True, None, dataflow, version)
189
+ logger.info(f"pulling official dataflow {dataflow} with version {version}")
190
+ local_file_path = OFFICIAL_PATH.format(commit_ish=version, dataflow=dataflow)
191
+
192
+ h_files = [
193
+ "__init__.py",
194
+ "requirements.txt",
195
+ "README.md",
196
+ "valid_configs.jsonl",
197
+ "tags.json",
198
+ ]
199
+
200
+ if os.path.exists(local_file_path) and not overwrite:
201
+ raise ValueError(
202
+ f"Dataflow {user}/{dataflow} with version {version} already exists locally. Not downloading."
203
+ )
204
+ os.makedirs(local_file_path, exist_ok=True)
205
+ for h_file in h_files:
206
+ if user:
207
+ url = BASE_URL.format(commit_ish=version) + f"/user/{user}/{dataflow}/{h_file}"
208
+ else:
209
+ url = BASE_URL.format(commit_ish=version) + f"/dagworks/{dataflow}/{h_file}"
210
+ # response = requests.get(url)
211
+ status_code, text = _get_request(url)
212
+ if status_code == 404:
213
+ raise ValueError(f"Dataflow {user}/{dataflow}/{h_file} not found at {url}.")
214
+ elif status_code != 200:
215
+ raise ValueError(
216
+ f"Dataflow {user}/{dataflow} returned status code {status_code}:\n{text}"
217
+ )
218
+ file_contents = text
219
+ if os.path.exists(os.path.join(local_file_path, h_file)):
220
+ logger.info(f"Warning: overwriting {h_file} in {local_file_path}")
221
+ with open(os.path.join(local_file_path, h_file), "w") as f:
222
+ f.write(file_contents)
223
+ logger.info(f"wrote {h_file} to {local_file_path}")
224
+
225
+
226
+ @_track_function_call
227
+ def import_module(
228
+ dataflow: str, user: str = None, version: str = "latest", overwrite: bool = False
229
+ ) -> ModuleType:
230
+ """Pulls & imports dataflow code from github and returns a module.
231
+
232
+ .. code-block:: python
233
+
234
+ from hamilton import dataflows, driver
235
+ # downloads into ~/.hamilton/dataflows and loads the module -- WARNING: ensure you know what code you're importing!
236
+ # NAME_OF_DATAFLOW = dataflow.import_module("NAME_OF_DATAFLOW") # if using official dataflow
237
+ NAME_OF_DATAFLOW = dataflow.import_module("NAME_OF_DATAFLOW", "NAME_OF_USER")
238
+ dr = (
239
+ driver.Builder()
240
+ .with_config({}) # replace with configuration as appropriate
241
+ .with_modules(NAME_OF_DATAFLOW)
242
+ .build()
243
+ )
244
+ # execute the dataflow, specifying what you want back. Will return a dictionary.
245
+ result = dr.execute(
246
+ [NAME_OF_DATAFLOW.FUNCTION_NAME, ...], # this specifies what you want back
247
+ inputs={...} # pass in inputs as appropriate
248
+ )
249
+
250
+ :param dataflow: the name of the dataflow.
251
+ :param user: Optional. If none it assumes official.
252
+ :param version: the version to get. "latest" will resolve to the most recent commit. \
253
+ Otherwise pass a the commit SHA you want to pull.
254
+ :param overwrite: whether to overwrite the local path. Default is False.
255
+ :return: a Module that you can then pass to Hamilton.
256
+ """
257
+ if version == "latest":
258
+ version = latest_commit(dataflow, user)
259
+ if user:
260
+ local_file_path = (
261
+ USER_PATH.format(commit_ish=version, user=user, dataflow=dataflow) + "/__init__.py"
262
+ )
263
+ else:
264
+ local_file_path = (
265
+ OFFICIAL_PATH.format(commit_ish=version, dataflow=dataflow) + "/__init__.py"
266
+ )
267
+ if not os.path.exists(local_file_path) or overwrite:
268
+ pull_module(dataflow, user, version=version, overwrite=overwrite)
269
+ else:
270
+ logger.info(f"Found {user}/{dataflow} with version {version} locally. Not downloading.")
271
+
272
+ if dataflow in sys.modules:
273
+ logger.info(
274
+ f"Warning: overwriting existing {dataflow} module with version {sys.modules[dataflow].__version__}"
275
+ )
276
+ spec = importlib.util.spec_from_file_location(dataflow, local_file_path)
277
+ module = importlib.util.module_from_spec(spec)
278
+ module.__version__ = version
279
+ sys.modules[dataflow] = module
280
+ spec.loader.exec_module(module)
281
+ return module
282
+
283
+
284
+ class InspectResult(NamedTuple):
285
+ version: str # git commit sha/package version
286
+ user: str # github user URL
287
+ dataflow: str # dataflow URL
288
+ python_dependencies: list[str] # python dependencies
289
+ configurations: list[str] # configurations for the dataflow stored as a JSON string
290
+
291
+
292
+ @_track_function_call
293
+ def inspect(dataflow: str, user: str = None, version: str = "latest") -> InspectResult:
294
+ """Inspects a dataflow for information.
295
+
296
+ This is a helper function to get information about a dataflow that exists locally.
297
+ It does not get more information because we don't want to assume we can import the module.
298
+
299
+ .. code-block:: python
300
+
301
+ from hamilton import dataflows
302
+
303
+ info = dataflows.inspect("text_summarization", "zilto")
304
+
305
+ :param dataflow: the dataflow name.
306
+ :param user: the github name of the user. None for DAGWorks official.
307
+ :param version: the version to inspect. "latest" will resolve to the most recent commit, else pass \
308
+ a commit SHA.
309
+ :return: hamilton.dataflow.InspectResult object that contains version, user URL, dataflow URL, python dependencies, configurations.
310
+ """
311
+ if version == "latest":
312
+ version = latest_commit(dataflow, user)
313
+
314
+ if user:
315
+ local_file_path = USER_PATH.format(commit_ish=version, user=user, dataflow=dataflow)
316
+ dataflow_url = (
317
+ f"https://github.com/apache/hamilton/tree/{version}/contrib/contrib/user/{user}/{dataflow}",
318
+ )
319
+ user_url = f"https://github.com/{user}"
320
+ else:
321
+ local_file_path = OFFICIAL_PATH.format(commit_ish=version, dataflow=dataflow)
322
+ dataflow_url = (
323
+ f"https://github.com/apache/hamilton/tree/{version}/contrib/contrib/dagworks/{dataflow}",
324
+ )
325
+ user_url = None
326
+ if not os.path.exists(local_file_path):
327
+ raise ValueError(
328
+ f"Dataflow {user or 'dagworks'}/{dataflow} with version {version} does not exist locally. Not inspecting."
329
+ )
330
+ # return dictionary of python deps, inputs, nodes, designated outputs, commit hash
331
+ info: dict[str, str | builtins.list[dict] | builtins.list[str]] = {
332
+ "version": version,
333
+ "user": user_url,
334
+ "dataflow": dataflow_url,
335
+ "python_dependencies": [],
336
+ "configurations": [],
337
+ }
338
+
339
+ with open(os.path.join(local_file_path, "requirements.txt"), "r") as f:
340
+ file_contents = [line.strip() for line in f]
341
+ info["python_dependencies"] = file_contents
342
+
343
+ with open(os.path.join(local_file_path, "valid_configs.jsonl"), "r") as f:
344
+ file_contents = [line.strip() for line in f]
345
+ info["configurations"] = file_contents
346
+ return InspectResult(**info)
347
+
348
+
349
+ class InspectModuleResult(NamedTuple):
350
+ version: str # git commit sha/package version
351
+ user: str # github user URL
352
+ dataflow: str # dataflow URL
353
+ python_dependencies: list[str] # python dependencies
354
+ configurations: list[str] # configurations for the dataflow stored as a JSON string
355
+ possible_inputs: list[tuple[str, type]]
356
+ nodes: list[tuple[str, type]]
357
+ designated_outputs: list[tuple[str, type]]
358
+
359
+
360
+ @_track_function_call
361
+ def inspect_module(module: ModuleType) -> InspectModuleResult:
362
+ """Inspects the import module for information.
363
+
364
+ This does more than `inspect` because the module has been loaded and thus
365
+ we can put it into a Hamilton driver and ask questions of it.
366
+
367
+ .. code-block:: python
368
+
369
+ from hamilton.contrib.user.zilto import text_summarization
370
+ from hamilton import dataflows
371
+
372
+ info = dataflows.inspect_module(text_summarization)
373
+
374
+ :param module: the module with Hamilton code to deeply introspect.
375
+ :return: hamilton.dataflow.InspectModuleResult object.
376
+ """
377
+ if not module.__file__.startswith(DATAFLOW_FOLDER):
378
+ logger.info("not a downloaded dataflow module.")
379
+ return None
380
+ version = module.__version__
381
+ dataflow = module.__file__.split("/")[-2]
382
+ if "hamilton/contrib/dagworks" in module.__file__:
383
+ user = None
384
+ local_file_path = OFFICIAL_PATH.format(commit_ish=version, dataflow=dataflow)
385
+ user_url = None
386
+ dataflow_url = (
387
+ f"https://github.com/apache/hamilton/tree/{version}/contrib/contrib/dagworks/{dataflow}"
388
+ )
389
+ else:
390
+ user = module.__file__.split("/")[-3]
391
+ user_url = f"https://github.com/{user}"
392
+ local_file_path = USER_PATH.format(commit_ish=version, user=user, dataflow=dataflow)
393
+ dataflow_url = f"https://github.com/apache/hamilton/tree/{version}/contrib/contrib/user/{user}/{dataflow}"
394
+
395
+ if not os.path.exists(local_file_path):
396
+ raise ValueError(
397
+ f"Dataflow {user or 'dagworks'}/{dataflow} with version {version} does not exist locally. Not inspecting."
398
+ )
399
+ # return dictionary of python deps, inputs, nodes, designated outputs, commit hash
400
+ info: dict[
401
+ str, str | builtins.list[dict] | builtins.list[str] | builtins.list[tuple[str, type]]
402
+ ] = {
403
+ "version": version,
404
+ "user": user_url,
405
+ "dataflow": dataflow_url,
406
+ "python_dependencies": [],
407
+ "possible_inputs": [],
408
+ "nodes": [],
409
+ "designated_outputs": [],
410
+ "configurations": [],
411
+ }
412
+
413
+ with open(os.path.join(local_file_path, "requirements.txt"), "r") as f:
414
+ file_contents = f.readlines()
415
+ info["python_dependencies"] = file_contents
416
+
417
+ with open(os.path.join(local_file_path, "valid_configs.jsonl"), "r") as f:
418
+ file_contents = [json.loads(s) for s in f]
419
+ info["configurations"] = file_contents
420
+
421
+ dr = driver.Driver(info["configurations"][0], module)
422
+ vars = dr.list_available_variables()
423
+ info["possible_inputs"] = [(var.name, var.type) for var in vars if var.is_external_input]
424
+ info["nodes"] = [(var.name, var.type) for var in vars if not var.is_external_input]
425
+ info["designated_outputs"] = [
426
+ (var.name, var.type) for var in vars if var.tags.get("designated_output", False) == "True"
427
+ ]
428
+ return InspectModuleResult(**info)
429
+
430
+
431
+ @_track_function_call
432
+ def install_dependencies_string(dataflow: str, user: str = None, version: str = "latest") -> str:
433
+ """Returns a string for the user to install dependencies.
434
+
435
+ :param dataflow: the name of the dataflow.
436
+ :param user: the github name of the user.
437
+ :param version: the version to inspect. "latest" will resolve to the most recent commit, else pass \
438
+ a commit SHA.
439
+ :return: pip install string to use.
440
+ """
441
+ if version == "latest":
442
+ version = latest_commit(dataflow, user)
443
+ if user:
444
+ local_file_path = USER_PATH.format(commit_ish=version, user=user, dataflow=dataflow)
445
+ else:
446
+ local_file_path = OFFICIAL_PATH.format(commit_ish=version, dataflow=dataflow)
447
+ if not os.path.exists(local_file_path):
448
+ logger.info(
449
+ "Dataflow does not exist locally. Can't provide details on how to install dependencies."
450
+ )
451
+ return ""
452
+ return f"pip install -r {local_file_path}/requirements.txt"
453
+
454
+
455
+ from importlib.metadata import PackageNotFoundError
456
+ from importlib.metadata import version as pkg_version
457
+
458
+
459
+ @_track_function_call
460
+ def are_py_dependencies_satisfied(dataflow, user=None, version="latest"):
461
+ """Given a commit_ish, user & dataflow, reads the requirements.txt and checks if \
462
+ those dependencies have been satisfied in the currently running python interpreter.
463
+
464
+ Note: this does not handle versions. Just whether the package is installed or not.
465
+
466
+ :param dataflow: the name of the dataflow.
467
+ :param user: the github name of the user. None if DAGWorks official.
468
+ :param version: the version to inspect. "latest" will resolve to the most recent commit, else pass \
469
+ a commit SHA.
470
+ :return: boolean whether the dependencies are satisfied.
471
+ """
472
+ if version == "latest":
473
+ version = latest_commit(dataflow, user)
474
+ if user:
475
+ requirements_path = (
476
+ USER_PATH.format(commit_ish=version, user=user, dataflow=dataflow) + "/requirements.txt"
477
+ )
478
+ else:
479
+ requirements_path = (
480
+ OFFICIAL_PATH.format(commit_ish=version, dataflow=dataflow) + "/requirements.txt"
481
+ )
482
+
483
+ if not os.path.exists(requirements_path):
484
+ logger.info(f"requirements.txt not found at {requirements_path}")
485
+ return True
486
+
487
+ # Get list of currently installed packages
488
+ with open(requirements_path, "r") as req_file:
489
+ lines = req_file.readlines()
490
+
491
+ for line in lines:
492
+ line = line.strip()
493
+ equals = line.find("=")
494
+ less_than = line.find("<")
495
+ greater_than = line.find(">")
496
+ version_marker = min(equals, less_than, greater_than)
497
+ if version_marker > 0:
498
+ package_name, required_version = (
499
+ line[:version_marker],
500
+ line[version_marker + 1 :],
501
+ )
502
+ else:
503
+ package_name = line
504
+ required_version = None
505
+ required_version # noqa here for now...
506
+ try:
507
+ installed_version = pkg_version(package_name)
508
+ installed_version # noqa here for now..
509
+ except PackageNotFoundError:
510
+ logger.info(f"Package '{package_name}' is not installed.")
511
+ return False
512
+
513
+ # TODO: Check version if specified
514
+ # if required_version and installed_version != required_version:
515
+ # logger.info(
516
+ # f"Package '{package_name}' version mismatch. Required: {required_version}, Installed: {installed_version}")
517
+ # return False
518
+
519
+ logger.info("All requirements are satisfied.")
520
+ return True
521
+
522
+
523
+ @_track_function_call
524
+ def list(version: str = "latest", user: str = None) -> list:
525
+ """Lists dataflows locally downloaded based on commit_ish and user.
526
+
527
+ :param version: the version to inspect. "latest" will resolve to the most recent commit, else pass \
528
+ a commit SHA.
529
+ :param user: the github name of the user.
530
+ :return: list of tuples of (version, user, dataflow)
531
+ """
532
+ if version == "latest":
533
+ version = "main"
534
+ if not os.path.exists(DATAFLOW_FOLDER):
535
+ # TODO better error message here.
536
+ logger.info(f"Folder {DATAFLOW_FOLDER} does not exist.")
537
+ return []
538
+
539
+ dataflows = []
540
+
541
+ for ci in os.listdir(DATAFLOW_FOLDER):
542
+ if (
543
+ (version and ci != version)
544
+ or os.path.isfile(os.path.join(DATAFLOW_FOLDER, ci))
545
+ or ci.startswith(".")
546
+ ):
547
+ continue
548
+ commit_path = os.path.join(DATAFLOW_FOLDER, ci, "contrib", "hamilton", "contrib", "user")
549
+ for usr in os.listdir(commit_path):
550
+ if (
551
+ (user and usr != user)
552
+ or os.path.isfile(os.path.join(commit_path, usr))
553
+ or usr.startswith(".")
554
+ ):
555
+ continue
556
+ user_path = os.path.join(commit_path, usr)
557
+ for df in os.listdir(user_path):
558
+ if os.path.isfile(os.path.join(user_path, df)) or df.startswith("."):
559
+ continue
560
+ dataflows.append((ci, usr, df))
561
+
562
+ # for ci, usr, df in dataflows:
563
+ # logger.info(f"version: {ci}, user: {usr}, dataflow: {df}")
564
+
565
+ return dataflows
566
+
567
+
568
+ @_track_function_call
569
+ def find(query: str, version: str = None, user: str = None):
570
+ """Searches for locally downloaded dataflows based on a query string.
571
+
572
+ :param query: key words to search for.
573
+ :param version: the version to inspect. "latest" will resolve to the most recent commit, else pass \
574
+ a commit SHA.
575
+ :param user: the github name of the user.
576
+ :return: list of tuples of (version, user, dataflow)
577
+ """
578
+ if not os.path.exists(DATAFLOW_FOLDER):
579
+ logger.info(f"Folder {DATAFLOW_FOLDER} does not exist.")
580
+ return []
581
+
582
+ matches = set()
583
+
584
+ for ci in os.listdir(DATAFLOW_FOLDER):
585
+ if (
586
+ (version and ci != version)
587
+ or os.path.isfile(os.path.join(DATAFLOW_FOLDER, ci))
588
+ or ci.startswith(".")
589
+ ):
590
+ continue
591
+ commit_path = os.path.join(DATAFLOW_FOLDER, ci, "contrib", "hamilton", "contrib", "user")
592
+ for usr in os.listdir(commit_path):
593
+ if (
594
+ (user and usr != user)
595
+ or os.path.isfile(os.path.join(commit_path, usr))
596
+ or usr.startswith(".")
597
+ ):
598
+ continue
599
+ user_path = os.path.join(commit_path, usr)
600
+ for df in os.listdir(user_path):
601
+ for root, _, files in os.walk(user_path + "/" + df):
602
+ for file in files:
603
+ if not (
604
+ file.startswith(".") or file.endswith(".py") or file.endswith(".md")
605
+ ):
606
+ continue
607
+ file_path = os.path.join(root, file)
608
+ with open(file_path, "r") as f:
609
+ # dumb search -- just see if the string is in the file verbatim.
610
+ content = f.read()
611
+ if query in content:
612
+ matches.add((ci, usr, df))
613
+ # TODO: incorporate tags
614
+ return matches
615
+
616
+
617
+ @_track_function_call
618
+ def copy(
619
+ dataflow: ModuleType,
620
+ destination_path: str,
621
+ overwrite: bool = False,
622
+ renamed_module: str = None,
623
+ ):
624
+ """Copies a dataflow module to the passed in path.
625
+
626
+ .. code-block:: python
627
+
628
+ from hamilton import dataflows
629
+
630
+ # dynamically pull and then copy
631
+ NAME_OF_DATAFLOW = dataflow.import_module("NAME_OF_DATAFLOW", "NAME_OF_USER")
632
+ dataflow.copy(NAME_OF_DATAFLOW, destination_path="PATH_TO_DIRECTORY")
633
+ # copy from the installed library
634
+ from hamilton.contrib.user.NAME_OF_USER import NAME_OF_DATAFLOW
635
+
636
+ dataflow.copy(NAME_OF_DATAFLOW, destination_path="PATH_TO_DIRECTORY")
637
+
638
+ :param dataflow: the module to copy.
639
+ :param destination_path: the path to a directory to place the module in.
640
+ :param overwrite: whether to overwrite the destination. Default is False and raise an error.
641
+ :param renamed_module: whether to rename the copied module. Default is None and will use the original name.
642
+ """
643
+ # make sure the module exists and has a file to copy
644
+ if not os.path.exists(dataflow.__file__):
645
+ raise ValueError(f"Dataflow {dataflow.__name__} does not exist locally. Not copying.")
646
+
647
+ if renamed_module: # rename the module
648
+ module_name = renamed_module
649
+ else:
650
+ # get the module name
651
+ module_name = dataflow.__name__.split(".")[-1]
652
+
653
+ # append the module name to the destination path
654
+ destination_path = os.path.join(destination_path, module_name)
655
+ # make the directories if they don't exist
656
+ if not os.path.exists(destination_path):
657
+ os.makedirs(destination_path, exist_ok=True)
658
+
659
+ # Note: we'll need to change this if we change how module code is structured.
660
+ file_path = os.path.join(destination_path, "__init__.py")
661
+ # if the file_path exists and overwrite is False, raise an error
662
+ if os.path.exists(file_path) and not overwrite:
663
+ raise ValueError(
664
+ f"Destination {file_path} already exists. Not copying. Use overwrite=True to overwrite."
665
+ )
666
+ # save the module code to the destination path
667
+ shutil.copy(dataflow.__file__, file_path)
668
+ logger.info(f"Successfully copied {dataflow.__name__} to {file_path}.")
669
+
670
+
671
+ @_track_function_call
672
+ def init():
673
+ """Creates a template for someone to add a new flow for
674
+
675
+ Don't want to bite off having to setup a fork,etc. This will
676
+ just create a new directory with template files.
677
+ """
678
+ # TODO:
679
+ pass
680
+
681
+
682
+ if __name__ == "__main__":
683
+ logging.basicConfig(level=logging.INFO, stream=sys.stdout)
684
+ _user = "zilto"
685
+ _version = "latest" # or a git commit hash
686
+ _dataflow = "text_summarization"
687
+ _module = import_module(_dataflow, _user, _version, overwrite=True)
688
+
689
+ dr = driver.Driver({}, _module)
690
+ # logger.info(dr.list_available_variables())
691
+ dr.display_all_functions("./dag", {"format": "png", "view": False})
692
+
693
+ logger.info("Listing dataflows ---")
694
+ dfs = list(version=None, user="zilto")
695
+ logger.info(dfs)
696
+ logger.info("Listing chunks ---")
697
+ chunks = find("chunk")
698
+ logger.info(chunks)
699
+ logger.info("Determining python dependencies ---")
700
+ are_py_dependencies_satisfied(_user, _dataflow, _version)
701
+ import pprint
702
+
703
+ logger.info("Inspect module output ---")
704
+ logger.info(pprint.pformat(inspect_module(_module)))
705
+ logger.info("Resolve dataflow commit output ---")
706
+ _version = latest_commit(_dataflow, _user)
707
+ logger.info(_version)
708
+ logger.info("Install dependencies output ---")
709
+ logger.info(install_dependencies_string(_dataflow, _user, _version))
710
+ logger.info("Inspect output ---")
711
+ logger.info(inspect(_dataflow, _user, _version))
712
+
713
+ from hamilton.contrib.user.zilto import text_summarization # noqa:F401
714
+
715
+ copy(
716
+ text_summarization,
717
+ destination_path="~/temp/",
718
+ overwrite=True,
719
+ renamed_module="text_summarization_copy",
720
+ )
721
+ copy(
722
+ _module,
723
+ destination_path="~/temp/",
724
+ overwrite=True,
725
+ renamed_module="text_summarization_copy2",
726
+ )