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 +15 -0
- wringlet/data_provenance.py +156 -0
- wringlet/jars/.gitkeep +0 -0
- wringlet/jobs/getting_started_job.py +78 -0
- wringlet/py4j_utils.py +21 -0
- wringlet-0.0.3.dev6.dist-info/METADATA +135 -0
- wringlet-0.0.3.dev6.dist-info/RECORD +9 -0
- wringlet-0.0.3.dev6.dist-info/WHEEL +4 -0
- wringlet-0.0.3.dev6.dist-info/entry_points.txt +3 -0
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,,
|