pyspark-testframework 0.1.0__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.
- pyspark_testframework-0.1.0.dist-info/LICENSE +21 -0
- pyspark_testframework-0.1.0.dist-info/METADATA +104 -0
- pyspark_testframework-0.1.0.dist-info/RECORD +11 -0
- pyspark_testframework-0.1.0.dist-info/WHEEL +5 -0
- pyspark_testframework-0.1.0.dist-info/top_level.txt +1 -0
- testframework/__init__.py +0 -0
- testframework/_version.py +16 -0
- testframework/base.py +88 -0
- testframework/tests.py +21 -0
- testframework/utils/__init__.py +0 -0
- testframework/utils/decorators.py +71 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Woonstad Rotterdam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pyspark-testframework
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Testframework for PySpark DataFrames
|
|
5
|
+
Author-email: Woonstad Rotterdam <info@woonstadrotterdam.nl>, Tomer Gabay <tomer.gabay@woonstadrotterdam.nl>, Vincent van der Meij <vincent.van.der.meij@woonstadrotterdam.nl>, Tiddo Loos <tiddo.loos@woonstadrotterdam.nl>, Ben Verhees <ben.verhees@woonstadrotterdam.nl>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 Woonstad Rotterdam
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/woonstadrotterdam/pyspark-testframework
|
|
29
|
+
Project-URL: Issues, https://github.com/woonstadrotterdam/pyspark-testframework/issues
|
|
30
|
+
Keywords: pyspark,dataframe,test,testframework
|
|
31
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
32
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
33
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
34
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: OS Independent
|
|
37
|
+
Requires-Python: >=3.9.5
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Requires-Dist: pyspark >=3.3.4
|
|
41
|
+
Provides-Extra: dev
|
|
42
|
+
Requires-Dist: pyspark-testframework[test] ; extra == 'dev'
|
|
43
|
+
Requires-Dist: ruff ==0.4.8 ; extra == 'dev'
|
|
44
|
+
Provides-Extra: test
|
|
45
|
+
Requires-Dist: pre-commit ==3.7.1 ; extra == 'test'
|
|
46
|
+
Requires-Dist: pytest ==8.2.2 ; extra == 'test'
|
|
47
|
+
|
|
48
|
+
[](https://github.com/astral-sh/ruff)
|
|
49
|
+

|
|
50
|
+

|
|
51
|
+
|
|
52
|
+
# pyspark-testframework
|
|
53
|
+
|
|
54
|
+
⏳ **Work in progress**
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+

|
|
58
|
+

|
|
59
|
+

|
|
60
|
+

|
|
61
|
+

|
|
62
|
+

|
|
63
|
+
&width=145>)
|
|
64
|
+
|
|
65
|
+
The goal of the `pyspark-testframework` is to provide a simple way to create tests for PySpark DataFrames. The test results are returned in DataFrame format as well.
|
|
66
|
+
|
|
67
|
+
## Example
|
|
68
|
+
|
|
69
|
+
Input DataFrame:
|
|
70
|
+
|
|
71
|
+
| primary_key | email |
|
|
72
|
+
| ----------- | ------------------------- |
|
|
73
|
+
| 1 | info@woonstadrotterdam.nl |
|
|
74
|
+
| 2 | infowoonstadrotterdam.nl |
|
|
75
|
+
| 3 | @woonstadrotterdam.nl |
|
|
76
|
+
| 4 | dev@woonstadrotterdam.nl |
|
|
77
|
+
| 5 | Null |
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from testframework.tests import RegexTest
|
|
81
|
+
|
|
82
|
+
email_regex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
|
83
|
+
|
|
84
|
+
mail_tester = RegexTest(
|
|
85
|
+
name="ValidEmail",
|
|
86
|
+
pattern=email_regex
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
test_result = mail_tester.test(
|
|
90
|
+
df=df,
|
|
91
|
+
col="email",
|
|
92
|
+
nullable=False
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
test_result.show()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| primary_key | email\_\_ValidEmail |
|
|
99
|
+
| ----------- | ------------------- |
|
|
100
|
+
| 1 | True |
|
|
101
|
+
| 2 | False |
|
|
102
|
+
| 3 | False |
|
|
103
|
+
| 4 | True |
|
|
104
|
+
| 5 | False |
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
testframework/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
testframework/_version.py,sha256=IMl2Pr_Sy4LVRKy_Sm4CdwUl1Gryous6ncL96EMYsnM,411
|
|
3
|
+
testframework/base.py,sha256=4L6UD6TgRiQMvCyJykvJKzXxhR8NXay7N2r7UTJmT-M,2854
|
|
4
|
+
testframework/tests.py,sha256=IcI1O-A6HCMFPPkHKB4Q4B8d1hTx8PNU1aO3JDJ95ww,691
|
|
5
|
+
testframework/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
testframework/utils/decorators.py,sha256=IuW0DXnYqFFGZqdiZQmr01MqTUQLni71MIHhC7MAVQ0,2427
|
|
7
|
+
pyspark_testframework-0.1.0.dist-info/LICENSE,sha256=XHXrWnYTM8HAM7azt1zAhsP6aD-sM-2DPoUAfoYx57Y,1075
|
|
8
|
+
pyspark_testframework-0.1.0.dist-info/METADATA,sha256=bW7590ROTcc824BtSR4sF141nnlpd2RjzZLOHhNzoOQ,4482
|
|
9
|
+
pyspark_testframework-0.1.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
10
|
+
pyspark_testframework-0.1.0.dist-info/top_level.txt,sha256=_4NVWFQs5_tlaT7Uw1qDG1-F4Q8L9cQxmK9wvaHOtss,14
|
|
11
|
+
pyspark_testframework-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
testframework
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# file generated by setuptools_scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
TYPE_CHECKING = False
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Tuple, Union
|
|
6
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
7
|
+
else:
|
|
8
|
+
VERSION_TUPLE = object
|
|
9
|
+
|
|
10
|
+
version: str
|
|
11
|
+
__version__: str
|
|
12
|
+
__version_tuple__: VERSION_TUPLE
|
|
13
|
+
version_tuple: VERSION_TUPLE
|
|
14
|
+
|
|
15
|
+
__version__ = version = '0.1.0'
|
|
16
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
testframework/base.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional, Union
|
|
3
|
+
|
|
4
|
+
from pyspark.sql import Column, DataFrame
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DataQualityTest(ABC):
|
|
8
|
+
def __init__(self, name: str, description: Optional[str] = None):
|
|
9
|
+
self.name = name
|
|
10
|
+
self.description = description
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def test(
|
|
14
|
+
self,
|
|
15
|
+
df: DataFrame,
|
|
16
|
+
col: str,
|
|
17
|
+
primary_key: Union[str, list[str]],
|
|
18
|
+
nullable: bool,
|
|
19
|
+
result_col: Optional[str] = None,
|
|
20
|
+
) -> DataFrame:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def _test_impl(self, df: DataFrame, col: str, nullable: bool) -> Column:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Test(DataQualityTest):
|
|
29
|
+
"""
|
|
30
|
+
Abstract base class for implementing various tests on DataFrame columns.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def _test_impl(self, df: DataFrame, col: str, nullable: bool) -> Column:
|
|
35
|
+
"""
|
|
36
|
+
Abstract method for implementing the test logic on a specified column.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
df (DataFrame): The DataFrame containing the column to test.
|
|
40
|
+
col (str): The name of the column to test.
|
|
41
|
+
nullable (bool): Flag indicating whether the column can contain null values.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Column: The result of the test as a Spark SQL Column.
|
|
45
|
+
"""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def generate_result_col_name(self, col: str) -> str:
|
|
49
|
+
"""
|
|
50
|
+
Generates a standardized name for the test result column.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
col (str): The name of the column being tested.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
str: The name of the test result column.
|
|
57
|
+
"""
|
|
58
|
+
return f"{col}__{self.name}"
|
|
59
|
+
|
|
60
|
+
def test(
|
|
61
|
+
self,
|
|
62
|
+
df: DataFrame,
|
|
63
|
+
col: str,
|
|
64
|
+
primary_key: Union[str, list[str]],
|
|
65
|
+
nullable: bool,
|
|
66
|
+
result_col: Optional[str] = None,
|
|
67
|
+
) -> DataFrame:
|
|
68
|
+
"""
|
|
69
|
+
Applies the test to the specified column of the DataFrame.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
df (DataFrame): The DataFrame to test.
|
|
73
|
+
col (str): The name of the column to test.
|
|
74
|
+
primary_key (Union[str, list[str]]): The column name(s) of the primary key(s).
|
|
75
|
+
nullable (bool): Flag indicating whether the column is allowed to have Null values.
|
|
76
|
+
result_col (Optional[str]): The name of the column to store the test result. By default None. If None, a default name will be generated.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
DataFrame: A DataFrame with the test results for the specified column.
|
|
80
|
+
"""
|
|
81
|
+
primary_key = [primary_key] if isinstance(primary_key, str) else primary_key
|
|
82
|
+
test_function = self._test_impl(df, col, nullable)
|
|
83
|
+
result_col = result_col if result_col else self.generate_result_col_name(col)
|
|
84
|
+
|
|
85
|
+
# Apply the test result to the DataFrame
|
|
86
|
+
return df.withColumn(result_col, test_function).select(
|
|
87
|
+
primary_key + [col, result_col]
|
|
88
|
+
)
|
testframework/tests.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from pyspark.sql import Column, DataFrame
|
|
2
|
+
from pyspark.sql import functions as F
|
|
3
|
+
from pyspark.sql.types import StringType
|
|
4
|
+
|
|
5
|
+
from testframework.base import Test
|
|
6
|
+
from testframework.utils.decorators import account_for_nullable, allowed_col_types
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RegexTest(Test):
|
|
10
|
+
"""
|
|
11
|
+
Test to check if a column's values match a regular expression pattern.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, name: str, pattern: str) -> None:
|
|
15
|
+
super().__init__(name=name)
|
|
16
|
+
self.pattern = pattern
|
|
17
|
+
|
|
18
|
+
@account_for_nullable
|
|
19
|
+
@allowed_col_types([StringType])
|
|
20
|
+
def _test_impl(self, df: DataFrame, col: str, nullable: bool) -> Column:
|
|
21
|
+
return F.regexp_extract(F.col(col), self.pattern, 0) != ""
|
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
import pyspark.sql.functions as F
|
|
5
|
+
from pyspark.sql import Column, DataFrame
|
|
6
|
+
from pyspark.sql.types import DataType
|
|
7
|
+
|
|
8
|
+
from testframework.base import Test
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def account_for_nullable(
|
|
12
|
+
func: Callable[[Test, DataFrame, str, bool], Column],
|
|
13
|
+
) -> Column:
|
|
14
|
+
"""
|
|
15
|
+
A decorator for test methods that adjusts the return value based on the column's nullability
|
|
16
|
+
|
|
17
|
+
This decorator wraps a test method to handle nullable columns appropriately. If a column
|
|
18
|
+
contains null values, those values will be converted to True or False, depending on the `nullable` parameter
|
|
19
|
+
passed to the test method. The decorator ensures that the final result is a boolean column,
|
|
20
|
+
with True for values matching the test criteria, False for non-matching values.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@wraps(func)
|
|
24
|
+
def wrapper(test: Test, df: DataFrame, column: str, nullable: bool) -> Column:
|
|
25
|
+
# Call the original test implementation
|
|
26
|
+
test_result = func(test, df, column, nullable)
|
|
27
|
+
|
|
28
|
+
# Adjust the result based on the nullable flag and null values
|
|
29
|
+
if nullable:
|
|
30
|
+
return (
|
|
31
|
+
F.when(F.col(column).isNull(), F.lit(True))
|
|
32
|
+
.otherwise(test_result)
|
|
33
|
+
.cast("boolean")
|
|
34
|
+
)
|
|
35
|
+
else:
|
|
36
|
+
return (
|
|
37
|
+
F.when(F.col(column).isNull(), F.lit(False))
|
|
38
|
+
.otherwise(test_result)
|
|
39
|
+
.cast("boolean")
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return wrapper
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def allowed_col_types(
|
|
46
|
+
expected_types: list[DataType],
|
|
47
|
+
) -> Callable[
|
|
48
|
+
[Callable[[Test, DataFrame, str, bool], DataFrame]],
|
|
49
|
+
Callable[[Test, DataFrame, str, bool], DataFrame],
|
|
50
|
+
]:
|
|
51
|
+
expected_types_tuple = tuple(
|
|
52
|
+
expected_types
|
|
53
|
+
) # Convert list to tuple once outside the wrapper
|
|
54
|
+
|
|
55
|
+
def decorator(
|
|
56
|
+
func: Callable[[Test, DataFrame, str, bool], DataFrame],
|
|
57
|
+
) -> Callable[[Test, DataFrame, str, bool], DataFrame]:
|
|
58
|
+
@wraps(func)
|
|
59
|
+
def wrapper(
|
|
60
|
+
test: Test, df: DataFrame, column: str, nullable: bool
|
|
61
|
+
) -> DataFrame:
|
|
62
|
+
col_type = df.schema[column].dataType
|
|
63
|
+
if not isinstance(col_type, expected_types_tuple):
|
|
64
|
+
raise TypeError(
|
|
65
|
+
f"Column '{column}' is not of expected types {expected_types}. Found type: {col_type}"
|
|
66
|
+
)
|
|
67
|
+
return func(test, df, column, nullable)
|
|
68
|
+
|
|
69
|
+
return wrapper
|
|
70
|
+
|
|
71
|
+
return decorator
|