kedro-sumz-utils 0.0.1__tar.gz

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.
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.1
2
+ Name: kedro-sumz-utils
3
+ Version: 0.0.1
4
+ Summary: Utility functions and kedro datasets for Data projects.
5
+ Author-email: Juan Barreto <juan.barreto@sumz.co>
6
+ Project-URL: Homepage, https://github.com/pypa/sampleproject
7
+ Project-URL: Bug Tracker, https://github.com/pypa/sampleproject/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: kedro[spark]<0.19.0,>=0.18.0
13
+ Requires-Dist: kedro-mlflow==0.11.10
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "kedro-sumz-utils"
7
+ version = "0.0.1"
8
+ authors = [
9
+ { name="Juan Barreto", email="juan.barreto@sumz.co" },
10
+ ]
11
+ description = "Utility functions and kedro datasets for Data projects."
12
+ readme = "README.md"
13
+ requires-python = ">=3.9"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+ dependencies = [
19
+ "kedro[spark]>=0.18.0,<0.19.0",
20
+ "kedro-mlflow==0.11.10"
21
+ ]
22
+ [project.urls]
23
+ "Homepage" = "https://github.com/pypa/sampleproject"
24
+ "Bug Tracker" = "https://github.com/pypa/sampleproject/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,81 @@
1
+ """Custom Kedro-Virgo context class."""
2
+ import logging
3
+ from pathlib import Path
4
+ from typing import Any, Dict, Union
5
+
6
+ from kedro.config import ConfigLoader
7
+ from kedro.framework.context import KedroContext
8
+ from pluggy import PluginManager
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class KedroSparkContext(KedroContext):
14
+ """Custom kedro context which initializes the Spark Session."""
15
+
16
+ def __init__(
17
+ self,
18
+ package_name: str,
19
+ project_path: Union[str, Path],
20
+ config_loader: ConfigLoader,
21
+ hook_manager: PluginManager,
22
+ env: str = None,
23
+ extra_params: Dict[str, Any] = None,
24
+ ):
25
+ logger.info("Creating Kedro Context")
26
+ super().__init__(
27
+ package_name, project_path, config_loader, hook_manager, env, extra_params
28
+ )
29
+ self._spark_session = None
30
+ self._package_name = (
31
+ package_name if package_name else Path(__file__).parent.name
32
+ )
33
+
34
+ if not self._init_databricks():
35
+ self._init_spark()
36
+
37
+ def _init_spark(self):
38
+ from pyspark import SparkConf
39
+ from pyspark.sql import SparkSession
40
+
41
+ if SparkSession.getActiveSession() is None:
42
+ logger.info("Initializing Spark session")
43
+ parameters = self._config_loader["spark"]
44
+ logger.info(parameters)
45
+ spark_conf = SparkConf().setAll(list(parameters.items()))
46
+
47
+ spark_session_conf = (
48
+ SparkSession.builder.appName(self._package_name)
49
+ .master("local[*, 4]")
50
+ .config(conf=spark_conf)
51
+ )
52
+
53
+ self._spark_session = spark_session_conf.getOrCreate()
54
+ self._spark_session.sparkContext.setLogLevel("WARN")
55
+ logger.info("Spark Web URL: %s", self._spark_session.sparkContext.uiWebUrl)
56
+ else:
57
+ self._spark_session = SparkSession.getActiveSession()
58
+
59
+ def _init_databricks(self) -> bool:
60
+ try:
61
+ from databricks.connect import DatabricksSession
62
+ from pyspark.sql import SparkSession
63
+
64
+ if SparkSession.getActiveSession() is None:
65
+ logger.info("Initializing Databricks session")
66
+ self._spark_session = DatabricksSession.builder.profile(
67
+ "connect"
68
+ ).getOrCreate()
69
+ else:
70
+ self._spark_session = SparkSession.getActiveSession()
71
+ return True
72
+ except ImportError:
73
+ logger.info(
74
+ "Databricks Connect is not installed. Initializing Spark session instead."
75
+ )
76
+ return False
77
+
78
+ @property
79
+ def spark_session(self):
80
+ """Spark session property."""
81
+ return self._spark_session
@@ -0,0 +1,12 @@
1
+ """Dummy dataset for testing purposes."""
2
+ from typing import Any
3
+
4
+ from kedro.io import MemoryDataSet
5
+
6
+
7
+ class DummyDataset(MemoryDataSet):
8
+ """Dummy dataset for testing purposes."""
9
+
10
+ # pylint: disable=useless-super-delegation,too-few-public-methods
11
+ def __init__(self, data: Any = True, copy_mode: str = None):
12
+ super().__init__(data, copy_mode)
@@ -0,0 +1,155 @@
1
+ """``AbstractDataSet`` implementation to access DeltaTables using
2
+ ``delta-spark``
3
+ """
4
+ from pathlib import PurePosixPath
5
+ from typing import NoReturn
6
+
7
+ from delta.tables import DeltaTable
8
+
9
+ # pylint: disable=unused-import
10
+ from kedro.extras.datasets.spark import SparkHiveDataSet # noqa: F401
11
+ from kedro.extras.datasets.spark.spark_dataset import (
12
+ _split_filepath,
13
+ _strip_dbfs_prefix,
14
+ )
15
+ from kedro.io.core import AbstractDataSet, DataSetError
16
+ from pyspark.sql import SparkSession
17
+ from pyspark.sql.utils import AnalysisException
18
+
19
+ from kedro_sumz.utils import get_kedro_context
20
+
21
+
22
+ class DeltaTableDataSet(AbstractDataSet[None, DeltaTable]):
23
+ """``DeltaTableDataSet`` loads data into DeltaTable objects.
24
+
25
+ Example usage for the
26
+ `YAML API <https://kedro.readthedocs.io/en/stable/data/\
27
+ data_catalog.html#use-the-data-catalog-with-the-yaml-api>`_:
28
+
29
+ .. code-block:: yaml
30
+
31
+ weather@spark:
32
+ type: spark.SparkDataSet
33
+ filepath: data/02_intermediate/data.parquet
34
+ file_format: "delta"
35
+
36
+ weather@delta:
37
+ type: virgo.extras.datasets.DeltaTableDataSet
38
+ directory: data/02_intermediate
39
+ table: "data.parquet"
40
+
41
+ weather_table@delta:
42
+ type: virgo.extras.datasets.delta.DeltaTableDataSet
43
+ directory: intermediate
44
+ table: data
45
+
46
+ Example usage for the
47
+ `Python API <https://kedro.readthedocs.io/en/stable/data/\
48
+ data_catalog.html#use-the-data-catalog-with-the-code-api>`_:
49
+ ::
50
+
51
+ >>> from pyspark.sql import SparkSession
52
+ >>> from pyspark.sql.types import (StructField, StringType,
53
+ >>> IntegerType, StructType)
54
+ >>>
55
+ >>> from kedro.extras.datasets.spark import DeltaTableDataSet, SparkDataSet
56
+ >>>
57
+ >>> schema = StructType([StructField("name", StringType(), True),
58
+ >>> StructField("age", IntegerType(), True)])
59
+ >>>
60
+ >>> data = [('Alex', 31), ('Bob', 12), ('Clarke', 65), ('Dave', 29)]
61
+ >>>
62
+ >>> spark_df = SparkSession.builder.getOrCreate().createDataFrame(data, schema)
63
+ >>>
64
+ >>> data_set = SparkHiveDataSet(database="test_database", table="test_table",
65
+ >>> write_mode="overwrite")
66
+ >>> data_set.save(spark_df)
67
+ >>> delta_dataset = DeltaTableDataSet(directory="test_database", table="test_table")
68
+ >>> delta_table = delta_dataset.load()
69
+ >>>
70
+ >>> delta_table.update()
71
+ """
72
+
73
+ # this dataset cannot be used with ``ParallelRunner``,
74
+ # therefore it has the attribute ``_SINGLE_PROCESS = True``
75
+ # for parallelism within a Spark pipeline please consider
76
+ # using ``ThreadRunner`` instead
77
+ _SINGLE_PROCESS = True
78
+
79
+ def __init__(
80
+ self,
81
+ directory: str,
82
+ table: str,
83
+ dataset_type: str = None,
84
+ ) -> None:
85
+ """Creates a new instance of ``DeltaTableDataSet``.
86
+
87
+ Args:
88
+ directory: Base path to a folder or a database.
89
+ table: Name of the table or file.
90
+ dataset_type: Type of the dataset. Can be either "table" or "file".
91
+ """
92
+
93
+ if not dataset_type and ("/" in directory or "/" in table):
94
+ self._type = "file"
95
+ elif not dataset_type and "." in directory:
96
+ self._type = "table"
97
+ else:
98
+ self._type = dataset_type
99
+ if not self._type:
100
+ raise DataSetError(
101
+ "Could not determine dataset type. Please specify it explicitly."
102
+ )
103
+ if self._type == "table":
104
+ path = f"{directory}.{table}"
105
+ self._database = ".".join(path.split(".")[:-1])
106
+ self._table = table
107
+ self._full_table_address = f"{self._database}.{self._table}"
108
+ else:
109
+ path = f"{directory}/{table}"
110
+ fs_prefix, filepath = _split_filepath(path)
111
+ self._fs_prefix = fs_prefix
112
+ self._filepath = PurePosixPath(filepath)
113
+ self._full_table_address = (
114
+ f"delta.`{self._fs_prefix + str(self._filepath)}`"
115
+ )
116
+ self._type = "file"
117
+
118
+ @staticmethod
119
+ def _get_spark() -> SparkSession:
120
+ """Get the active Spark session."""
121
+ if hasattr(get_kedro_context(), "spark_session"):
122
+ return get_kedro_context().spark_session
123
+ else:
124
+ SparkSession.getActiveSession()
125
+
126
+ def _load(self) -> DeltaTable:
127
+ return DeltaTable.forName(self._get_spark(), self._full_table_address)
128
+
129
+ def _save(self, data: None) -> NoReturn:
130
+ raise DataSetError(f"{self.__class__.__name__} is a read only dataset type")
131
+
132
+ def _exists(self) -> bool:
133
+ # noqa # pylint:disable=protected-access
134
+ if self._type == "table":
135
+ return (
136
+ self._get_spark()
137
+ ._jsparkSession.catalog()
138
+ .tableExists(self._database, self._table)
139
+ )
140
+
141
+ load_path = _strip_dbfs_prefix(self._fs_prefix + str(self._filepath))
142
+
143
+ try:
144
+ self._get_spark().read.load(path=load_path, format="delta")
145
+ except AnalysisException as exception:
146
+ if "is not a Delta table" in exception.desc:
147
+ return False
148
+ raise
149
+
150
+ return True
151
+
152
+ def _describe(self):
153
+ return {
154
+ "full_table_address": self._full_table_address,
155
+ }
@@ -0,0 +1,277 @@
1
+ from functools import partial
2
+ from typing import Any, Dict, Optional
3
+ from copy import deepcopy
4
+ import mlflow
5
+ from kedro.io import AbstractDataSet, DataSetError
6
+ from mlflow.tracking import MlflowClient
7
+ from kedro_mlflow.io.models.mlflow_model_logger_dataset import (
8
+ MlflowModelLoggerDataSet
9
+ )
10
+ from kedro_sumz.utils.mlflow import ModelWithConfig
11
+
12
+
13
+ class MlflowModelWithConfigDataSet(MlflowModelLoggerDataSet):
14
+ """
15
+ Wrapper for saving, logging and loading for all MLFlow model flavor
16
+ including its configuration
17
+ """
18
+ def __init__(
19
+ self,
20
+ flavor: str,
21
+ run_id: Optional[str] = None,
22
+ artifact_path: Optional[str] = "model",
23
+ pyfunc_workflow: Optional[str] = None,
24
+ load_args: Optional[Dict[str, Any]] = None,
25
+ save_args: Optional[Dict[str, Any]] = None
26
+ ) -> None:
27
+ """Initializes MlflowModelWithConfigDataSet
28
+
29
+ Args:
30
+ flavor (str): Flavor of the model
31
+ run_id (Optional[str], optional): Mlflow run id. Defaults to None.
32
+ artifact_path (Optional[str], optional): Path of the model artifact. Defaults to "model".
33
+ pyfunc_workflow (Optional[str], optional): Name of the pyfunc model. Defaults to None.
34
+ load_args (Optional[Dict[str, Any]], optional): Load args used in the
35
+ mlflow load_model function. Defaults to None.
36
+ save_args (Optional[Dict[str, Any]], optional): Save args used in the
37
+ mlflow log_model function. Defaults to None.
38
+ """
39
+ super().__init__(flavor, run_id, artifact_path, pyfunc_workflow, load_args, save_args)
40
+
41
+ def _save_model_in_run(self, model: ModelWithConfig):
42
+ """
43
+ Saves a model in a run as an artifact
44
+
45
+ Args:
46
+ model (ModelWithConfig): Object which stores the model and its configuration
47
+
48
+ Raises:
49
+ DataSetError: When the object passed is not an instance of the ModelWithConfig class
50
+ """
51
+ if not isinstance(model, ModelWithConfig):
52
+ raise DataSetError(
53
+ "The model to be logged must be an instance of the ModelWithConfig class"
54
+ )
55
+
56
+ if not self._logging_activated:
57
+ return
58
+
59
+ save_args = self._prepare_save_args(
60
+ model_with_config=model
61
+ )
62
+
63
+ if self._flavor == "mlflow.pyfunc":
64
+ # PyFunc models utilise either `python_model` or `loader_module`
65
+ # workflow. We we assign the passed `model` object to one of those keys
66
+ # depending on the chosen `pyfunc_workflow`.
67
+ save_args[self._pyfunc_workflow] = model.model
68
+ self._mlflow_model_module.log_model(
69
+ **save_args
70
+ )
71
+ else:
72
+ # Otherwise we save using the common workflow
73
+ self._mlflow_model_module.log_model(
74
+ model.model,
75
+ **save_args
76
+ )
77
+
78
+ def _prepare_save_args(
79
+ self,
80
+ model_with_config: ModelWithConfig
81
+ ) -> Dict[str, Any]:
82
+ save_args = deepcopy(self._save_args)
83
+
84
+ save_args["signature"] = self._save_arg_value(
85
+ catalog_save_arg=self._save_args.get("signature"),
86
+ model_with_config_arg=model_with_config.signature
87
+ )
88
+
89
+ save_args["conda_env"] = self._save_arg_value(
90
+ catalog_save_arg=self._save_args.get("conda_env"),
91
+ model_with_config_arg=model_with_config.conda_env
92
+ )
93
+
94
+ save_args["pip_requirements"] = self._save_arg_value(
95
+ catalog_save_arg=self._save_args.get("pip_requirements"),
96
+ model_with_config_arg=model_with_config.pip_requirements
97
+ )
98
+
99
+ save_args["extra_pip_requirements"] = self._save_arg_value(
100
+ catalog_save_arg=self._save_args.get("extra_pip_requirements"),
101
+ model_with_config_arg=model_with_config.extra_pip_requirements
102
+ )
103
+
104
+ save_args["artifact_path"] = self._artifact_path
105
+
106
+ return save_args
107
+
108
+ def _save_arg_value(
109
+ self,
110
+ catalog_save_arg: Any,
111
+ model_with_config_arg: Any
112
+ )->Any:
113
+ if catalog_save_arg is None and model_with_config_arg is None:
114
+ return None
115
+ elif catalog_save_arg is None:
116
+ return model_with_config_arg
117
+ else:
118
+ return catalog_save_arg
119
+
120
+
121
+ class MlflowMetricsDictDataSet(AbstractDataSet):
122
+ """This class represent MLflow metrics dictionary dataset."""
123
+
124
+ def __init__(
125
+ self,
126
+ prefix: str = None,
127
+ run_id: str = None,
128
+ ):
129
+ """Initialise MlflowMetricsDictDataSet.
130
+
131
+ Args:
132
+ prefix (str, optional): Prefix for metrics logged in MLflow. Defaults to None
133
+ run_id (str, optional): ID of MLflow run. Defaults to None
134
+ """
135
+ self._prefix = prefix
136
+ self.run_id = run_id
137
+ self._logging_activated = True # by default, logging is activated!
138
+
139
+ @property
140
+ def run_id(self):
141
+ """Get run id.
142
+
143
+ If active run is not found, tries to find last experiment.
144
+
145
+ Raise `DataSetError` exception if run id can't be found.
146
+
147
+ Returns:
148
+ str: String contains run_id.
149
+ """
150
+ if self._run_id is not None:
151
+ return self._run_id
152
+ run = mlflow.active_run()
153
+ if run:
154
+ return run.info.run_id
155
+ raise DataSetError("Cannot find run id.")
156
+
157
+ @run_id.setter
158
+ def run_id(self, run_id):
159
+ self._run_id = run_id
160
+
161
+ # we want to be able to turn logging off for an entire pipeline run
162
+ # To avoid that a single call to a dataset in the catalog creates a new run automatically
163
+ # we want to be able to turn everything off
164
+ @property
165
+ def _logging_activated(self):
166
+ return self.__logging_activated
167
+
168
+ @_logging_activated.setter
169
+ def _logging_activated(self, flag):
170
+ if not isinstance(flag, bool):
171
+ raise ValueError(f"_logging_activated must be a boolean, got {type(flag)}")
172
+ self.__logging_activated = flag
173
+
174
+ def _load(self) -> Dict[str, float]:
175
+ """Load MlflowMetricsDictDataSet.
176
+
177
+ Returns:
178
+ Dict[str, float]: Dictionary with MLflow metrics.
179
+ """
180
+ all_metrics = self._get_all_metrics()
181
+
182
+ dataset_metrics = self._filter_dataset_metrics(all_metrics)
183
+
184
+ if not dataset_metrics:
185
+ raise DataSetError(
186
+ "Tried to load an empty metrics dictionary"
187
+ f"be sure that metrics with the prefix '{self._prefix}' exists"
188
+ "in the current run"
189
+ )
190
+
191
+ return dataset_metrics
192
+
193
+ def _save(self, data: Dict[str, float]) -> None:
194
+ """
195
+ Save given MLflow metrics dictionary dataset
196
+ and log it in MLflow as metrics.
197
+
198
+ Args:
199
+ data (Dict[str, float]): Metrics dictionary.
200
+ """
201
+ if not data:
202
+ raise DataSetError(
203
+ "Metrics dictionary should have at least one metric"
204
+ )
205
+
206
+ client = MlflowClient()
207
+ try:
208
+ run_id = self.run_id
209
+ except DataSetError:
210
+ # If run_id can't be found log_metric would create new run.
211
+ run_id = None
212
+
213
+ log_metric = (
214
+ partial(client.log_metric, run_id)
215
+ if run_id is not None
216
+ else mlflow.log_metric
217
+ )
218
+
219
+ if not all([isinstance(value, float) for value in data.values()]):
220
+ raise DataSetError(f"All metric values should be of type `float`")
221
+
222
+ metrics = {f"{self._prefix}.{key}": value for key, value in data.items()}
223
+
224
+ if self._logging_activated:
225
+ for k, v in metrics.items():
226
+ log_metric(k, v)
227
+
228
+ def _exists(self) -> bool:
229
+ """Check if MLflow metrics dataset exists.
230
+
231
+ Returns:
232
+ bool: Is MLflow metrics dataset exists?
233
+ """
234
+ all_metrics = self._get_all_metrics()
235
+ dataset_metrics = self._filter_dataset_metrics(all_metrics)
236
+ return len(dataset_metrics.keys())>0
237
+
238
+ def _describe(self) -> Dict[str, Any]:
239
+ """Describe MLflow metrics dataset.
240
+
241
+ Returns:
242
+ Dict[str, Any]: Dictionary with MLflow metrics
243
+ dictionary dataset description.
244
+ """
245
+ return {
246
+ "run_id": self._run_id,
247
+ "prefix": self._prefix,
248
+ }
249
+
250
+ def _filter_dataset_metrics(self, metrics: Dict[str, float]) -> Dict[str, float]:
251
+ """
252
+ Filter a metric dictionary by keeping only those that
253
+ belongs to the dataset
254
+
255
+ Args:
256
+ metric (Dict[str, float]): Metrics dictionary.
257
+ Returns:
258
+ Dict[str, float]: Dictionary with the metrics that belongs
259
+ to the dataset
260
+ """
261
+ dataset_metrics = {
262
+ k: v for k, v in metrics.items()
263
+ if k.startswith(self._prefix)
264
+ }
265
+
266
+ return dataset_metrics
267
+
268
+ def _get_all_metrics(self)->Dict[str, float]:
269
+ """Retrieve all metrics logged in the current run
270
+
271
+ Returns:
272
+ Dict[str, float]: Dictionary with all the metrics logged
273
+ in the current run
274
+ """
275
+ client = MlflowClient()
276
+ run = client.get_run(self.run_id)
277
+ return run.data.metrics