data-validation-engine 0.9.0__tar.gz → 0.10.0__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.
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/PKG-INFO +9 -7
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/README.md +3 -1
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/pyproject.toml +13 -10
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/common/error_utils.py +1 -1
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/base/contract.py +0 -4
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/base/reader.py +71 -24
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/base/rules.py +194 -3
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/exceptions.py +28 -16
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/duckdb/duckdb_helpers.py +26 -6
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/duckdb/readers/csv.py +20 -17
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/duckdb/readers/json.py +2 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/duckdb/readers/xml.py +8 -4
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/duckdb/rules.py +107 -38
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/spark/contract.py +3 -2
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/spark/readers/csv.py +6 -4
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/spark/readers/json.py +2 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/spark/readers/xml.py +9 -5
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/spark/rules.py +127 -25
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/spark/spark_helpers.py +24 -4
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/metadata/contract.py +1 -1
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/metadata/rules.py +15 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/readers/csv.py +6 -8
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/readers/utilities.py +12 -13
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/readers/xml.py +10 -12
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/readers/xml_linting.py +3 -2
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/utilities.py +1 -2
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/configuration/v1/__init__.py +97 -6
- data_validation_engine-0.10.0/src/dve/core_engine/configuration/v1/hierarchy.py +210 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/constants.py +5 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/functions/implementations.py +0 -110
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/message.py +4 -2
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/models.py +3 -1
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/type_hints.py +10 -1
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/metadata_parser/models.py +1 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/parser/file_handling/service.py +5 -2
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/pipeline/pipeline.py +188 -28
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/pipeline/utils.py +28 -2
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/reporting/__init__.py +2 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/reporting/error_report.py +1 -1
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/reporting/excel_report.py +11 -2
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/LICENSE +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/common/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/base/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/base/auditing.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/base/backend.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/base/core.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/base/reference_data.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/base/utilities.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/duckdb/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/duckdb/auditing.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/duckdb/contract.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/duckdb/readers/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/duckdb/reference_data.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/duckdb/types.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/duckdb/utilities.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/spark/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/spark/auditing.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/spark/backend.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/spark/readers/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/spark/reference_data.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/spark/types.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/implementations/spark/utilities.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/metadata/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/metadata/reporting.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/readers/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/backends/types.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/configuration/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/configuration/base.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/configuration/v1/filters.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/configuration/v1/rule_stores/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/configuration/v1/rule_stores/models.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/configuration/v1/steps.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/engine.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/exceptions.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/functions/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/loggers.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/templating.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/core_engine/validation.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/metadata_parser/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/metadata_parser/domain_types.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/metadata_parser/exc.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/metadata_parser/function_library.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/metadata_parser/function_wrapper.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/metadata_parser/model_generator.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/metadata_parser/utilities.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/parser/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/parser/exceptions.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/parser/file_handling/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/parser/file_handling/helpers.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/parser/file_handling/implementations/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/parser/file_handling/implementations/base.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/parser/file_handling/implementations/dbfs.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/parser/file_handling/implementations/file.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/parser/file_handling/implementations/s3.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/parser/file_handling/log_handler.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/parser/file_handling/utilities.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/parser/type_hints.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/parser/utilities.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/pipeline/__init__.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/pipeline/duckdb_pipeline.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/pipeline/foundry_ddb_pipeline.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/pipeline/spark_pipeline.py +0 -0
- {data_validation_engine-0.9.0 → data_validation_engine-0.10.0}/src/dve/reporting/constants.py +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: data-validation-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: `nhs data validation engine` is a framework used to validate data
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Author: NHS England
|
|
8
8
|
Author-email: england.contactus@nhs.net
|
|
9
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.11,<3.13
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
11
|
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Operating System :: POSIX :: Linux
|
|
@@ -18,7 +18,7 @@ Requires-Dist: Jinja2 (==3.1.6)
|
|
|
18
18
|
Requires-Dist: boto3 (>=1.34.162,<1.36)
|
|
19
19
|
Requires-Dist: botocore (>=1.34.162,<1.36)
|
|
20
20
|
Requires-Dist: delta-spark (>=3.0.0,<=3.2.0)
|
|
21
|
-
Requires-Dist: duckdb (
|
|
21
|
+
Requires-Dist: duckdb (>=1.4,<1.4.5)
|
|
22
22
|
Requires-Dist: lxml (==6.1.1)
|
|
23
23
|
Requires-Dist: numpy (==1.26.4)
|
|
24
24
|
Requires-Dist: openpyxl (==3.1.5)
|
|
@@ -27,8 +27,8 @@ Requires-Dist: polars (==0.20.31)
|
|
|
27
27
|
Requires-Dist: pyarrow (==23.0.1)
|
|
28
28
|
Requires-Dist: pydantic (==2.13.4)
|
|
29
29
|
Requires-Dist: pyspark (>=3.5.0,<=3.5.5)
|
|
30
|
-
Requires-Dist: typing_extensions (==4.
|
|
31
|
-
Requires-Dist: urllib3 (==2.
|
|
30
|
+
Requires-Dist: typing_extensions (==4.16.0)
|
|
31
|
+
Requires-Dist: urllib3 (==2.8.0)
|
|
32
32
|
Project-URL: Changelog, https://github.com/NHSDigital/data-validation-engine/blob/main/CHANGELOG.md
|
|
33
33
|
Project-URL: Documentation, https://nhsdigital.github.io/data-validation-engine/
|
|
34
34
|
Project-URL: Homepage, https://github.com/NHSDigital/data-validation-engine
|
|
@@ -55,6 +55,7 @@ __The DVE offers__:
|
|
|
55
55
|
- Format normalization to Parquet for a unified data representation
|
|
56
56
|
- Data modelling and typecasting
|
|
57
57
|
- Business-rule validations executed on supported backends such as Spark and DuckDB, with the option to add custom backends
|
|
58
|
+
- Validate and enforce referential integrity checks with minimal configuration
|
|
58
59
|
- Deriving new fields and entities
|
|
59
60
|
- Clear validation reporting, including summary insights and record-level error messages
|
|
60
61
|
|
|
@@ -79,7 +80,8 @@ Below is a list of features that we would like to implement or have been request
|
|
|
79
80
|
| Uplift to Python 3.11 | 0.2.0 | Yes |
|
|
80
81
|
| Uplift Pyspark to 3.5 | 0.8.0 | Yes |
|
|
81
82
|
| Allow DVE to run on Python 3.12+ | 0.8.0 | Yes |
|
|
82
|
-
| Upgrade to Pydantic 2.0 | 0.9.0 | Yes
|
|
83
|
+
| Upgrade to Pydantic 2.0 | 0.9.0 | Yes |
|
|
84
|
+
| Upgrade DuckDB to v1.4 | 0.10.0 | Yes |
|
|
83
85
|
| Uplift Pyspark to 4.0+ | TBA | No |
|
|
84
86
|
| Polars upgrade to v1+ | TBA | No |
|
|
85
87
|
| DuckDB upgrade to v1.5+ | TBA | No |
|
|
@@ -17,6 +17,7 @@ __The DVE offers__:
|
|
|
17
17
|
- Format normalization to Parquet for a unified data representation
|
|
18
18
|
- Data modelling and typecasting
|
|
19
19
|
- Business-rule validations executed on supported backends such as Spark and DuckDB, with the option to add custom backends
|
|
20
|
+
- Validate and enforce referential integrity checks with minimal configuration
|
|
20
21
|
- Deriving new fields and entities
|
|
21
22
|
- Clear validation reporting, including summary insights and record-level error messages
|
|
22
23
|
|
|
@@ -41,7 +42,8 @@ Below is a list of features that we would like to implement or have been request
|
|
|
41
42
|
| Uplift to Python 3.11 | 0.2.0 | Yes |
|
|
42
43
|
| Uplift Pyspark to 3.5 | 0.8.0 | Yes |
|
|
43
44
|
| Allow DVE to run on Python 3.12+ | 0.8.0 | Yes |
|
|
44
|
-
| Upgrade to Pydantic 2.0 | 0.9.0 | Yes
|
|
45
|
+
| Upgrade to Pydantic 2.0 | 0.9.0 | Yes |
|
|
46
|
+
| Upgrade DuckDB to v1.4 | 0.10.0 | Yes |
|
|
45
47
|
| Uplift Pyspark to 4.0+ | TBA | No |
|
|
46
48
|
| Polars upgrade to v1+ | TBA | No |
|
|
47
49
|
| DuckDB upgrade to v1.5+ | TBA | No |
|
|
@@ -7,8 +7,8 @@ authors = [
|
|
|
7
7
|
]
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
classifiers = [
|
|
10
|
+
"Development Status :: 4 - Beta",
|
|
10
11
|
"Programming Language :: Python :: 3",
|
|
11
|
-
"Programming Language :: Python :: 3.10",
|
|
12
12
|
"Programming Language :: Python :: 3.11",
|
|
13
13
|
"Programming Language :: Python :: 3.12",
|
|
14
14
|
"Operating System :: POSIX :: Linux",
|
|
@@ -25,17 +25,17 @@ Issues = "https://github.com/NHSDigital/data-validation-engine/issues"
|
|
|
25
25
|
Changelog = "https://github.com/NHSDigital/data-validation-engine/blob/main/CHANGELOG.md"
|
|
26
26
|
|
|
27
27
|
[tool.poetry]
|
|
28
|
-
version = "0.
|
|
28
|
+
version = "0.10.0"
|
|
29
29
|
packages = [
|
|
30
30
|
{ include = "dve", from = "src" },
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
[tool.poetry.dependencies]
|
|
34
|
-
python = ">=3.
|
|
34
|
+
python = ">=3.11,<3.13" # breaking changes beyond 3.12
|
|
35
35
|
boto3 = ">=1.34.162,<1.36" # breaking change beyond 1.36
|
|
36
36
|
botocore = ">=1.34.162,<1.36" # breaking change beyond 1.36
|
|
37
37
|
delta-spark = ">=3.0.0,<=3.2.0"
|
|
38
|
-
duckdb = "1.1.
|
|
38
|
+
duckdb = ">=1.4,<1.4.5"
|
|
39
39
|
Jinja2 = "3.1.6"
|
|
40
40
|
lxml = "6.1.1"
|
|
41
41
|
numpy = "1.26.4"
|
|
@@ -45,8 +45,8 @@ polars = "0.20.31"
|
|
|
45
45
|
pyarrow = "23.0.1"
|
|
46
46
|
pydantic = "2.13.4"
|
|
47
47
|
pyspark = ">=3.5.0,<=3.5.5"
|
|
48
|
-
typing_extensions = "4.
|
|
49
|
-
urllib3 = "2.
|
|
48
|
+
typing_extensions = "4.16.0"
|
|
49
|
+
urllib3 = "2.8.0" # dependency of boto3 & botocore
|
|
50
50
|
|
|
51
51
|
[tool.poetry.group.dev]
|
|
52
52
|
optional = true
|
|
@@ -59,8 +59,11 @@ include-groups = [
|
|
|
59
59
|
commitizen = "4.9.1"
|
|
60
60
|
pre-commit = "4.3.0"
|
|
61
61
|
charset-normalizer = "3.4.6"
|
|
62
|
-
python-discovery = "1.
|
|
62
|
+
python-discovery = "1.6.1"
|
|
63
63
|
idna = "3.15" # dependency of requests
|
|
64
|
+
virtualenv = "21.14.9"
|
|
65
|
+
certifi = "2026.10.23"
|
|
66
|
+
filelock = "4.2.1"
|
|
64
67
|
|
|
65
68
|
[tool.poetry.group.test]
|
|
66
69
|
optional = true
|
|
@@ -71,7 +74,7 @@ coverage = "7.11.0"
|
|
|
71
74
|
moto = {extras = ["s3"], version = "4.2.14"}
|
|
72
75
|
cryptography = "50.0.0" # dependency of `moto`
|
|
73
76
|
requests = "2.33.0" # dependency of `moto`
|
|
74
|
-
Werkzeug = "3.1.
|
|
77
|
+
Werkzeug = "3.1.9"
|
|
75
78
|
pytest = "9.0.3"
|
|
76
79
|
pytest-lazy-fixtures = "1.4.0" # switched from https://github.com/TvoroG/pytest-lazy-fixture as it's no longer supported
|
|
77
80
|
xlsx2csv = "0.8.4" # polars requirement
|
|
@@ -80,7 +83,7 @@ xlsx2csv = "0.8.4" # polars requirement
|
|
|
80
83
|
optional = true
|
|
81
84
|
|
|
82
85
|
[tool.poetry.group.lint.dependencies]
|
|
83
|
-
black = "26.
|
|
86
|
+
black = "26.10.1"
|
|
84
87
|
astroid = "3.3.9"
|
|
85
88
|
isort = "5.13.2"
|
|
86
89
|
pylint = "3.3.9"
|
|
@@ -108,7 +111,7 @@ mkdocs = "1.6.1"
|
|
|
108
111
|
mkdocstrings = { version = "1.0.3", extras = ["python"] }
|
|
109
112
|
griffelib = "2.0.1"
|
|
110
113
|
pymdown-extensions = "11.0.1"
|
|
111
|
-
zensical = "0.0.
|
|
114
|
+
zensical = "0.0.63"
|
|
112
115
|
|
|
113
116
|
[tool.ruff]
|
|
114
117
|
line-length = 100
|
|
@@ -443,10 +443,6 @@ class BaseDataContract(Generic[EntityType], ABC):
|
|
|
443
443
|
],
|
|
444
444
|
)
|
|
445
445
|
|
|
446
|
-
if contract_metadata.cache_originals:
|
|
447
|
-
for entity_name in list(entities):
|
|
448
|
-
entities[f"Original{entity_name}"] = entities[entity_name]
|
|
449
|
-
|
|
450
446
|
return entities, feedback_errors_uri, successful, processing_errors_uri
|
|
451
447
|
|
|
452
448
|
def read_parquet(self, path: URI, **kwargs) -> EntityType:
|
|
@@ -8,8 +8,16 @@ from typing import Any, ClassVar, Optional, TypeVar
|
|
|
8
8
|
from pydantic import BaseModel
|
|
9
9
|
from typing_extensions import Protocol
|
|
10
10
|
|
|
11
|
-
from dve.core_engine.backends.exceptions import
|
|
11
|
+
from dve.core_engine.backends.exceptions import (
|
|
12
|
+
CriticalMessageBearingError,
|
|
13
|
+
MessageBearingError,
|
|
14
|
+
ReaderLacksEntityTypeSupport,
|
|
15
|
+
)
|
|
12
16
|
from dve.core_engine.backends.types import EntityName, EntityType
|
|
17
|
+
from dve.core_engine.configuration.v1 import (
|
|
18
|
+
AllowedAdditionalReaderChecks,
|
|
19
|
+
_ReaderAdditionalChecksConfig,
|
|
20
|
+
)
|
|
13
21
|
from dve.core_engine.message import FeedbackMessage
|
|
14
22
|
from dve.core_engine.type_hints import URI, ArbitraryFunction, WrapDecorator
|
|
15
23
|
from dve.parser.file_handling.service import open_stream
|
|
@@ -65,6 +73,10 @@ class BaseFileReader(ABC):
|
|
|
65
73
|
decorated with the '@read_function' decorator, and is used in `read_entity_type`.
|
|
66
74
|
|
|
67
75
|
"""
|
|
76
|
+
ft_error_code: Optional[str] = "MalformedFile"
|
|
77
|
+
"""Default error code for when a file/submission cannot be parsed succesfully."""
|
|
78
|
+
ft_error_message: Optional[str] = "The resource doesn't seem to be a valid text file"
|
|
79
|
+
"""Default error message for when a file/submission cannot be parsed succesfully."""
|
|
68
80
|
|
|
69
81
|
def __init_subclass__(cls, *_, **__) -> None:
|
|
70
82
|
"""When this class is subclassed, create and populate the `__read_methods__`
|
|
@@ -109,7 +121,10 @@ class BaseFileReader(ABC):
|
|
|
109
121
|
entity_name: EntityName,
|
|
110
122
|
schema: type[BaseModel],
|
|
111
123
|
all_model_fields: Optional[set[str]] = None,
|
|
112
|
-
|
|
124
|
+
additional_checks: Optional[
|
|
125
|
+
dict[AllowedAdditionalReaderChecks, _ReaderAdditionalChecksConfig]
|
|
126
|
+
] = None,
|
|
127
|
+
):
|
|
113
128
|
"""Read to the specified entity type, if supported.
|
|
114
129
|
|
|
115
130
|
NOTE: Simple types should either be returned as strings (if present) or
|
|
@@ -117,21 +132,47 @@ class BaseFileReader(ABC):
|
|
|
117
132
|
data contract.
|
|
118
133
|
|
|
119
134
|
"""
|
|
120
|
-
|
|
121
|
-
|
|
135
|
+
additional_checks = additional_checks or {}
|
|
136
|
+
|
|
137
|
+
self.raise_if_not_sensible_file(resource, entity_name)
|
|
138
|
+
|
|
139
|
+
if entity_type == Iterator[dict[str, Any]]:
|
|
140
|
+
entity = self.read_to_py_iterator(
|
|
122
141
|
resource, entity_name, schema, all_model_fields # type: ignore
|
|
123
142
|
)
|
|
124
143
|
|
|
125
|
-
|
|
144
|
+
else:
|
|
126
145
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
146
|
+
try:
|
|
147
|
+
reader_func = self.__read_methods__[entity_type]
|
|
148
|
+
except KeyError as err:
|
|
149
|
+
raise ReaderLacksEntityTypeSupport(entity_type=entity_type) from err
|
|
131
150
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
151
|
+
entity = reader_func(
|
|
152
|
+
self,
|
|
153
|
+
resource,
|
|
154
|
+
entity_name,
|
|
155
|
+
schema,
|
|
156
|
+
all_model_fields=all_model_fields, # type: ignore
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if config := additional_checks.get("check_empty"):
|
|
160
|
+
if self.check_entity_empty(entity):
|
|
161
|
+
raise MessageBearingError(
|
|
162
|
+
f"The mandatory entity {entity_name} is empty",
|
|
163
|
+
messages=[
|
|
164
|
+
FeedbackMessage(
|
|
165
|
+
entity=entity_name,
|
|
166
|
+
record=None,
|
|
167
|
+
failure_type="submission",
|
|
168
|
+
error_location=entity_name,
|
|
169
|
+
error_code=config.error_code,
|
|
170
|
+
error_message=config.error_message,
|
|
171
|
+
)
|
|
172
|
+
],
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return entity
|
|
135
176
|
|
|
136
177
|
def add_record_index(self, entity: EntityType, **kwargs) -> EntityType:
|
|
137
178
|
"""Add a record index to the entity"""
|
|
@@ -141,6 +182,10 @@ class BaseFileReader(ABC):
|
|
|
141
182
|
"""Drop a record index to the entity"""
|
|
142
183
|
raise NotImplementedError(f"drop_record_index not implemented in {self.__class__}")
|
|
143
184
|
|
|
185
|
+
def check_entity_empty(self, entity: EntityType) -> bool:
|
|
186
|
+
"""Determine if the entity supplied is empty"""
|
|
187
|
+
raise NotImplementedError(f"check_entity_empty not implemented in {self.__class__}")
|
|
188
|
+
|
|
144
189
|
def write_parquet(
|
|
145
190
|
self,
|
|
146
191
|
entity: EntityType,
|
|
@@ -171,20 +216,22 @@ class BaseFileReader(ABC):
|
|
|
171
216
|
return False
|
|
172
217
|
return True
|
|
173
218
|
|
|
174
|
-
def raise_if_not_sensible_file(
|
|
219
|
+
def raise_if_not_sensible_file(
|
|
220
|
+
self,
|
|
221
|
+
resource: URI,
|
|
222
|
+
entity_name: str,
|
|
223
|
+
):
|
|
175
224
|
"""Sense check that the file is a text file. Raise error if doesn't
|
|
176
225
|
appear to be the case."""
|
|
177
226
|
if not self._check_likely_text_file(resource):
|
|
178
|
-
raise
|
|
227
|
+
raise CriticalMessageBearingError(
|
|
179
228
|
"The submitted file doesn't appear to be text",
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
)
|
|
189
|
-
],
|
|
229
|
+
message=FeedbackMessage(
|
|
230
|
+
entity=entity_name,
|
|
231
|
+
record=None,
|
|
232
|
+
failure_type="submission",
|
|
233
|
+
error_location="Whole File",
|
|
234
|
+
error_code=self.ft_error_code,
|
|
235
|
+
error_message=self.ft_error_message,
|
|
236
|
+
),
|
|
190
237
|
)
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import logging
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
5
|
from collections import defaultdict
|
|
6
|
-
from collections.abc import Iterable
|
|
7
|
-
from typing import Any, ClassVar, Generic, NoReturn, Optional, TypeVar
|
|
6
|
+
from collections.abc import Iterable, Iterator
|
|
7
|
+
from typing import Any, ClassVar, Generic, MutableMapping, NoReturn, Optional, TypeVar
|
|
8
8
|
from uuid import uuid4
|
|
9
9
|
|
|
10
10
|
from typing_extensions import Literal, Protocol, get_type_hints
|
|
@@ -27,6 +27,7 @@ from dve.core_engine.backends.metadata.rules import (
|
|
|
27
27
|
CopyEntity,
|
|
28
28
|
DeferredFilter,
|
|
29
29
|
EntityRemoval,
|
|
30
|
+
GroupIdentification,
|
|
30
31
|
HeaderJoin,
|
|
31
32
|
ImmediateFilter,
|
|
32
33
|
InnerJoin,
|
|
@@ -43,8 +44,11 @@ from dve.core_engine.backends.metadata.rules import (
|
|
|
43
44
|
TableUnion,
|
|
44
45
|
)
|
|
45
46
|
from dve.core_engine.backends.types import Entities, EntityType, StageSuccessful
|
|
47
|
+
from dve.core_engine.configuration.v1.hierarchy import EntityHierarchy, HierarchyNode
|
|
46
48
|
from dve.core_engine.exceptions import CriticalProcessingError
|
|
47
49
|
from dve.core_engine.loggers import get_logger
|
|
50
|
+
from dve.core_engine.message import FeedbackMessage
|
|
51
|
+
from dve.core_engine.templating import template_object
|
|
48
52
|
from dve.core_engine.type_hints import URI, DVEStageName, EntityName, Messages, TemplateVariables
|
|
49
53
|
|
|
50
54
|
T_contra = TypeVar("T_contra", bound=AbstractStep, contravariant=True)
|
|
@@ -55,6 +59,8 @@ _StepFunctions = dict[type[T], "_UnboundStepFunction[T]"]
|
|
|
55
59
|
"""A convenience type indicating a mapping from config type to step method."""
|
|
56
60
|
Stage = Literal["Pre-filter", "Filter", "Post-filter"]
|
|
57
61
|
"""The name of a stage within a rule."""
|
|
62
|
+
TempTableName = str
|
|
63
|
+
"""temp tables to cache intermediate results"""
|
|
58
64
|
|
|
59
65
|
|
|
60
66
|
class _UnboundStepFunction(Generic[T_contra], Protocol): # pylint: disable=too-few-public-methods
|
|
@@ -128,6 +134,7 @@ class BaseStepImplementations(Generic[EntityType], ABC): # pylint: disable=too-
|
|
|
128
134
|
):
|
|
129
135
|
self.logger = logger or get_logger(type(self).__name__)
|
|
130
136
|
"""The `logging.Logger instance for the data contract config."""
|
|
137
|
+
self.entity_cache_tracker: MutableMapping[EntityName, TempTableName] = {}
|
|
131
138
|
|
|
132
139
|
@classmethod
|
|
133
140
|
@abstractmethod
|
|
@@ -307,7 +314,8 @@ class BaseStepImplementations(Generic[EntityType], ABC): # pylint: disable=too-
|
|
|
307
314
|
"""
|
|
308
315
|
raise NotImplementedError
|
|
309
316
|
|
|
310
|
-
|
|
317
|
+
@abstractmethod
|
|
318
|
+
def identify_orphans(self, entities: Entities, *, config: OrphanIdentification) -> Iterable:
|
|
311
319
|
"""Identify records in an entity which don't have at least one corresponding
|
|
312
320
|
match in the target. A new boolean column will be added to `entity` ('IsOrphaned')
|
|
313
321
|
indicating whether the condition matched.
|
|
@@ -320,6 +328,14 @@ class BaseStepImplementations(Generic[EntityType], ABC): # pylint: disable=too-
|
|
|
320
328
|
"""
|
|
321
329
|
raise NotImplementedError
|
|
322
330
|
|
|
331
|
+
@abstractmethod
|
|
332
|
+
def check_mandatory_group(self, entities: Entities, *, config: GroupIdentification) -> Iterator:
|
|
333
|
+
"""
|
|
334
|
+
Check that a mandatory key in an entity has at least one valid entry in the all the child
|
|
335
|
+
entities.
|
|
336
|
+
"""
|
|
337
|
+
raise NotImplementedError
|
|
338
|
+
|
|
323
339
|
@abstractmethod
|
|
324
340
|
def union(self, entities: Entities, *, config: TableUnion) -> Messages:
|
|
325
341
|
"""Union two entities together, taking the columns from each by name.
|
|
@@ -352,6 +368,157 @@ class BaseStepImplementations(Generic[EntityType], ABC): # pylint: disable=too-
|
|
|
352
368
|
|
|
353
369
|
"""
|
|
354
370
|
|
|
371
|
+
def identify_and_remove_orphans(
|
|
372
|
+
self,
|
|
373
|
+
working_directory: URI,
|
|
374
|
+
entities: Entities,
|
|
375
|
+
entity_hierarchy: EntityHierarchy,
|
|
376
|
+
key_fields: Optional[dict[str, list[str]]] = None,
|
|
377
|
+
) -> tuple[Messages, dict[EntityName, bool]]:
|
|
378
|
+
"""
|
|
379
|
+
Identifies and removes orphan records by traversing the EntityHierarchy object.
|
|
380
|
+
An orphan is a child record whose parent FK does not exist in the parent entity.
|
|
381
|
+
Processes recursively: removes orphans at each level, then processes children.
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
def process_node(node: HierarchyNode) -> bool:
|
|
385
|
+
"""Identify orphans and remove in a given node"""
|
|
386
|
+
issues_found: bool = False
|
|
387
|
+
if node.parent_entity is None:
|
|
388
|
+
return issues_found
|
|
389
|
+
|
|
390
|
+
self.logger.info(f"Checking for orphan records in {node.entity_name}")
|
|
391
|
+
|
|
392
|
+
join_expr = " AND ".join(
|
|
393
|
+
f"{node.parent_entity}.{k} = {node.entity_name}.{v}"
|
|
394
|
+
for k, v in node.join_fields.items()
|
|
395
|
+
)
|
|
396
|
+
location = list(node.join_fields.values())[0]
|
|
397
|
+
with BackgroundMessageWriter(
|
|
398
|
+
working_directory=working_directory,
|
|
399
|
+
dve_stage=self.__stage_name__,
|
|
400
|
+
key_fields=key_fields,
|
|
401
|
+
logger=self.logger,
|
|
402
|
+
) as msg_writer:
|
|
403
|
+
_orph_records = self.identify_orphans(
|
|
404
|
+
entities=entities,
|
|
405
|
+
config=OrphanIdentification(
|
|
406
|
+
id=list(node.join_fields.values())[0],
|
|
407
|
+
entity_name=node.entity_name,
|
|
408
|
+
target_name=node.parent_entity,
|
|
409
|
+
join_condition=join_expr,
|
|
410
|
+
),
|
|
411
|
+
)
|
|
412
|
+
_messages = [
|
|
413
|
+
FeedbackMessage(
|
|
414
|
+
entity=node.entity_name,
|
|
415
|
+
record=record, # type: ignore
|
|
416
|
+
error_location=location,
|
|
417
|
+
error_message=template_object(node.missing_parent_id_error_message, record),
|
|
418
|
+
failure_type="record",
|
|
419
|
+
error_type="record",
|
|
420
|
+
error_code=node.missing_parent_id_error_code,
|
|
421
|
+
reporting_field=location,
|
|
422
|
+
category="Parent Missing",
|
|
423
|
+
)
|
|
424
|
+
for record in _orph_records
|
|
425
|
+
]
|
|
426
|
+
msg_writer.write_queue.put(_messages)
|
|
427
|
+
|
|
428
|
+
self.cache_entity(node.entity_name, entities)
|
|
429
|
+
|
|
430
|
+
_orph_count = len(_messages)
|
|
431
|
+
|
|
432
|
+
self.logger.info(
|
|
433
|
+
f"Found {_orph_count} orphan records between {node.entity_name} and {node.parent_entity}" # pylint: disable=C0301
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
return _orph_count > 0
|
|
437
|
+
|
|
438
|
+
entity_issues_found: dict[EntityName, bool] = {}
|
|
439
|
+
|
|
440
|
+
for tree in entity_hierarchy.entity_trees.values():
|
|
441
|
+
for node in tree.iterate_root_down():
|
|
442
|
+
entity_issues_found[node.entity_name] = process_node(node)
|
|
443
|
+
|
|
444
|
+
return [], entity_issues_found
|
|
445
|
+
|
|
446
|
+
def identify_and_remove_missing_mandatory_groups(
|
|
447
|
+
self,
|
|
448
|
+
working_directory: URI,
|
|
449
|
+
entities: Entities,
|
|
450
|
+
entity_hierarchy: EntityHierarchy,
|
|
451
|
+
key_fields: Optional[dict[str, list[str]]] = None,
|
|
452
|
+
) -> tuple[Messages, dict[EntityName, bool]]:
|
|
453
|
+
"""
|
|
454
|
+
Identify that an entity with a mandatory key has at least one valid child record.
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
def process_node(node: HierarchyNode) -> bool:
|
|
458
|
+
"""Identify at least one valid child for a mandatory entity at a given node."""
|
|
459
|
+
if node.parent_entity is None or not node.mandatory:
|
|
460
|
+
return False
|
|
461
|
+
|
|
462
|
+
self.logger.info(
|
|
463
|
+
f"Identifying that mandatory entity `{node.parent_entity}` has at least 1 valid child record in {node.entity_name}" # pylint: disable=C0301
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
join_expr = " AND ".join(
|
|
467
|
+
f"{node.parent_entity}.{k} = {node.entity_name}.{v}"
|
|
468
|
+
for k, v in node.join_fields.items()
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
with BackgroundMessageWriter(
|
|
472
|
+
working_directory=working_directory,
|
|
473
|
+
dve_stage=self.__stage_name__,
|
|
474
|
+
key_fields=key_fields,
|
|
475
|
+
logger=self.logger,
|
|
476
|
+
) as msg_writer:
|
|
477
|
+
location = next(iter(node.join_fields.values()))
|
|
478
|
+
missing_children_records = self.check_mandatory_group(
|
|
479
|
+
entities=entities,
|
|
480
|
+
config=GroupIdentification(
|
|
481
|
+
entity_name=node.parent_entity,
|
|
482
|
+
target_name=node.entity_name,
|
|
483
|
+
join_condition=join_expr,
|
|
484
|
+
),
|
|
485
|
+
)
|
|
486
|
+
_messages = [
|
|
487
|
+
FeedbackMessage(
|
|
488
|
+
entity=node.parent_entity,
|
|
489
|
+
record=record, # type: ignore
|
|
490
|
+
error_location=location,
|
|
491
|
+
error_message=template_object(node.no_valid_records_error_message, record),
|
|
492
|
+
failure_type="record",
|
|
493
|
+
error_type="record",
|
|
494
|
+
error_code=node.no_valid_records_error_code,
|
|
495
|
+
reporting_field=location,
|
|
496
|
+
category="Children missing",
|
|
497
|
+
)
|
|
498
|
+
for record in missing_children_records
|
|
499
|
+
]
|
|
500
|
+
msg_writer.write_queue.put(_messages)
|
|
501
|
+
self.cache_entity(node.parent_entity, entities)
|
|
502
|
+
|
|
503
|
+
_no_valid_child_records: int = len(_messages)
|
|
504
|
+
|
|
505
|
+
self.logger.info(
|
|
506
|
+
f"Found {_no_valid_child_records} records with no valid children in {node.parent_entity}." # pylint: disable=C0301
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
return _no_valid_child_records > 0
|
|
510
|
+
|
|
511
|
+
entity_issues_found: dict[EntityName, bool] = {}
|
|
512
|
+
|
|
513
|
+
for tree in entity_hierarchy.entity_trees.values():
|
|
514
|
+
for node in tree.iterate_lowest_descendent_up():
|
|
515
|
+
if node.parent_entity and node.mandatory:
|
|
516
|
+
result = process_node(node)
|
|
517
|
+
if result:
|
|
518
|
+
entity_issues_found[node.parent_entity] = result
|
|
519
|
+
|
|
520
|
+
return [], entity_issues_found
|
|
521
|
+
|
|
355
522
|
# pylint: disable=R0912,R0914
|
|
356
523
|
def apply_sync_filters(
|
|
357
524
|
self,
|
|
@@ -428,6 +595,7 @@ class BaseStepImplementations(Generic[EntityType], ABC): # pylint: disable=too-
|
|
|
428
595
|
excluded_columns=filter_column_names,
|
|
429
596
|
reporting=rule.reporting,
|
|
430
597
|
parent=rule.parent,
|
|
598
|
+
error_on_null=True,
|
|
431
599
|
),
|
|
432
600
|
)
|
|
433
601
|
if not success:
|
|
@@ -459,6 +627,7 @@ class BaseStepImplementations(Generic[EntityType], ABC): # pylint: disable=too-
|
|
|
459
627
|
expression=f"NOT ({rule.expression})",
|
|
460
628
|
reporting=rule.reporting,
|
|
461
629
|
parent=rule.parent,
|
|
630
|
+
error_on_null=True,
|
|
462
631
|
),
|
|
463
632
|
)
|
|
464
633
|
if not success:
|
|
@@ -691,3 +860,25 @@ class BaseStepImplementations(Generic[EntityType], ABC): # pylint: disable=too-
|
|
|
691
860
|
):
|
|
692
861
|
"""Method to filter out record rejection errors from the data contract for a given entity"""
|
|
693
862
|
raise NotImplementedError()
|
|
863
|
+
|
|
864
|
+
@staticmethod
|
|
865
|
+
def get_entity_count(entity: EntityType) -> int:
|
|
866
|
+
"""Method to get count of records in entity"""
|
|
867
|
+
raise NotImplementedError()
|
|
868
|
+
|
|
869
|
+
def cache_entity(self, entity_name: EntityName, entities: Entities):
|
|
870
|
+
"""Store the materialised query in memory and update entity to query directly.
|
|
871
|
+
If the entity is already cached, the new cache should be created first, then the old one
|
|
872
|
+
removed as part of the function (in case the newer cache depends on the older one)."""
|
|
873
|
+
raise NotImplementedError()
|
|
874
|
+
|
|
875
|
+
def _remove_cached_artifact(self, entity_name: EntityName):
|
|
876
|
+
"""Delete artifact in memory and clear from the entity cache keeping track.
|
|
877
|
+
This should not be used directly as removing artifacts from memory, may lead to some
|
|
878
|
+
entities being unable to be processed as their execution plans depend on these artifacts."""
|
|
879
|
+
raise NotImplementedError()
|
|
880
|
+
|
|
881
|
+
def clear_entity_cache(self):
|
|
882
|
+
"""Helper method to remove all artifacts and cache trackers at end of processing."""
|
|
883
|
+
for entity_name in list(self.entity_cache_tracker):
|
|
884
|
+
self._remove_cached_artifact(entity_name)
|
|
@@ -33,27 +33,39 @@ class MessageBearingError(BackendError):
|
|
|
33
33
|
"""The messages to be returned as part of the error."""
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
class
|
|
36
|
+
class CriticalMessageBearingError(BackendError):
|
|
37
|
+
"""
|
|
38
|
+
A backend error that comes with a pre-created message.
|
|
39
|
+
The intention of this exception vs MessageBearingError is that
|
|
40
|
+
this should be used to halt the processing of a submission.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, *args: object, message: FeedbackMessage) -> None:
|
|
44
|
+
super().__init__(*args)
|
|
45
|
+
self.message = message
|
|
46
|
+
"""The message to be returned as part of the error."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class UnableToParseCSVError(CriticalMessageBearingError):
|
|
37
50
|
"""An error raised when unable to parse a CSV file"""
|
|
38
51
|
|
|
39
52
|
def __init__(
|
|
40
|
-
self,
|
|
53
|
+
self,
|
|
54
|
+
entity_name: Optional[str],
|
|
55
|
+
error_message: Optional[str],
|
|
56
|
+
error_code: Optional[str],
|
|
41
57
|
):
|
|
42
58
|
super().__init__(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
error_message=field_check_error_message,
|
|
54
|
-
error_code=field_check_error_code,
|
|
55
|
-
)
|
|
56
|
-
]
|
|
59
|
+
message=FeedbackMessage(
|
|
60
|
+
entity=entity_name,
|
|
61
|
+
record=None,
|
|
62
|
+
failure_type="submission",
|
|
63
|
+
is_informational=False,
|
|
64
|
+
error_type="csv read",
|
|
65
|
+
error_message=error_message
|
|
66
|
+
or "Unable to parse the CSV file. Please check the structure of your CSV.", # pylint: disable=C0301
|
|
67
|
+
error_code=error_code or "MalformedCSV",
|
|
68
|
+
)
|
|
57
69
|
)
|
|
58
70
|
|
|
59
71
|
|