data-validation-engine 0.8.4__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.
Files changed (107) hide show
  1. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/PKG-INFO +17 -39
  2. data_validation_engine-0.9.0/README.md +59 -0
  3. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/pyproject.toml +6 -5
  4. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/auditing.py +8 -8
  5. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/auditing.py +8 -8
  6. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/contract.py +7 -10
  7. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/duckdb_helpers.py +11 -3
  8. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/readers/csv.py +4 -4
  9. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/readers/json.py +2 -2
  10. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/readers/xml.py +2 -2
  11. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/auditing.py +1 -1
  12. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/backend.py +1 -1
  13. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/contract.py +7 -7
  14. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/spark_helpers.py +28 -21
  15. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/utilities.py +1 -1
  16. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/metadata/contract.py +2 -2
  17. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/metadata/reporting.py +9 -7
  18. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/metadata/rules.py +22 -30
  19. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/readers/csv.py +3 -3
  20. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/readers/utilities.py +3 -2
  21. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/readers/xml.py +22 -8
  22. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/utilities.py +38 -6
  23. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/configuration/v1/__init__.py +6 -6
  24. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/configuration/v1/steps.py +8 -9
  25. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/engine.py +47 -34
  26. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/loggers.py +1 -1
  27. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/message.py +12 -14
  28. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/models.py +32 -18
  29. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/validation.py +4 -5
  30. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/metadata_parser/domain_types.py +143 -93
  31. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/metadata_parser/function_library.py +2 -2
  32. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/metadata_parser/function_wrapper.py +17 -24
  33. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/metadata_parser/model_generator.py +7 -9
  34. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/metadata_parser/models.py +51 -45
  35. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/metadata_parser/utilities.py +12 -1
  36. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/implementations/file.py +4 -4
  37. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/pipeline/pipeline.py +3 -3
  38. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/pipeline/utils.py +3 -3
  39. data_validation_engine-0.8.4/README.md +0 -81
  40. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/LICENSE +0 -0
  41. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/__init__.py +0 -0
  42. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/common/__init__.py +0 -0
  43. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/common/error_utils.py +0 -0
  44. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/__init__.py +0 -0
  45. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/__init__.py +0 -0
  46. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/__init__.py +0 -0
  47. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/backend.py +0 -0
  48. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/contract.py +0 -0
  49. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/core.py +0 -0
  50. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/reader.py +0 -0
  51. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/reference_data.py +0 -0
  52. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/rules.py +0 -0
  53. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/base/utilities.py +0 -0
  54. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/exceptions.py +0 -0
  55. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/__init__.py +0 -0
  56. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/__init__.py +0 -0
  57. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/readers/__init__.py +0 -0
  58. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/reference_data.py +0 -0
  59. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/rules.py +0 -0
  60. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/types.py +0 -0
  61. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/duckdb/utilities.py +0 -0
  62. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/__init__.py +0 -0
  63. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/readers/__init__.py +0 -0
  64. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/readers/csv.py +0 -0
  65. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/readers/json.py +0 -0
  66. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/readers/xml.py +0 -0
  67. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/reference_data.py +0 -0
  68. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/rules.py +0 -0
  69. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/implementations/spark/types.py +0 -0
  70. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/metadata/__init__.py +0 -0
  71. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/readers/__init__.py +0 -0
  72. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/readers/xml_linting.py +0 -0
  73. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/backends/types.py +0 -0
  74. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/configuration/__init__.py +0 -0
  75. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/configuration/base.py +0 -0
  76. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/configuration/v1/filters.py +0 -0
  77. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/configuration/v1/rule_stores/__init__.py +0 -0
  78. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/configuration/v1/rule_stores/models.py +0 -0
  79. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/constants.py +0 -0
  80. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/exceptions.py +0 -0
  81. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/functions/__init__.py +0 -0
  82. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/functions/implementations.py +0 -0
  83. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/templating.py +0 -0
  84. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/core_engine/type_hints.py +0 -0
  85. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/metadata_parser/__init__.py +0 -0
  86. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/metadata_parser/exc.py +0 -0
  87. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/parser/__init__.py +0 -0
  88. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/parser/exceptions.py +0 -0
  89. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/__init__.py +0 -0
  90. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/helpers.py +0 -0
  91. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/implementations/__init__.py +0 -0
  92. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/implementations/base.py +0 -0
  93. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/implementations/dbfs.py +0 -0
  94. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/implementations/s3.py +0 -0
  95. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/log_handler.py +0 -0
  96. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/service.py +0 -0
  97. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/parser/file_handling/utilities.py +0 -0
  98. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/parser/type_hints.py +0 -0
  99. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/parser/utilities.py +0 -0
  100. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/pipeline/__init__.py +0 -0
  101. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/pipeline/duckdb_pipeline.py +0 -0
  102. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/pipeline/foundry_ddb_pipeline.py +0 -0
  103. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/pipeline/spark_pipeline.py +0 -0
  104. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/reporting/__init__.py +0 -0
  105. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/reporting/constants.py +0 -0
  106. {data_validation_engine-0.8.4 → data_validation_engine-0.9.0}/src/dve/reporting/error_report.py +0 -0
  107. {data_validation_engine-0.8.4 → 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.8.4
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 :: OS Independent
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 (==1.10.19)
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://github.com/NHSDigital/data-validation-engine/blob/616b55890306db4546177f7effac48ca241857ec/overrides/.icons/nhseng.svg" alt="" width="5%" height="100%" align="left">
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
  [![CI Unit Tests](https://github.com/NHSDigital/data-validation-engine/actions/workflows/ci_testing.yml/badge.svg)](https://github.com/NHSDigital/data-validation-engine/actions/workflows/ci_testing.yml)
48
48
  [![CI Formatting & Linting](https://github.com/NHSDigital/data-validation-engine/actions/workflows/ci_linting.yml/badge.svg)](https://github.com/NHSDigital/data-validation-engine/actions/workflows/ci_linting.yml)
49
49
 
50
- The Data Validation Engine (DVE) is a configuration driven data validation library built and utilised by NHS England. Currently the package has been reverted from v1.0.0 release to a 0.x as we feel the package is not yet mature enough to be considered a 1.0.0 release. So please bear this in mind if reading through the commits and references to a v1+ release when on v0.x.
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
- 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` 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/).
52
+ __The DVE offers__:
53
53
 
54
- Once a dischema file has been defined, you are ready to use the DVE. The DVE is typically orchestrated based on four key "services". These are...
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
- | | Service | Purpose |
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
- The DVE is a Python package and can be installed using package managers such as [pip](https://pypi.org/project/pip/). As of the latest release we support Python 3.10 & 3.11, with Spark v3.4 and DuckDB v1.1. In the future we will be looking to upgrade the DVE to working on a higher versions of Python, DuckDB and Spark.
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 | No |
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
+ ![License](https://img.shields.io/github/license/NHSDigital/data-validation-engine)
7
+ ![PyPi](https://img.shields.io/pypi/v/data-validation-engine)
8
+ ![Conda](https://anaconda.org/nhs/data-validation-engine/badges/version.svg)
9
+ [![CI Unit Tests](https://github.com/NHSDigital/data-validation-engine/actions/workflows/ci_testing.yml/badge.svg)](https://github.com/NHSDigital/data-validation-engine/actions/workflows/ci_testing.yml)
10
+ [![CI Formatting & Linting](https://github.com/NHSDigital/data-validation-engine/actions/workflows/ci_linting.yml/badge.svg)](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 :: OS Independent",
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.8.4"
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 = "1.10.19"
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
@@ -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.11.2"
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"
@@ -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, validate_arguments
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.type_) == Literal else mdl.type_
103
- for fld, mdl in self._record_type.__fields__.items()
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
- @validate_arguments
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
- @validate_arguments
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
- @validate_arguments
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
- @validate_arguments
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
- ).dict(),
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
- PYTHON_TYPE_TO_DUCKDB_TYPE,
17
+ get_duckdb_type_from_annotation,
18
18
  table_exists,
19
19
  )
20
- from dve.core_engine.backends.utilities import PYTHON_TYPE_TO_POLARS_TYPE
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
- [f"{fld} {PYTHON_TYPE_TO_DUCKDB_TYPE.get(dtype)}" for fld, dtype in self.schema.items()]
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.dict() for rec in recs],
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
- fld.name: get_polars_type_from_annotation(fld.type_)
100
- for fld in stringify_model(schema).__fields__.values()
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: dict[str, ModelField] = contract_metadata.schemas[
134
- entity_name
135
- ].__fields__
132
+ entity_fields = contract_metadata.schemas[entity_name].model_fields
136
133
  ddb_schema: dict[str, DuckDBPyType] = {
137
- fld.name: get_duckdb_type_from_annotation(fld.annotation)
138
- for fld in entity_fields.values()
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
- fld.name: get_polars_type_from_annotation(fld.annotation)
143
- for fld in entity_fields.values()
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
@@ -129,8 +129,8 @@ def get_duckdb_type_from_annotation(type_annotation: Any) -> DuckDBPyType:
129
129
  'optional' wrapper and return the inner type
130
130
  - A subclass of `typing.TypedDict` with values typed using supported types. This
131
131
  will parse the value types as Polars types and return a duckdb STRUCT.
132
- - A dataclass or `pydantic.main.ModelMetaClass` with values typed using supported types.
133
- This will parse the field types as Polars types and return a duckdb STRUCT.
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.
134
134
  - Any supported type, with a `typing_extensions.Annotated` wrapper.
135
135
 
136
136
  Any `ClassVar` types within `TypedDict`s, dataclasses, or `pydantic` models will be
@@ -139,6 +139,14 @@ def get_duckdb_type_from_annotation(type_annotation: Any) -> DuckDBPyType:
139
139
  """
140
140
  type_origin = get_origin(type_annotation)
141
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
+
142
150
  # An `Optional` or `Union` type, check to ensure non-heterogenity.
143
151
  if type_origin is Union:
144
152
  python_type = _get_non_heterogenous_type(get_args(type_annotation))
@@ -111,8 +111,8 @@ class DuckDBCSVReader(CSVFileReader):
111
111
  }
112
112
 
113
113
  ddb_schema: dict[str, SQLType] = {
114
- fld.name: str(get_duckdb_type_from_annotation(fld.annotation)) # type: ignore
115
- for fld in schema.__fields__.values()
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
- fld.name: get_polars_type_from_annotation(fld.annotation) # type: ignore
170
- for fld in schema.__fields__.values()
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
- fld.name: str(get_duckdb_type_from_annotation(fld.annotation)) # type: ignore
60
- for fld in schema.__fields__.values()
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
- fld.name: get_polars_type_from_annotation(fld.annotation)
51
- for fld in stringify_model(schema).__fields__.values()
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.dict() for rec in recs], schema=self.spark_schema
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.dict()], schema=get_type_from_annotation(type(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 ModelField
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, ModelField] = contract_metadata.schemas[entity_name].__fields__
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, mdl_field.annotation
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(mdl_field.annotation).alias(fld)
160
- )
160
+ get_type_from_annotation(fld_info.annotation)).alias(fld)
161
161
  )
162
- for fld, mdl_field in entity_fields.items()
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
  )
@@ -13,11 +13,10 @@ from dataclasses import dataclass, is_dataclass
13
13
  from decimal import Decimal
14
14
  from functools import wraps
15
15
  from pathlib import Path
16
- from typing import Any, ClassVar, Optional, TypeVar, Union, overload
16
+ from typing import Any, ClassVar, Literal, Optional, TypeVar, Union, overload
17
17
 
18
18
  from delta.exceptions import ConcurrentAppendException, DeltaConcurrentModificationException
19
19
  from pydantic import BaseModel
20
- from pydantic.types import ConstrainedDecimal
21
20
  from pyspark.sql import DataFrame, Row, SparkSession
22
21
  from pyspark.sql import functions as sf
23
22
  from pyspark.sql import types as st
@@ -52,6 +51,7 @@ FourArgWrapped = Callable[[Column, Column, Column, Column], Column]
52
51
  """A wrapped function (Spark UDF) taking four args."""
53
52
 
54
53
 
54
+ # TODO - lets see if we can bin this off as it's a bit overkill
55
55
  @dataclass(frozen=True)
56
56
  class DecimalConfig:
57
57
  """Configuration for a Python decimal to enable it to be mapped to a
@@ -64,13 +64,13 @@ class DecimalConfig:
64
64
 
65
65
  """
66
66
 
67
- precision: int = 38
67
+ max_digits: int = 38
68
68
  """
69
69
  The precision of the decimal. This is the total number of digits in the
70
70
  decimal.
71
71
 
72
72
  """
73
- scale: int = 18
73
+ decimal_places: int = 18
74
74
  """
75
75
  The scale of the decimal. This is the number of digits to the right of the
76
76
  decimal point.
@@ -78,10 +78,12 @@ class DecimalConfig:
78
78
  """
79
79
 
80
80
  def __post_init__(self):
81
- if not 0 < self.precision <= 38:
82
- raise ValueError("Precision must be between 1 and 38 (inclusive)")
83
- if not 0 <= self.scale <= self.precision:
84
- raise ValueError("Scale must be between 0 and the precision (inclusive)")
81
+ if not 0 < self.max_digits <= 38:
82
+ raise ValueError("Max digits must be between 1 and 38 (inclusive)")
83
+ if not 0 <= self.decimal_places <= self.max_digits:
84
+ raise ValueError(
85
+ "Decimal Places must be between 0 and the specified number of digits (inclusive)"
86
+ )
85
87
 
86
88
 
87
89
  DEFAULT_DECIMAL_CONFIG = DecimalConfig()
@@ -96,7 +98,9 @@ PYTHON_TYPE_TO_SPARK_TYPE: dict[type, st.DataType] = {
96
98
  bytes: st.BinaryType(),
97
99
  dt.date: st.DateType(),
98
100
  dt.datetime: st.TimestampType(),
99
- Decimal: st.DecimalType(DEFAULT_DECIMAL_CONFIG.precision, DEFAULT_DECIMAL_CONFIG.scale),
101
+ Decimal: st.DecimalType(
102
+ DEFAULT_DECIMAL_CONFIG.max_digits, DEFAULT_DECIMAL_CONFIG.decimal_places
103
+ ),
100
104
  }
101
105
  """A mapping of Python types to the equivalent Spark types."""
102
106
 
@@ -164,7 +168,7 @@ def get_type_from_annotation(type_annotation: Any) -> st.DataType:
164
168
  'optional' wrapper and return the inner type (Spark types are all nullable).
165
169
  - A subclass of `typing.TypedDict` with values typed using supported types. This
166
170
  will parse the value types as Spark types and return a Spark `StructType`.
167
- - A dataclass or `pydantic.main.ModelMetaClass` with values typed using supported types.
171
+ - A dataclass or `pydantic.BaseModel` with values typed using supported types.
168
172
  This will parse the field types as Spark types and return a Spark `StructType`.
169
173
  - Any supported type, with a `typing_extensions.Annotated` wrapper.
170
174
  - A `decimal.Decimal` wrapped with `typing_extensions.Annotated` with a `DecimalConfig`
@@ -178,6 +182,14 @@ def get_type_from_annotation(type_annotation: Any) -> st.DataType:
178
182
  """
179
183
  type_origin = get_origin(type_annotation)
180
184
 
185
+ if type_origin is Literal:
186
+ types = [get_type_from_annotation(type(t)) for t in get_args(type_annotation)]
187
+ if not types or not all(t == types[0] for t in types):
188
+ raise ValueError(
189
+ f"Unable to determine a single concrete type for Literal. Got {type_annotation!r}"
190
+ )
191
+ return types[0]
192
+
181
193
  # An `Optional` or `Union` type, check to ensure non-heterogenity.
182
194
  if type_origin is Union:
183
195
  python_type = _get_non_heterogenous_type(get_args(type_annotation))
@@ -194,13 +206,13 @@ def get_type_from_annotation(type_annotation: Any) -> st.DataType:
194
206
  if python_type is not Decimal:
195
207
  return get_type_from_annotation(python_type)
196
208
 
197
- try: # Grab the decimal configuration from the list of other args.
198
- configuration: DecimalConfig = next(
199
- filter(lambda config: isinstance(config, DecimalConfig), other_args)
200
- )
201
- except StopIteration:
209
+ config_options = [arg for arg in other_args if hasattr(arg, "max_digits")]
210
+ if config_options:
211
+ configuration = config_options[0]
212
+ else:
202
213
  configuration = DEFAULT_DECIMAL_CONFIG
203
- return st.DecimalType(configuration.precision, configuration.scale)
214
+
215
+ return st.DecimalType(configuration.max_digits, configuration.decimal_places)
204
216
 
205
217
  # Ensure that we have a concrete type at this point.
206
218
  if not isinstance(type_annotation, type):
@@ -234,11 +246,6 @@ def get_type_from_annotation(type_annotation: Any) -> st.DataType:
234
246
 
235
247
  return st.StructType(fields)
236
248
 
237
- if issubclass(type_annotation, ConstrainedDecimal):
238
- precision = int(type_annotation.max_digits or 38)
239
- scale = int(type_annotation.decimal_places or precision)
240
- return st.DecimalType(precision, scale)
241
-
242
249
  if type_annotation is list:
243
250
  raise ValueError(
244
251
  f"list must have type annotation (e.g. `list[str]`), got {type_annotation!r}"