data-validation-engine 0.8.3__tar.gz → 0.9.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.8.3 → data_validation_engine-0.9.0}/PKG-INFO +17 -39
- data_validation_engine-0.9.0/README.md +59 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/pyproject.toml +8 -7
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/auditing.py +8 -8
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/auditing.py +8 -8
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/contract.py +7 -10
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/duckdb_helpers.py +35 -22
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/readers/csv.py +4 -4
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/readers/json.py +2 -2
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/readers/xml.py +2 -2
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/auditing.py +1 -1
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/backend.py +1 -1
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/contract.py +7 -7
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/spark_helpers.py +53 -44
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/utilities.py +1 -1
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/metadata/contract.py +2 -2
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/metadata/reporting.py +9 -7
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/metadata/rules.py +22 -30
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/readers/csv.py +3 -3
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/readers/utilities.py +3 -2
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/readers/xml.py +22 -8
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/utilities.py +38 -6
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/configuration/v1/__init__.py +6 -6
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/configuration/v1/steps.py +8 -9
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/engine.py +47 -34
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/loggers.py +1 -1
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/message.py +12 -14
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/models.py +32 -18
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/validation.py +4 -5
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/metadata_parser/domain_types.py +143 -93
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/metadata_parser/function_library.py +2 -2
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/metadata_parser/function_wrapper.py +17 -24
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/metadata_parser/model_generator.py +7 -9
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/metadata_parser/models.py +51 -45
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/metadata_parser/utilities.py +32 -1
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/implementations/file.py +4 -4
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/pipeline/pipeline.py +3 -3
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/pipeline/utils.py +3 -3
- data_validation_engine-0.8.3/README.md +0 -81
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/LICENSE +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/common/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/common/error_utils.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/backend.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/contract.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/core.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/reader.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/reference_data.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/rules.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/utilities.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/exceptions.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/readers/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/reference_data.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/rules.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/types.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/utilities.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/readers/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/readers/csv.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/readers/json.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/readers/xml.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/reference_data.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/rules.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/types.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/metadata/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/readers/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/readers/xml_linting.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/types.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/configuration/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/configuration/base.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/configuration/v1/filters.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/configuration/v1/rule_stores/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/configuration/v1/rule_stores/models.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/constants.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/exceptions.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/functions/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/functions/implementations.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/templating.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/core_engine/type_hints.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/metadata_parser/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/metadata_parser/exc.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/parser/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/parser/exceptions.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/helpers.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/implementations/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/implementations/base.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/implementations/dbfs.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/implementations/s3.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/log_handler.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/service.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/utilities.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/parser/type_hints.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/parser/utilities.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/pipeline/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/pipeline/duckdb_pipeline.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/pipeline/foundry_ddb_pipeline.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/pipeline/spark_pipeline.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/reporting/__init__.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/reporting/constants.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/reporting/error_report.py +0 -0
- {data_validation_engine-0.8.3 → data_validation_engine-0.9.0}/src/dve/reporting/excel_report.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: data-validation-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.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
|
|
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Classifier: Operating System ::
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
15
|
Classifier: Topic :: Software Development :: Libraries
|
|
16
16
|
Classifier: Typing :: Typed
|
|
17
17
|
Requires-Dist: Jinja2 (==3.1.6)
|
|
@@ -25,7 +25,7 @@ Requires-Dist: openpyxl (==3.1.5)
|
|
|
25
25
|
Requires-Dist: pandas (==2.3.3)
|
|
26
26
|
Requires-Dist: polars (==0.20.31)
|
|
27
27
|
Requires-Dist: pyarrow (==23.0.1)
|
|
28
|
-
Requires-Dist: pydantic (==
|
|
28
|
+
Requires-Dist: pydantic (==2.13.4)
|
|
29
29
|
Requires-Dist: pyspark (>=3.5.0,<=3.5.5)
|
|
30
30
|
Requires-Dist: typing_extensions (==4.15.0)
|
|
31
31
|
Requires-Dist: urllib3 (==2.7.0)
|
|
@@ -37,7 +37,7 @@ Project-URL: Repository, https://github.com/NHSDigital/data-validation-engine.gi
|
|
|
37
37
|
Description-Content-Type: text/markdown
|
|
38
38
|
|
|
39
39
|
<h1 style="display: flex; align-items: center; gap: 10px;">
|
|
40
|
-
<img src="https://
|
|
40
|
+
<img src="https://raw.githubusercontent.com/NHSDigital/data-validation-engine/616b55890306db4546177f7effac48ca241857ec/overrides/.icons/nhseng.svg" alt="" width="5%" height="100%" align="left">
|
|
41
41
|
Data Validation Engine
|
|
42
42
|
</h1>
|
|
43
43
|
|
|
@@ -47,49 +47,27 @@ Description-Content-Type: text/markdown
|
|
|
47
47
|
[](https://github.com/NHSDigital/data-validation-engine/actions/workflows/ci_testing.yml)
|
|
48
48
|
[](https://github.com/NHSDigital/data-validation-engine/actions/workflows/ci_linting.yml)
|
|
49
49
|
|
|
50
|
-
The Data Validation Engine (DVE) is a configuration
|
|
50
|
+
The Data Validation Engine (DVE) is a configuration-driven data validation library created and used by NHS England. It lets users define validation rules once and apply them across multiple dataset collections - supporting consistent, accurate data checks.
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
__The DVE offers__:
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
- SQL configuration-based validations
|
|
55
|
+
- Format normalization to Parquet for a unified data representation
|
|
56
|
+
- Data modelling and typecasting
|
|
57
|
+
- Business-rule validations executed on supported backends such as Spark and DuckDB, with the option to add custom backends
|
|
58
|
+
- Deriving new fields and entities
|
|
59
|
+
- Clear validation reporting, including summary insights and record-level error messages
|
|
55
60
|
|
|
56
|
-
|
|
57
|
-
| -- | ------- | ------- |
|
|
58
|
-
| 1. | File Transformation | This service will take submitted files and turn them into stringified parquet file(s) to ensure that a consistent data structure can be passed through the other services. |
|
|
59
|
-
| 2. | Data Contract | This service will validate and perform type casting against a stringified parquet file using [pydantic models](https://docs.pydantic.dev/1.10/). |
|
|
60
|
-
| 3. | Business Rules | The business rules service will perform more complex validations such as comparisons between fields and tables, aggregations, filters etc to generate new entities. |
|
|
61
|
-
| 4. | Error Reports | The error reports service will take all the errors raised in previous services and surface them into a readable format for a downstream users/service. Currently, this implemented to be an excel spreadsheet but could be reconfigured to meet other requirements/use cases. |
|
|
62
|
-
|
|
63
|
-
If you'd like more detailed documentation around these services the please read the extended documentation [here](https://nhsdigital.github.io/data-validation-engine/).
|
|
64
|
-
|
|
65
|
-
The DVE has been designed in a way that's modular and can support users who just want to utilise specific "services" from the DVE (i.e. just the file transformation + data contract). Additionally, the DVE is designed to support different backend implementations. As part of the base installation of DVE, you will find backend support for `Spark` and `DuckDB`. So, if you need a `MySQL` backend implementation, you can implement this yourself. Given our organisations requirements, it will be unlikely that we add anymore specific backend implementations into the base package beyond Spark and DuckDB. So, if you are unable to implement this yourself, I would recommend reading the guidance on [requesting new features and raising bug reports here](#requesting-new-features-and-raising-bug-reports).
|
|
66
|
-
|
|
67
|
-
Additionally, if you'd like to contribute a new backend implementation into the base DVE package, then please look at the [Contributing](#Contributing) section.
|
|
61
|
+
As mentioned above, the DVE is "configuration driven" which means the majority of development for you as a user will be building a JSON document to describe how the data will be validated. The JSON document is known as a `dischema` (data ingest schema) file and example files can be accessed [here ↗️](https://github.com/NHSDigital/data-validation-engine/tree/main/tests/testdata). If you'd like to learn more about JSON document and how to build one from scratch, then please read the documentation [here ↗️](https://nhsdigital.github.io/data-validation-engine/).
|
|
68
62
|
|
|
69
63
|
## Installation and usage
|
|
70
64
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
If you're planning to use the Spark backend implementation, you will also need OpenJDK 11 installed.
|
|
74
|
-
|
|
75
|
-
Python dependencies are listed in `pyproject.toml`.
|
|
76
|
-
|
|
77
|
-
To install the DVE package you can simply install using a package manager such as [pip](https://pypi.org/project/pip/).
|
|
78
|
-
|
|
79
|
-
```
|
|
80
|
-
pip install data-validation-engine
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
*Note - Only versions >=0.6.2 are available on PyPi. For older versions please install directly from the git repo or build from source.*
|
|
84
|
-
|
|
85
|
-
Once you have installed the DVE you are ready to use it. For guidance on how to create your dischema JSON document (configuration), please read the [documentation](https://nhsdigital.github.io/data-validation-engine/).
|
|
86
|
-
|
|
87
|
-
Version 0.0.1 does support a working Python 3.7 installation. However, we will not be supporting any issues with that version of the DVE if you choose to use it. __Use at your own risk__.
|
|
65
|
+
Please see the documentation [here ↗️](https://nhsdigital.github.io/data-validation-engine/user_guidance/install/).
|
|
88
66
|
|
|
89
67
|
## Requesting new features and raising bug reports
|
|
90
68
|
**Before creating new issues, please check to see if the same bug/feature has been created already. Where a duplicate is created, the ticket will be closed and referenced to an existing issue.**
|
|
91
69
|
|
|
92
|
-
If you have spotted a bug with the DVE then please raise an issue [here](https://github.com/nhsengland/Data-Validation-Engine/issues) using the "bug template".
|
|
70
|
+
If you have spotted a bug with the DVE then please raise an issue [here ↗️](https://github.com/nhsengland/Data-Validation-Engine/issues) using the "bug template".
|
|
93
71
|
|
|
94
72
|
If you have feature request then please follow the same process whilst using the "Feature request template".
|
|
95
73
|
|
|
@@ -101,7 +79,7 @@ Below is a list of features that we would like to implement or have been request
|
|
|
101
79
|
| Uplift to Python 3.11 | 0.2.0 | Yes |
|
|
102
80
|
| Uplift Pyspark to 3.5 | 0.8.0 | Yes |
|
|
103
81
|
| Allow DVE to run on Python 3.12+ | 0.8.0 | Yes |
|
|
104
|
-
| Upgrade to Pydantic 2.0 | 0.9.0 |
|
|
82
|
+
| Upgrade to Pydantic 2.0 | 0.9.0 | Yes |
|
|
105
83
|
| Uplift Pyspark to 4.0+ | TBA | No |
|
|
106
84
|
| Polars upgrade to v1+ | TBA | No |
|
|
107
85
|
| DuckDB upgrade to v1.5+ | TBA | No |
|
|
@@ -111,7 +89,7 @@ Below is a list of features that we would like to implement or have been request
|
|
|
111
89
|
If you are interested in getting any of the unreleased features listed above available, then please read the [Contributing](#Contributing) section and then submit us a pull request.
|
|
112
90
|
|
|
113
91
|
## Contributing
|
|
114
|
-
Please see guidance [here](https://github.com/NHSDigital/data-validation-engine/blob/main/CONTRIBUTE.md).
|
|
92
|
+
Please see guidance [here ↗️](https://github.com/NHSDigital/data-validation-engine/blob/main/CONTRIBUTE.md).
|
|
115
93
|
|
|
116
94
|
## Legal
|
|
117
95
|
This codebase is released under the MIT License. This covers both the codebase and any sample code in the documentation.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<h1 style="display: flex; align-items: center; gap: 10px;">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/NHSDigital/data-validation-engine/616b55890306db4546177f7effac48ca241857ec/overrides/.icons/nhseng.svg" alt="" width="5%" height="100%" align="left">
|
|
3
|
+
Data Validation Engine
|
|
4
|
+
</h1>
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
[](https://github.com/NHSDigital/data-validation-engine/actions/workflows/ci_testing.yml)
|
|
10
|
+
[](https://github.com/NHSDigital/data-validation-engine/actions/workflows/ci_linting.yml)
|
|
11
|
+
|
|
12
|
+
The Data Validation Engine (DVE) is a configuration-driven data validation library created and used by NHS England. It lets users define validation rules once and apply them across multiple dataset collections - supporting consistent, accurate data checks.
|
|
13
|
+
|
|
14
|
+
__The DVE offers__:
|
|
15
|
+
|
|
16
|
+
- SQL configuration-based validations
|
|
17
|
+
- Format normalization to Parquet for a unified data representation
|
|
18
|
+
- Data modelling and typecasting
|
|
19
|
+
- Business-rule validations executed on supported backends such as Spark and DuckDB, with the option to add custom backends
|
|
20
|
+
- Deriving new fields and entities
|
|
21
|
+
- Clear validation reporting, including summary insights and record-level error messages
|
|
22
|
+
|
|
23
|
+
As mentioned above, the DVE is "configuration driven" which means the majority of development for you as a user will be building a JSON document to describe how the data will be validated. The JSON document is known as a `dischema` (data ingest schema) file and example files can be accessed [here ↗️](https://github.com/NHSDigital/data-validation-engine/tree/main/tests/testdata). If you'd like to learn more about JSON document and how to build one from scratch, then please read the documentation [here ↗️](https://nhsdigital.github.io/data-validation-engine/).
|
|
24
|
+
|
|
25
|
+
## Installation and usage
|
|
26
|
+
|
|
27
|
+
Please see the documentation [here ↗️](https://nhsdigital.github.io/data-validation-engine/user_guidance/install/).
|
|
28
|
+
|
|
29
|
+
## Requesting new features and raising bug reports
|
|
30
|
+
**Before creating new issues, please check to see if the same bug/feature has been created already. Where a duplicate is created, the ticket will be closed and referenced to an existing issue.**
|
|
31
|
+
|
|
32
|
+
If you have spotted a bug with the DVE then please raise an issue [here ↗️](https://github.com/nhsengland/Data-Validation-Engine/issues) using the "bug template".
|
|
33
|
+
|
|
34
|
+
If you have feature request then please follow the same process whilst using the "Feature request template".
|
|
35
|
+
|
|
36
|
+
## Upcoming features
|
|
37
|
+
Below is a list of features that we would like to implement or have been requested.
|
|
38
|
+
| Feature | Release Version | Released? |
|
|
39
|
+
| ------------------------------------------------------------------------------- | ----------------- | --------- |
|
|
40
|
+
| Open source release | 0.1.0 | Yes |
|
|
41
|
+
| Uplift to Python 3.11 | 0.2.0 | Yes |
|
|
42
|
+
| Uplift Pyspark to 3.5 | 0.8.0 | Yes |
|
|
43
|
+
| Allow DVE to run on Python 3.12+ | 0.8.0 | Yes |
|
|
44
|
+
| Upgrade to Pydantic 2.0 | 0.9.0 | Yes |
|
|
45
|
+
| Uplift Pyspark to 4.0+ | TBA | No |
|
|
46
|
+
| Polars upgrade to v1+ | TBA | No |
|
|
47
|
+
| DuckDB upgrade to v1.5+ | TBA | No |
|
|
48
|
+
| Python 3.13 & 3.14 upgrade | TBA | No |
|
|
49
|
+
| Create a more user friendly interface for building and modifying dischema files | TBA | No |
|
|
50
|
+
|
|
51
|
+
If you are interested in getting any of the unreleased features listed above available, then please read the [Contributing](#Contributing) section and then submit us a pull request.
|
|
52
|
+
|
|
53
|
+
## Contributing
|
|
54
|
+
Please see guidance [here ↗️](https://github.com/NHSDigital/data-validation-engine/blob/main/CONTRIBUTE.md).
|
|
55
|
+
|
|
56
|
+
## Legal
|
|
57
|
+
This codebase is released under the MIT License. This covers both the codebase and any sample code in the documentation.
|
|
58
|
+
|
|
59
|
+
Any HTML or Markdown documentation is [© Crown copyright](https://www.nationalarchives.gov.uk/information-management/re-using-public-sector-information/uk-government-licensing-framework/crown-copyright/) and available under the terms of the [Open Government 3.0 licence](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).
|
|
@@ -11,7 +11,7 @@ classifiers = [
|
|
|
11
11
|
"Programming Language :: Python :: 3.10",
|
|
12
12
|
"Programming Language :: Python :: 3.11",
|
|
13
13
|
"Programming Language :: Python :: 3.12",
|
|
14
|
-
"Operating System ::
|
|
14
|
+
"Operating System :: POSIX :: Linux",
|
|
15
15
|
"Topic :: Software Development :: Libraries",
|
|
16
16
|
"Typing :: Typed",
|
|
17
17
|
]
|
|
@@ -25,7 +25,7 @@ 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.9.0"
|
|
29
29
|
packages = [
|
|
30
30
|
{ include = "dve", from = "src" },
|
|
31
31
|
]
|
|
@@ -43,7 +43,7 @@ openpyxl = "3.1.5"
|
|
|
43
43
|
pandas = "2.3.3"
|
|
44
44
|
polars = "0.20.31"
|
|
45
45
|
pyarrow = "23.0.1"
|
|
46
|
-
pydantic = "
|
|
46
|
+
pydantic = "2.13.4"
|
|
47
47
|
pyspark = ">=3.5.0,<=3.5.5"
|
|
48
48
|
typing_extensions = "4.15.0"
|
|
49
49
|
urllib3 = "2.7.0" # dependency of boto3 & botocore
|
|
@@ -69,7 +69,7 @@ optional = true
|
|
|
69
69
|
behave = "1.3.3"
|
|
70
70
|
coverage = "7.11.0"
|
|
71
71
|
moto = {extras = ["s3"], version = "4.2.14"}
|
|
72
|
-
cryptography = "
|
|
72
|
+
cryptography = "50.0.0" # dependency of `moto`
|
|
73
73
|
requests = "2.33.0" # dependency of `moto`
|
|
74
74
|
Werkzeug = "3.1.6"
|
|
75
75
|
pytest = "9.0.3"
|
|
@@ -84,7 +84,8 @@ black = "26.3.1"
|
|
|
84
84
|
astroid = "3.3.9"
|
|
85
85
|
isort = "5.13.2"
|
|
86
86
|
pylint = "3.3.9"
|
|
87
|
-
mypy = "1.
|
|
87
|
+
mypy = "1.20.2"
|
|
88
|
+
librt = "0.11.0" # mypy dependency
|
|
88
89
|
boto3-stubs = {extras = ["essential"], version = "1.26.72"}
|
|
89
90
|
botocore-stubs = "1.29.72"
|
|
90
91
|
pandas-stubs = "1.2.0.62"
|
|
@@ -106,7 +107,7 @@ click = "8.2.1"
|
|
|
106
107
|
mkdocs = "1.6.1"
|
|
107
108
|
mkdocstrings = { version = "1.0.3", extras = ["python"] }
|
|
108
109
|
griffelib = "2.0.1"
|
|
109
|
-
pymdown-extensions = "
|
|
110
|
+
pymdown-extensions = "11.0.1"
|
|
110
111
|
zensical = "0.0.46"
|
|
111
112
|
|
|
112
113
|
[tool.ruff]
|
|
@@ -291,7 +292,7 @@ redefining-builtins-modules = [
|
|
|
291
292
|
|
|
292
293
|
[tool.commitizen]
|
|
293
294
|
name = "cz_conventional_commits"
|
|
294
|
-
tag_format = "v$major.$minor.$patch"
|
|
295
|
+
tag_format = "v$major.$minor.${patch}$prerelease"
|
|
295
296
|
version_scheme = "pep440"
|
|
296
297
|
version_provider = "poetry"
|
|
297
298
|
update_changelog_on_bump = true
|
|
@@ -15,7 +15,7 @@ from queue import Queue as ThreadQueue
|
|
|
15
15
|
from types import TracebackType
|
|
16
16
|
from typing import Any, ClassVar, Generic, Optional, TypeVar, Union
|
|
17
17
|
|
|
18
|
-
from pydantic import ValidationError,
|
|
18
|
+
from pydantic import ValidationError, validate_call
|
|
19
19
|
from typing_extensions import Literal, get_origin
|
|
20
20
|
|
|
21
21
|
from dve.core_engine.models import (
|
|
@@ -99,8 +99,8 @@ class BaseAuditor(Generic[AuditReturnType]):
|
|
|
99
99
|
def schema(self) -> dict[str, type]:
|
|
100
100
|
"""Determine python schema of auditor"""
|
|
101
101
|
return {
|
|
102
|
-
fld: str if get_origin(mdl.
|
|
103
|
-
for fld, mdl in self._record_type.
|
|
102
|
+
fld: str if get_origin(mdl.annotation) == Literal else mdl.annotation # type: ignore
|
|
103
|
+
for fld, mdl in self._record_type.model_fields.items()
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
@staticmethod
|
|
@@ -196,7 +196,7 @@ class BaseAuditingManager(
|
|
|
196
196
|
"""Convert AuditReturnType to iterable of dictionaries"""
|
|
197
197
|
raise NotImplementedError()
|
|
198
198
|
|
|
199
|
-
@
|
|
199
|
+
@validate_call
|
|
200
200
|
def add_processing_records(self, processing_records: list[ProcessingStatusRecord]):
|
|
201
201
|
"""Add an entry to the processing_status auditor."""
|
|
202
202
|
if self.pool:
|
|
@@ -208,7 +208,7 @@ class BaseAuditingManager(
|
|
|
208
208
|
records=[dict(rec) for rec in processing_records]
|
|
209
209
|
)
|
|
210
210
|
|
|
211
|
-
@
|
|
211
|
+
@validate_call
|
|
212
212
|
def add_submission_statistics_records(self, sub_stats: list[SubmissionStatisticsRecord]):
|
|
213
213
|
"""Add an entry to the submission statistics auditor."""
|
|
214
214
|
if self.pool:
|
|
@@ -218,7 +218,7 @@ class BaseAuditingManager(
|
|
|
218
218
|
)
|
|
219
219
|
return self._submission_statistics.add_records(records=[dict(rec) for rec in sub_stats])
|
|
220
220
|
|
|
221
|
-
@
|
|
221
|
+
@validate_call
|
|
222
222
|
def add_transfer_records(self, transfer_records: list[TransferRecord]):
|
|
223
223
|
"""Add an entry to the transfers auditor"""
|
|
224
224
|
if self.pool:
|
|
@@ -227,7 +227,7 @@ class BaseAuditingManager(
|
|
|
227
227
|
)
|
|
228
228
|
return self._transfers.add_records(records=[dict(rec) for rec in transfer_records])
|
|
229
229
|
|
|
230
|
-
@
|
|
230
|
+
@validate_call
|
|
231
231
|
def add_new_submissions(
|
|
232
232
|
self,
|
|
233
233
|
submissions: list[SubmissionMetadata],
|
|
@@ -250,7 +250,7 @@ class BaseAuditingManager(
|
|
|
250
250
|
processing_status="received",
|
|
251
251
|
job_run_id=job_run_id,
|
|
252
252
|
**ts_info,
|
|
253
|
-
).
|
|
253
|
+
).model_dump(),
|
|
254
254
|
}
|
|
255
255
|
processing_status_recs.append(processing_rec)
|
|
256
256
|
if sub_info:
|
|
@@ -14,10 +14,10 @@ from dve.core_engine.backends.base.auditing import (
|
|
|
14
14
|
OrderCriteria,
|
|
15
15
|
)
|
|
16
16
|
from dve.core_engine.backends.implementations.duckdb.duckdb_helpers import (
|
|
17
|
-
|
|
17
|
+
get_duckdb_type_from_annotation,
|
|
18
18
|
table_exists,
|
|
19
19
|
)
|
|
20
|
-
from dve.core_engine.backends.utilities import
|
|
20
|
+
from dve.core_engine.backends.utilities import get_polars_type_from_annotation
|
|
21
21
|
from dve.core_engine.models import (
|
|
22
22
|
AuditRecord,
|
|
23
23
|
ProcessingStatusRecord,
|
|
@@ -62,7 +62,10 @@ class DDBAuditor(BaseAuditor[DuckDBPyRelation]):
|
|
|
62
62
|
"""Generate create table sql script for auditor"""
|
|
63
63
|
_sql_expression = f"CREATE TABLE {self._name} ("
|
|
64
64
|
_sql_expression += ", ".join(
|
|
65
|
-
[
|
|
65
|
+
[
|
|
66
|
+
f"{fld} {get_duckdb_type_from_annotation(dtype)}"
|
|
67
|
+
for fld, dtype in self.schema.items()
|
|
68
|
+
]
|
|
66
69
|
)
|
|
67
70
|
_sql_expression += ")"
|
|
68
71
|
return _sql_expression
|
|
@@ -70,10 +73,7 @@ class DDBAuditor(BaseAuditor[DuckDBPyRelation]):
|
|
|
70
73
|
@property
|
|
71
74
|
def polars_schema(self) -> dict[str, PolarsType]:
|
|
72
75
|
"""Get polars dataframe schema for auditor"""
|
|
73
|
-
return {
|
|
74
|
-
fld: PYTHON_TYPE_TO_POLARS_TYPE.get(dtype, pl.Utf8) # type: ignore
|
|
75
|
-
for fld, dtype in self.schema.items()
|
|
76
|
-
}
|
|
76
|
+
return {fld: get_polars_type_from_annotation(dtype) for fld, dtype in self.schema.items()}
|
|
77
77
|
|
|
78
78
|
def get_relation(self) -> DuckDBPyRelation:
|
|
79
79
|
"""Get a relation to interact with the auditor duckdb table"""
|
|
@@ -106,7 +106,7 @@ class DDBAuditor(BaseAuditor[DuckDBPyRelation]):
|
|
|
106
106
|
"""Convert a list of audit records to a relation"""
|
|
107
107
|
# pylint: disable=W0612
|
|
108
108
|
rec_df = pl.DataFrame( # type: ignore
|
|
109
|
-
[rec.
|
|
109
|
+
[rec.model_dump() for rec in recs],
|
|
110
110
|
schema=self.polars_schema,
|
|
111
111
|
)
|
|
112
112
|
return self._connection.sql("select * from rec_df")
|
|
@@ -14,7 +14,6 @@ from duckdb import DuckDBPyConnection, DuckDBPyRelation
|
|
|
14
14
|
from duckdb.typing import DuckDBPyType
|
|
15
15
|
from polars.datatypes.classes import DataTypeClass as PolarsType
|
|
16
16
|
from pydantic import BaseModel
|
|
17
|
-
from pydantic.fields import ModelField
|
|
18
17
|
|
|
19
18
|
import dve.parser.file_handling as fh
|
|
20
19
|
from dve.common.error_utils import (
|
|
@@ -96,8 +95,8 @@ class DuckDBDataContract(BaseDataContract[DuckDBPyRelation]):
|
|
|
96
95
|
) -> DuckDBPyRelation:
|
|
97
96
|
"""Create DuckDB Relation from iterator of records"""
|
|
98
97
|
polars_schema: dict[str, PolarsType] = {
|
|
99
|
-
|
|
100
|
-
for fld in stringify_model(schema).
|
|
98
|
+
name: get_polars_type_from_annotation(fld.annotation)
|
|
99
|
+
for name, fld in stringify_model(schema).model_fields.items()
|
|
101
100
|
}
|
|
102
101
|
_lazy_df = pl.LazyFrame(records, polars_schema) # type: ignore # pylint: disable=unused-variable
|
|
103
102
|
return self._connection.sql("select * from _lazy_df")
|
|
@@ -130,17 +129,15 @@ class DuckDBDataContract(BaseDataContract[DuckDBPyRelation]):
|
|
|
130
129
|
) as msg_writer:
|
|
131
130
|
for entity_name, relation in entities.items():
|
|
132
131
|
# get dtypes for all fields -> python data types or use with relation
|
|
133
|
-
entity_fields
|
|
134
|
-
entity_name
|
|
135
|
-
].__fields__
|
|
132
|
+
entity_fields = contract_metadata.schemas[entity_name].model_fields
|
|
136
133
|
ddb_schema: dict[str, DuckDBPyType] = {
|
|
137
|
-
|
|
138
|
-
for fld in entity_fields.
|
|
134
|
+
name: get_duckdb_type_from_annotation(fld.annotation)
|
|
135
|
+
for name, fld in entity_fields.items()
|
|
139
136
|
}
|
|
140
137
|
ddb_schema[RECORD_INDEX_COLUMN_NAME] = get_duckdb_type_from_annotation(int)
|
|
141
138
|
polars_schema: dict[str, PolarsType] = {
|
|
142
|
-
|
|
143
|
-
for fld in entity_fields.
|
|
139
|
+
name: get_polars_type_from_annotation(fld.annotation)
|
|
140
|
+
for name, fld in entity_fields.items()
|
|
144
141
|
}
|
|
145
142
|
polars_schema[RECORD_INDEX_COLUMN_NAME] = get_polars_type_from_annotation(int)
|
|
146
143
|
if relation_is_empty(relation):
|
|
@@ -8,7 +8,7 @@ from dataclasses import is_dataclass
|
|
|
8
8
|
from datetime import date, datetime, time
|
|
9
9
|
from decimal import Decimal
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any, ClassVar, Union
|
|
11
|
+
from typing import Any, ClassVar, Literal, Union
|
|
12
12
|
from urllib.parse import urlparse
|
|
13
13
|
|
|
14
14
|
import duckdb.typing as ddbtyp
|
|
@@ -24,6 +24,7 @@ from dve.core_engine.backends.base.utilities import _get_non_heterogenous_type
|
|
|
24
24
|
from dve.core_engine.backends.utilities import DEFAULT_ISO_FORMATS, datetime_format_to_regex
|
|
25
25
|
from dve.core_engine.constants import RECORD_INDEX_COLUMN_NAME
|
|
26
26
|
from dve.core_engine.type_hints import URI, EntityName
|
|
27
|
+
from dve.metadata_parser.utilities import resilient_get
|
|
27
28
|
from dve.parser.file_handling.service import LocalFilesystemImplementation, _get_implementation
|
|
28
29
|
|
|
29
30
|
|
|
@@ -128,8 +129,8 @@ def get_duckdb_type_from_annotation(type_annotation: Any) -> DuckDBPyType:
|
|
|
128
129
|
'optional' wrapper and return the inner type
|
|
129
130
|
- A subclass of `typing.TypedDict` with values typed using supported types. This
|
|
130
131
|
will parse the value types as Polars types and return a duckdb STRUCT.
|
|
131
|
-
- A dataclass or `pydantic.
|
|
132
|
-
This will parse the field types as
|
|
132
|
+
- A dataclass or `pydantic.BaseModel` with values typed using supported types.
|
|
133
|
+
This will parse the field types as duckdb types and return a duckdb STRUCT.
|
|
133
134
|
- Any supported type, with a `typing_extensions.Annotated` wrapper.
|
|
134
135
|
|
|
135
136
|
Any `ClassVar` types within `TypedDict`s, dataclasses, or `pydantic` models will be
|
|
@@ -138,6 +139,14 @@ def get_duckdb_type_from_annotation(type_annotation: Any) -> DuckDBPyType:
|
|
|
138
139
|
"""
|
|
139
140
|
type_origin = get_origin(type_annotation)
|
|
140
141
|
|
|
142
|
+
if type_origin is Literal:
|
|
143
|
+
ddb_types = [get_duckdb_type_from_annotation(type(t)) for t in get_args(type_annotation)]
|
|
144
|
+
if not ddb_types or not all(t == ddb_types[0] for t in ddb_types):
|
|
145
|
+
raise ValueError(
|
|
146
|
+
f"Unable to determine a single concrete type for Literal. Got {type_annotation!r}"
|
|
147
|
+
)
|
|
148
|
+
return ddb_types[0]
|
|
149
|
+
|
|
141
150
|
# An `Optional` or `Union` type, check to ensure non-heterogenity.
|
|
142
151
|
if type_origin is Union:
|
|
143
152
|
python_type = _get_non_heterogenous_type(get_args(type_annotation))
|
|
@@ -451,23 +460,27 @@ def get_duckdb_cast_statement_from_annotation(
|
|
|
451
460
|
raise ValueError(f"dict must be `typing.TypedDict` subclass, got {type_annotation!r}")
|
|
452
461
|
|
|
453
462
|
for type_ in type_annotation.mro():
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
463
|
+
if issubclass(type_, (date, time)):
|
|
464
|
+
_date_format: str = resilient_get(
|
|
465
|
+
type_, "DATE_FORMAT", "TIME_FORMAT"
|
|
466
|
+
) or DEFAULT_ISO_FORMATS.get(
|
|
467
|
+
type_, DEFAULT_ISO_FORMATS.get(datetime)
|
|
468
|
+
) # type: ignore
|
|
469
|
+
dt_cast_statement = rf"CASE WHEN REGEXP_FULL_MATCH(TRIM({quoted_name}), '{datetime_format_to_regex(_date_format)}') THEN TRY_STRPTIME(TRIM({quoted_name}), '{_date_format}') ELSE NULL END" # pylint: disable=C0301
|
|
470
|
+
|
|
471
|
+
# datetime is subclass of date, so needs to be handled first
|
|
472
|
+
if issubclass(type_, datetime):
|
|
473
|
+
stmt = rf"TRY_CAST({dt_cast_statement} as TIMESTAMP)"
|
|
474
|
+
return stmt
|
|
475
|
+
if issubclass(type_, date):
|
|
476
|
+
stmt = rf"TRY_CAST({dt_cast_statement} as DATE)"
|
|
477
|
+
return stmt
|
|
478
|
+
if issubclass(type_, time):
|
|
479
|
+
stmt = rf"TRY_CAST({dt_cast_statement} as TIME)"
|
|
480
|
+
return stmt
|
|
481
|
+
else:
|
|
482
|
+
duck_type = get_duckdb_type_from_annotation(type_)
|
|
483
|
+
if duck_type:
|
|
484
|
+
stmt = f"TRIM({quoted_name})"
|
|
485
|
+
return _cast_as_ddb_type(stmt, type_) if parent_element else stmt
|
|
473
486
|
raise ValueError(f"No equivalent DuckDB type for {type_annotation!r}")
|
|
@@ -111,8 +111,8 @@ class DuckDBCSVReader(CSVFileReader):
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
ddb_schema: dict[str, SQLType] = {
|
|
114
|
-
|
|
115
|
-
for fld in schema.
|
|
114
|
+
name: str(get_duckdb_type_from_annotation(fld.annotation)) # type: ignore
|
|
115
|
+
for name, fld in schema.model_fields.items()
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
reader_options["columns"] = ddb_schema
|
|
@@ -166,8 +166,8 @@ class PolarsToDuckDBCSVReader(DuckDBCSVReader):
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
polars_types = {
|
|
169
|
-
|
|
170
|
-
for fld in schema.
|
|
169
|
+
name: get_polars_type_from_annotation(fld.annotation) # type: ignore
|
|
170
|
+
for name, fld in schema.model_fields.items()
|
|
171
171
|
}
|
|
172
172
|
reader_options["dtypes"] = polars_types
|
|
173
173
|
|
|
@@ -56,8 +56,8 @@ class DuckDBJSONReader(BaseFileReader):
|
|
|
56
56
|
"""Returns a relation object from the source json"""
|
|
57
57
|
|
|
58
58
|
ddb_schema: dict[str, SQLType] = {
|
|
59
|
-
|
|
60
|
-
for fld in schema.
|
|
59
|
+
name: str(get_duckdb_type_from_annotation(fld.annotation)) # type: ignore
|
|
60
|
+
for name, fld in schema.model_fields.items()
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
return self.add_record_index(
|
|
@@ -47,8 +47,8 @@ class DuckDBXMLStreamReader(XMLStreamReader):
|
|
|
47
47
|
)
|
|
48
48
|
|
|
49
49
|
polars_schema: dict[str, pl.DataType] = { # type: ignore
|
|
50
|
-
|
|
51
|
-
for fld in stringify_model(schema).
|
|
50
|
+
name: get_polars_type_from_annotation(fld.annotation)
|
|
51
|
+
for name, fld in stringify_model(schema).model_fields.items()
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
_lazy_frame = self.add_record_index(
|
|
@@ -116,7 +116,7 @@ class SparkAuditor(BaseAuditor[DataFrame]):
|
|
|
116
116
|
def conv_to_entity(self, recs: list[AuditRecord]) -> DataFrame:
|
|
117
117
|
"""Convert the dataframe to an iterable of the related audit record"""
|
|
118
118
|
return self._spark.createDataFrame( # type: ignore
|
|
119
|
-
[rec.
|
|
119
|
+
[rec.model_dump() for rec in recs], schema=self.spark_schema
|
|
120
120
|
)
|
|
121
121
|
|
|
122
122
|
def add_records(self, records: Iterable[dict[str, Any]]):
|
|
@@ -92,5 +92,5 @@ class SparkBackend(BaseBackend[DataFrame]):
|
|
|
92
92
|
|
|
93
93
|
def convert_submission_info(self, submission_info: SubmissionInfo) -> DataFrame:
|
|
94
94
|
return self.spark_session.createDataFrame( # type: ignore
|
|
95
|
-
[submission_info.
|
|
95
|
+
[submission_info.model_dump()], schema=get_type_from_annotation(type(submission_info))
|
|
96
96
|
)
|
|
@@ -7,7 +7,7 @@ from typing import Any, Optional
|
|
|
7
7
|
from uuid import uuid4
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel
|
|
10
|
-
from pydantic.fields import
|
|
10
|
+
from pydantic.fields import FieldInfo
|
|
11
11
|
from pyspark.sql import DataFrame, SparkSession
|
|
12
12
|
from pyspark.sql import functions as sf
|
|
13
13
|
from pyspark.sql.functions import col, lit
|
|
@@ -104,7 +104,9 @@ class SparkDataContract(BaseDataContract[DataFrame]):
|
|
|
104
104
|
|
|
105
105
|
successful = True
|
|
106
106
|
for entity_name, record_df in entities.items():
|
|
107
|
-
entity_fields: dict[str,
|
|
107
|
+
entity_fields: dict[str, FieldInfo] = contract_metadata.schemas[
|
|
108
|
+
entity_name
|
|
109
|
+
].model_fields
|
|
108
110
|
spark_schema = get_type_from_annotation(contract_metadata.schemas[entity_name])
|
|
109
111
|
spark_schema.add(StructField(RECORD_INDEX_COLUMN_NAME, LongType()))
|
|
110
112
|
if df_is_empty(record_df):
|
|
@@ -147,19 +149,17 @@ class SparkDataContract(BaseDataContract[DataFrame]):
|
|
|
147
149
|
self.logger.info(f"Data contract found {msg_count} issues in {entity_name}")
|
|
148
150
|
|
|
149
151
|
try:
|
|
150
|
-
# TODO: will need to revisit in pydantic v2 bump as model field no longer available
|
|
151
152
|
record_df = record_df.select(
|
|
152
153
|
*[
|
|
153
154
|
(
|
|
154
155
|
get_spark_cast_statement_from_annotation(
|
|
155
|
-
fld,
|
|
156
|
+
fld, fld_info.annotation
|
|
156
157
|
).alias(fld)
|
|
157
158
|
if fld in record_df.columns
|
|
158
159
|
else lit(None).cast(
|
|
159
|
-
get_type_from_annotation(
|
|
160
|
-
)
|
|
160
|
+
get_type_from_annotation(fld_info.annotation)).alias(fld)
|
|
161
161
|
)
|
|
162
|
-
for fld,
|
|
162
|
+
for fld, fld_info in entity_fields.items()
|
|
163
163
|
],
|
|
164
164
|
col(RECORD_INDEX_COLUMN_NAME).cast(LongType()).alias(RECORD_INDEX_COLUMN_NAME),
|
|
165
165
|
)
|