wringlet 0.0.3.dev6__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.
wringlet/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ from .data_provenance import (
2
+ add_provenance_column,
3
+ data_provenance_enabled,
4
+ data_provenance_session_builder,
5
+ provenance_column_name,
6
+ remove_provenance_column,
7
+ )
8
+
9
+ __all__ = [
10
+ "provenance_column_name",
11
+ "add_provenance_column",
12
+ "remove_provenance_column",
13
+ "data_provenance_enabled",
14
+ "data_provenance_session_builder",
15
+ ]
@@ -0,0 +1,156 @@
1
+ import os
2
+ import typing as T
3
+ from contextlib import contextmanager
4
+
5
+ from pyspark.sql import DataFrame, SparkSession
6
+
7
+ from wringlet.py4j_utils import _get_provenance_jvm_function
8
+
9
+ DataFrameOrView = str | DataFrame
10
+
11
+
12
+ def provenance_column_name(spark: SparkSession) -> str:
13
+ """
14
+ Returns the name of the provenance column from the Spark configuration.
15
+ """
16
+ return _get_provenance_jvm_function("provenanceColumnName", spark)(spark._jsparkSession)
17
+
18
+
19
+ @T.overload
20
+ def add_provenance_column(df_or_view: str, spark: SparkSession) -> str: ...
21
+
22
+
23
+ @T.overload
24
+ def add_provenance_column(df_or_view: DataFrame, spark: SparkSession) -> DataFrame: ...
25
+
26
+
27
+ def add_provenance_column(df_or_view: DataFrameOrView, spark: SparkSession) -> DataFrameOrView:
28
+ """
29
+ Adds a provenance column to the given DataFrame.
30
+ """
31
+ jfunction = _get_provenance_jvm_function("addProvenance", spark)
32
+ match df_or_view:
33
+ case str():
34
+ jfunction(spark._jsparkSession, df_or_view)
35
+ return df_or_view
36
+ case DataFrame():
37
+ return DataFrame(jfunction(df_or_view._jdf), spark)
38
+ case _:
39
+ raise TypeError("df_or_view must be a str or a DataFrame")
40
+
41
+
42
+ @T.overload
43
+ def remove_provenance_column(df_or_view: str, spark: SparkSession) -> str: ...
44
+
45
+
46
+ @T.overload
47
+ def remove_provenance_column(df_or_view: DataFrame, spark: SparkSession) -> DataFrame: ...
48
+
49
+
50
+ def remove_provenance_column(df_or_view: DataFrameOrView, spark: SparkSession) -> DataFrameOrView:
51
+ """
52
+ Removes the provenance column from the given DataFrame.
53
+ """
54
+ jfunction = _get_provenance_jvm_function("removeProvenance", spark)
55
+ match df_or_view:
56
+ case str():
57
+ jfunction(spark._jsparkSession, df_or_view)
58
+ return df_or_view
59
+ case DataFrame():
60
+ return DataFrame(jfunction(df_or_view._jdf), spark)
61
+ case _:
62
+ raise TypeError("df_or_view must be a str or a DataFrame")
63
+
64
+
65
+ @contextmanager
66
+ def data_provenance_enabled(
67
+ spark: SparkSession, *args: DataFrameOrView
68
+ ) -> T.Generator[T.Tuple[DataFrameOrView, ...] | DataFrameOrView | None, None, None]:
69
+ """
70
+ Context manager to enable data provenance for the duration of a block of code.
71
+ When DataFrames or view names are provided, it adds a provenance column to them
72
+ and removes it after the block is executed.
73
+ """
74
+ # Remember the previous state in case these are nested
75
+ is_data_provenance_enabled = str(spark.conf.get("spark.provenance.enabled", "false"))
76
+ try:
77
+ # Turn data provenance on for this block
78
+ spark.conf.set("spark.provenance.enabled", "true")
79
+ dataframe_or_views_with_provenance = tuple(add_provenance_column(df, spark) for df in args)
80
+ if not args:
81
+ yield None
82
+ else:
83
+ yield dataframe_or_views_with_provenance if len(args) > 1 else dataframe_or_views_with_provenance[0]
84
+ finally:
85
+ # Revert to whatever it was before
86
+ spark.conf.set("spark.provenance.enabled", is_data_provenance_enabled)
87
+ # Remove the provenance column from the DataFrames/views
88
+ for df in args:
89
+ remove_provenance_column(df, spark)
90
+
91
+
92
+ def data_provenance_session_builder(
93
+ provenance_builder: str = "display",
94
+ ) -> SparkSession.Builder:
95
+ """
96
+ Helper function to automatically find the bundled JAR
97
+ and initialize a SparkSession Builder with the plugin enabled.
98
+
99
+ Args:
100
+ provenance_builder: Builder strategy used by the Scala extension.
101
+ Supported values are: display, boolean, semi-why, full-why.
102
+ """
103
+ # 1. Find the path to the 'jars' folder dynamically
104
+ current_dir = os.path.dirname(os.path.abspath(__file__))
105
+ jar_path = os.path.join(current_dir, "jars", "dp-spark_2.13-0.0.1.jar")
106
+
107
+ # 2. Build and return the SparkSession
108
+ return (
109
+ SparkSession.builder.config("spark.jars", jar_path)
110
+ .config(
111
+ "spark.sql.extensions",
112
+ "org.dataprov.dp.wringlet.SparkProvenanceExtension",
113
+ )
114
+ .config("spark.provenance.builder", provenance_builder)
115
+ )
116
+
117
+
118
+ def get_minimal_sources(
119
+ df: DataFrame, source_dfs: T.Sequence[DataFrame], spark: SparkSession, provenance_col: str | None = None
120
+ ) -> T.List[DataFrame]:
121
+ """
122
+ Extracts the minimal rows from source DataFrames that contributed to the given final DataFrame.
123
+ """
124
+ if provenance_col is None:
125
+ provenance_col = provenance_column_name(spark)
126
+
127
+ gw = spark._sc._gateway
128
+ if gw is None:
129
+ raise RuntimeError("Spark context gateway is not initialized")
130
+
131
+ java_source_dfs = gw.jvm.java.util.ArrayList()
132
+ for source in source_dfs:
133
+ java_source_dfs.add(source._jdf)
134
+
135
+ try:
136
+ j_function = _get_provenance_jvm_function("getMinimalSources", spark)
137
+ java_result_list = j_function(df._jdf, java_source_dfs, provenance_col)
138
+ except Exception:
139
+ j_extractor = gw.jvm.org.dataprov.dp.wringlet.ProvenanceExtractor
140
+ java_result_list = j_extractor.getMinimalSources(df._jdf, java_source_dfs, provenance_col)
141
+
142
+ python_result_dfs = []
143
+ for i in range(java_result_list.size()):
144
+ python_result_dfs.append(DataFrame(java_result_list.get(i), spark))
145
+
146
+ return python_result_dfs
147
+
148
+
149
+ def _py_dataframe_extension_get_minimal_sources(
150
+ self: DataFrame, source_dfs: T.Sequence[DataFrame], provenance_col: str | None = None
151
+ ) -> T.List[DataFrame]:
152
+ """Helper attached to PySpark DataFrame class for fluent API usage."""
153
+ return get_minimal_sources(self, source_dfs, self.sparkSession, provenance_col)
154
+
155
+
156
+ setattr(DataFrame, "get_minimal_sources", _py_dataframe_extension_get_minimal_sources)
wringlet/jars/.gitkeep ADDED
File without changes
@@ -0,0 +1,78 @@
1
+ #
2
+ # File: https://github.com/data-prov/wringlet/blob/main/pyspark-wringlet/src/wringlet/jobs/getting_started_job.py
3
+ #
4
+ # See also https://github.com/data-prov/wringlet/blob/main/pyspark-wringlet/notebooks/demo.ipynb
5
+ #
6
+ """
7
+ Pyspark script to test the data provenance concept
8
+ """
9
+
10
+ import datetime
11
+
12
+ from pyspark.sql import SparkSession
13
+
14
+ import wringlet as dp
15
+
16
+ JOB_NAME = "getting_started_job"
17
+ today_date: str = datetime.date.today().strftime("%Y-%m-%d")
18
+ now_datetime: datetime.datetime = datetime.datetime.now(datetime.UTC)
19
+
20
+
21
+ def main():
22
+ """
23
+ The dp-spark JAR is expected to have been installed locally, for instance,
24
+ thanks to `sbt publishLocal publishM2` in the Scala-related folder
25
+ """
26
+ spark = (
27
+ SparkSession.builder.appName(JOB_NAME)
28
+ .config("spark.sql.extensions", "org.dataprov.dp.ProvenanceExtension")
29
+ .enableHiveSupport()
30
+ .getOrCreate()
31
+ )
32
+
33
+ # Showcase that the data_provenance_enabled() function works as expected
34
+ print(spark.conf.get("spark.provenance.enabled", "false"))
35
+ with dp.data_provenance_enabled(spark):
36
+ print(spark.conf.get("spark.provenance.enabled", "false"))
37
+ print(spark.conf.get("spark.provenance.enabled", "false"))
38
+
39
+ # Toy sample
40
+ df = spark.createDataFrame(
41
+ [
42
+ ("A", datetime.date(2026, 1, 15), 10.0, 90),
43
+ ("A", datetime.date(2026, 1, 16), 10.0, 120),
44
+ ("A", datetime.date(2026, 1, 17), 5.0, 300),
45
+ ("B", datetime.date(2026, 1, 15), 100.0, 20),
46
+ ("B", datetime.date(2026, 1, 16), 100.0, 30),
47
+ ("B", datetime.date(2026, 1, 17), 80.0, 60),
48
+ ],
49
+ ["product", "date", "price", "sales"],
50
+ )
51
+ df.printSchema()
52
+ df.show()
53
+
54
+ # Test with PySpark syntax
55
+
56
+ ## Without provenance
57
+ df2 = df.select("product")
58
+ print("Without provenance")
59
+ df2.show()
60
+
61
+ ## With provenance
62
+ df3 = None
63
+ with dp.data_provenance_enabled(spark):
64
+ df3 = df.select("product")
65
+ print("With provenance")
66
+ df3.show()
67
+
68
+ # Test with SQL syntax
69
+ result_df = None
70
+ df.createOrReplaceTempView("sales")
71
+ with dp.data_provenance_enabled(spark):
72
+ result_df = spark.sql("select * from sales")
73
+
74
+ result_df.show()
75
+
76
+
77
+ if __name__ == "__main__":
78
+ main()
wringlet/py4j_utils.py ADDED
@@ -0,0 +1,21 @@
1
+ import typing as T
2
+
3
+ from py4j.java_gateway import JVMView
4
+ from pyspark.sql import SparkSession
5
+
6
+
7
+ def _get_jvm_from_spark(spark: SparkSession) -> JVMView:
8
+ """
9
+ Helper function to get the JVM view from a SparkSession.
10
+ """
11
+ assert spark._jvm is not None
12
+ return spark._jvm
13
+
14
+
15
+ def _get_provenance_jvm_function(name: str, spark: SparkSession) -> T.Callable:
16
+ """
17
+ Retrieves JVM function identified by name from
18
+ Java gateway associated with Spark session.
19
+ """
20
+ jvm = _get_jvm_from_spark(spark)
21
+ return getattr(getattr(jvm, "org.dataprov.dp.wringlet.ProvenanceApi"), name)
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.3
2
+ Name: wringlet
3
+ Version: 0.0.3.dev6
4
+ Summary: PySpark helper utilities for Spark data provenance
5
+ Author: data-prov
6
+ Requires-Python: >=3.12
7
+ Project-URL: repository, https://github.com/data-prov/wringlet
8
+ Description-Content-Type: text/markdown
9
+
10
+ # PySpark - Fine-grained data provenance - PySpark part
11
+
12
+ ## Table of Content (ToC)
13
+
14
+ * [PySpark \- Fine\-grained data provenance \- PySpark part](#pyspark---fine-grained-data-provenance---pyspark-part)
15
+ * [Table of Content (ToC)](#table-of-content-toc)
16
+ * [Overview](#overview)
17
+ * [Quick Start](#quick-start)
18
+ * [Project Layout](#project-layout)
19
+ * [Common commands](#common-commands)
20
+ * [Development life\-cycle](#development-life-cycle)
21
+ * [PyPI package](#pypi-package)
22
+ * [CI/CD](#cicd)
23
+
24
+ Created by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc.go)
25
+
26
+ ## Overview
27
+
28
+ Python package in this monorepo for enabling Spark data provenance features
29
+ in PySpark jobs.
30
+
31
+ This project is managed with `uv`.
32
+
33
+ ## Quick Start
34
+
35
+ From the repository root:
36
+
37
+ ```bash
38
+ cd pyspark-wringlet
39
+ make init-uv-python
40
+ make init
41
+ make check
42
+ make test
43
+ make run
44
+ ```
45
+
46
+ ## Project Layout
47
+
48
+ ```text
49
+ pyspark-wringlet/
50
+ src/wringlet/
51
+ tests/
52
+ main.py
53
+ pyproject.toml
54
+ Makefile
55
+ ```
56
+
57
+ ## Common commands
58
+
59
+ * `make init` - Create/refresh lock file and sync dependencies
60
+ * `make update` - Upgrade dependencies and sync environment
61
+ * `make check` - Run lint and type checks
62
+ * `make test` - Run unit tests
63
+ * `make build` - Build package wheel
64
+ * `make publish` - Publish package (PyPI)
65
+
66
+ ## Development life-cycle
67
+
68
+ * In order to switch (bump) to a newer
69
+ [version of the Python package](https://github.com/data-prov/wringlet/blob/main/pyspark-wringlet/VERSION),
70
+ the following are the main options, ordered by the general probability of
71
+ occurrence in the development life-cycle, from the highest to the lowest)
72
+ * Increment the dev version (_e.g._, from `2.4.3.dev5` to `2.4.3.dev6` or
73
+ from `2.4.3` to `2.4.4.dev0`):
74
+ `make increment-dev-version`
75
+ * Bump to minor version (_e.g._, from `2.4.3.dev5` to `2.4.3`):
76
+ `make bump-to-minor-version`
77
+ * Bump to patch version (_e.g._, from `2.4.3.dev5` to `2.4.4`):
78
+ `make bump-to-patch-version`
79
+ * Bump to major version (_e.g._, from `2.4.3.dev5` to `3.0.0`):
80
+ `make bump-to-major-version`
81
+ * Then, the version bump has to be cascaded to related files (typically,
82
+ [`pyproject.toml`](https://github.com/data-prov/wringlet/blob/main/pyspark-wringlet/pyproject.toml))
83
+
84
+ ## PyPI package
85
+
86
+ * The
87
+ [Python package is published on PyPI](https://pypi.org/project/pyspark-data-provenance/)
88
+
89
+ * Optionally, the Python wheel may be published manually onto
90
+ [Pypi.org](https://pypi.org/project/pyspark-data-provenance/)
91
+ * The `UV_PUBLISH_TOKEN` environment variable then needs to be specified
92
+ (see the
93
+ [uv documentation](https://docs.astral.sh/uv/guides/package/#publishing-your-package)
94
+ for further details)
95
+
96
+ ```bash
97
+ make publish
98
+ ```
99
+
100
+ * However, the easiest way to publish the Python wheel is through the CI/CD
101
+ pipeline, that is, the
102
+ [GitHub Actions Python publishing pipeline](https://github.com/data-prov/wringlet/actions/workflows/python-publish.yml).
103
+ * That CI/CD pipeline is automatically triggered when creating a release on
104
+ the Git repository
105
+ * It may also be triggered manually by contributors of the Git repository
106
+
107
+ * Check the versions of the Python wheel on
108
+ [Pypi.org](https://pypi.org/project/pyspark-data-provenance/)
109
+
110
+ * Install the Python wheel locally, in the main/global Python environment
111
+ (as `spark-submit` does not seem to work properly with uv):
112
+
113
+ ```bash
114
+ make install-local
115
+ ```
116
+
117
+ * For information, that actually install the Python wheel with pip:
118
+
119
+ ```bash
120
+ python -mpip install -U pyspark-data-provenance
121
+ ```
122
+
123
+ * Run the PySpark job locally, in the main/global Python environment
124
+ (as `spark-submit` does not seem to work properly with uv):
125
+
126
+ ```bash
127
+ make run-local
128
+ ```
129
+
130
+ ## CI/CD
131
+
132
+ Repository-level workflows are provided for:
133
+
134
+ * CI: lint, type-check, and tests on pushes/PRs affecting this package
135
+ * Publish: build and publish on release
@@ -0,0 +1,9 @@
1
+ wringlet/__init__.py,sha256=8teUbaWAJzmxAny_X0WnGDD_Dm3YrNz68tQ4sl2qzy0,360
2
+ wringlet/data_provenance.py,sha256=i_FKaCMrjDVng-vXKuYaUaGo19o-cmb37KC5YguJD-0,5634
3
+ wringlet/jars/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ wringlet/jobs/getting_started_job.py,sha256=jN1VzKvJXiyCVn6xa3jJX1yZGT2KuMuIehTDcIQRjtw,2263
5
+ wringlet/py4j_utils.py,sha256=TGvYnZJmb_SgjDFVdM7hWf-dA5_4ktHFvApQ99DOTnE,602
6
+ wringlet-0.0.3.dev6.dist-info/WHEEL,sha256=GuAqCqoyQuys5_R4zkHUJFlKXw4RpRLNzo31-ui90WQ,81
7
+ wringlet-0.0.3.dev6.dist-info/entry_points.txt,sha256=IiCZThoHflXIJmJ5_675wH_RyJj4wCX4ieDa4vrVcEQ,80
8
+ wringlet-0.0.3.dev6.dist-info/METADATA,sha256=5KXEgs2Wfy-JI8nwEA5MbSORWsWObS65fjbswXSokFs,4070
9
+ wringlet-0.0.3.dev6.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.10.12
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ getting_started_job = wringlet.jobs.getting_started_job:main
3
+