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.
@@ -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
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
49
+ ![MIT License](https://img.shields.io/badge/License-MIT-yellow.svg)
50
+ ![Build Status](https://github.com/woonstadrotterdam/pyspark-testframework/actions/workflows/cicd.yml/badge.svg)
51
+
52
+ # pyspark-testframework
53
+
54
+ ⏳ **Work in progress**
55
+
56
+ ![](https://progress-bar.dev/100/?title=RegexTest&width=120)
57
+ ![](https://progress-bar.dev/50/?title=IsInteger&width=120)
58
+ ![](https://progress-bar.dev/50/?title=ValidEmail&width=113)
59
+ ![](https://progress-bar.dev/0/?title=ContainsValue&width=95)
60
+ ![](https://progress-bar.dev/0/?title=ValidValueRange&width=83)
61
+ ![](https://progress-bar.dev/0/?title=ValidCategory&width=95)
62
+ ![](https://progress-bar.dev/0/?title=CorrectValue&width=102)
63
+ ![](<https://progress-bar.dev/0/?title=(...)&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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.43.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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