data-validation-engine 0.8.2__tar.gz → 0.8.3__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 (106) hide show
  1. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/PKG-INFO +3 -2
  2. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/pyproject.toml +4 -3
  3. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/duckdb_helpers.py +22 -13
  4. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/contract.py +18 -6
  5. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/spark_helpers.py +48 -17
  6. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/utilities.py +43 -0
  7. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/LICENSE +0 -0
  8. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/README.md +0 -0
  9. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/__init__.py +0 -0
  10. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/common/__init__.py +0 -0
  11. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/common/error_utils.py +0 -0
  12. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/__init__.py +0 -0
  13. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/__init__.py +0 -0
  14. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/__init__.py +0 -0
  15. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/auditing.py +0 -0
  16. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/backend.py +0 -0
  17. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/contract.py +0 -0
  18. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/core.py +0 -0
  19. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/reader.py +0 -0
  20. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/reference_data.py +0 -0
  21. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/rules.py +0 -0
  22. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/base/utilities.py +0 -0
  23. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/exceptions.py +0 -0
  24. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/__init__.py +0 -0
  25. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/__init__.py +0 -0
  26. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/auditing.py +0 -0
  27. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/contract.py +0 -0
  28. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/readers/__init__.py +0 -0
  29. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/readers/csv.py +0 -0
  30. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/readers/json.py +0 -0
  31. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/readers/xml.py +0 -0
  32. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/reference_data.py +0 -0
  33. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/rules.py +0 -0
  34. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/types.py +0 -0
  35. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/duckdb/utilities.py +0 -0
  36. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/__init__.py +0 -0
  37. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/auditing.py +0 -0
  38. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/backend.py +0 -0
  39. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/readers/__init__.py +0 -0
  40. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/readers/csv.py +0 -0
  41. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/readers/json.py +0 -0
  42. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/readers/xml.py +0 -0
  43. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/reference_data.py +0 -0
  44. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/rules.py +0 -0
  45. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/types.py +0 -0
  46. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/implementations/spark/utilities.py +0 -0
  47. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/metadata/__init__.py +0 -0
  48. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/metadata/contract.py +0 -0
  49. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/metadata/reporting.py +0 -0
  50. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/metadata/rules.py +0 -0
  51. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/__init__.py +0 -0
  52. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/csv.py +0 -0
  53. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/utilities.py +0 -0
  54. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/xml.py +0 -0
  55. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/readers/xml_linting.py +0 -0
  56. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/backends/types.py +0 -0
  57. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/__init__.py +0 -0
  58. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/base.py +0 -0
  59. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/__init__.py +0 -0
  60. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/filters.py +0 -0
  61. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/rule_stores/__init__.py +0 -0
  62. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/rule_stores/models.py +0 -0
  63. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/configuration/v1/steps.py +0 -0
  64. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/constants.py +0 -0
  65. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/engine.py +0 -0
  66. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/exceptions.py +0 -0
  67. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/functions/__init__.py +0 -0
  68. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/functions/implementations.py +0 -0
  69. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/loggers.py +0 -0
  70. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/message.py +0 -0
  71. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/models.py +0 -0
  72. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/templating.py +0 -0
  73. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/type_hints.py +0 -0
  74. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/core_engine/validation.py +0 -0
  75. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/__init__.py +0 -0
  76. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/domain_types.py +0 -0
  77. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/exc.py +0 -0
  78. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/function_library.py +0 -0
  79. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/function_wrapper.py +0 -0
  80. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/model_generator.py +0 -0
  81. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/models.py +0 -0
  82. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/metadata_parser/utilities.py +0 -0
  83. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/__init__.py +0 -0
  84. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/exceptions.py +0 -0
  85. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/__init__.py +0 -0
  86. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/helpers.py +0 -0
  87. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/__init__.py +0 -0
  88. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/base.py +0 -0
  89. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/dbfs.py +0 -0
  90. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/file.py +0 -0
  91. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/implementations/s3.py +0 -0
  92. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/log_handler.py +0 -0
  93. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/service.py +0 -0
  94. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/file_handling/utilities.py +0 -0
  95. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/type_hints.py +0 -0
  96. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/parser/utilities.py +0 -0
  97. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/pipeline/__init__.py +0 -0
  98. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/pipeline/duckdb_pipeline.py +0 -0
  99. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/pipeline/foundry_ddb_pipeline.py +0 -0
  100. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/pipeline/pipeline.py +0 -0
  101. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/pipeline/spark_pipeline.py +0 -0
  102. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/pipeline/utils.py +0 -0
  103. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/reporting/__init__.py +0 -0
  104. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/reporting/constants.py +0 -0
  105. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/src/dve/reporting/error_report.py +0 -0
  106. {data_validation_engine-0.8.2 → data_validation_engine-0.8.3}/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.2
3
+ Version: 0.8.3
4
4
  Summary: `nhs data validation engine` is a framework used to validate data
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -10,6 +10,7 @@ Requires-Python: >=3.10,<3.13
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
13
14
  Classifier: Operating System :: OS Independent
14
15
  Classifier: Topic :: Software Development :: Libraries
15
16
  Classifier: Typing :: Typed
@@ -25,7 +26,7 @@ Requires-Dist: pandas (==2.3.3)
25
26
  Requires-Dist: polars (==0.20.31)
26
27
  Requires-Dist: pyarrow (==23.0.1)
27
28
  Requires-Dist: pydantic (==1.10.19)
28
- Requires-Dist: pyspark (>=3.0.0,<=3.5.2)
29
+ Requires-Dist: pyspark (>=3.5.0,<=3.5.5)
29
30
  Requires-Dist: typing_extensions (==4.15.0)
30
31
  Requires-Dist: urllib3 (==2.7.0)
31
32
  Project-URL: Changelog, https://github.com/NHSDigital/data-validation-engine/blob/main/CHANGELOG.md
@@ -10,6 +10,7 @@ classifiers = [
10
10
  "Programming Language :: Python :: 3",
11
11
  "Programming Language :: Python :: 3.10",
12
12
  "Programming Language :: Python :: 3.11",
13
+ "Programming Language :: Python :: 3.12",
13
14
  "Operating System :: OS Independent",
14
15
  "Topic :: Software Development :: Libraries",
15
16
  "Typing :: Typed",
@@ -24,7 +25,7 @@ Issues = "https://github.com/NHSDigital/data-validation-engine/issues"
24
25
  Changelog = "https://github.com/NHSDigital/data-validation-engine/blob/main/CHANGELOG.md"
25
26
 
26
27
  [tool.poetry]
27
- version = "0.8.2"
28
+ version = "0.8.3"
28
29
  packages = [
29
30
  { include = "dve", from = "src" },
30
31
  ]
@@ -43,7 +44,7 @@ pandas = "2.3.3"
43
44
  polars = "0.20.31"
44
45
  pyarrow = "23.0.1"
45
46
  pydantic = "1.10.19"
46
- pyspark = ">=3.0.0,<=3.5.2"
47
+ pyspark = ">=3.5.0,<=3.5.5"
47
48
  typing_extensions = "4.15.0"
48
49
  urllib3 = "2.7.0" # dependency of boto3 & botocore
49
50
 
@@ -106,7 +107,7 @@ mkdocs = "1.6.1"
106
107
  mkdocstrings = { version = "1.0.3", extras = ["python"] }
107
108
  griffelib = "2.0.1"
108
109
  pymdown-extensions = "10.21.3"
109
- zensical = "0.0.31"
110
+ zensical = "0.0.46"
110
111
 
111
112
  [tool.ruff]
112
113
  line-length = 100
@@ -21,6 +21,7 @@ from typing_extensions import Annotated, get_args, get_origin, get_type_hints
21
21
 
22
22
  from dve.common.error_utils import get_feedback_errors_uri
23
23
  from dve.core_engine.backends.base.utilities import _get_non_heterogenous_type
24
+ from dve.core_engine.backends.utilities import DEFAULT_ISO_FORMATS, datetime_format_to_regex
24
25
  from dve.core_engine.constants import RECORD_INDEX_COLUMN_NAME
25
26
  from dve.core_engine.type_hints import URI, EntityName
26
27
  from dve.parser.file_handling.service import LocalFilesystemImplementation, _get_implementation
@@ -361,7 +362,7 @@ def duckdb_record_index(cls):
361
362
 
362
363
  def _cast_as_ddb_type(field_expr: str, type_annotation: Any) -> str:
363
364
  """Cast to Duck DB type"""
364
- return f"""try_cast({field_expr} as {get_duckdb_type_from_annotation(type_annotation)})"""
365
+ return f"""TRY_CAST({field_expr} as {get_duckdb_type_from_annotation(type_annotation)})"""
365
366
 
366
367
 
367
368
  def _ddb_safely_quote_name(field_name: str) -> str:
@@ -378,9 +379,6 @@ def get_duckdb_cast_statement_from_annotation(
378
379
  element_name: str,
379
380
  type_annotation: Any,
380
381
  parent_element: bool = True,
381
- date_regex: str = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
382
- timestamp_regex: str = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}((\+|\-)[0-9]{2}:[0-9]{2})?$", # pylint: disable=C0301
383
- time_regex: str = r"^[0-9]{2}:[0-9]{2}:[0-9]{2}$",
384
382
  ) -> str:
385
383
  """Generate casting statements for duckdb relations from type annotations"""
386
384
  type_origin = get_origin(type_annotation)
@@ -391,19 +389,23 @@ def get_duckdb_cast_statement_from_annotation(
391
389
  if type_origin is Union:
392
390
  python_type = _get_non_heterogenous_type(get_args(type_annotation))
393
391
  return get_duckdb_cast_statement_from_annotation(
394
- element_name, python_type, parent_element, date_regex, timestamp_regex
392
+ element_name,
393
+ python_type,
394
+ parent_element,
395
395
  )
396
396
 
397
397
  # Type hint is e.g. `List[str]`, check to ensure non-heterogenity.
398
398
  if type_origin is list or (isinstance(type_origin, type) and issubclass(type_origin, list)):
399
399
  element_type = _get_non_heterogenous_type(get_args(type_annotation))
400
- stmt = f"list_transform({quoted_name}, x -> {get_duckdb_cast_statement_from_annotation('x',element_type, False, date_regex, timestamp_regex)})" # pylint: disable=C0301
400
+ stmt = f"LIST_TRANSFORM({quoted_name}, x -> {get_duckdb_cast_statement_from_annotation('x',element_type, False,)})" # pylint: disable=C0301
401
401
  return stmt if not parent_element else _cast_as_ddb_type(stmt, type_annotation)
402
402
 
403
403
  if type_origin is Annotated:
404
404
  python_type, *other_args = get_args(type_annotation) # pylint: disable=unused-variable
405
405
  return get_duckdb_cast_statement_from_annotation(
406
- element_name, python_type, parent_element, date_regex, timestamp_regex
406
+ element_name,
407
+ python_type,
408
+ parent_element,
407
409
  ) # add other expected params here
408
410
  # Ensure that we have a concrete type at this point.
409
411
  if not isinstance(type_annotation, type):
@@ -428,7 +430,9 @@ def get_duckdb_cast_statement_from_annotation(
428
430
  continue
429
431
 
430
432
  fields[field_name] = get_duckdb_cast_statement_from_annotation(
431
- f"{element_name}.{field_name}", field_annotation, False, date_regex, timestamp_regex
433
+ f"{element_name}.{field_name}",
434
+ field_annotation,
435
+ False,
432
436
  )
433
437
 
434
438
  if not fields:
@@ -436,7 +440,7 @@ def get_duckdb_cast_statement_from_annotation(
436
440
  f"No type annotations in dict/dataclass type (got {type_annotation!r})"
437
441
  )
438
442
  cast_exprs = ",".join([f'"{nme}":= {stmt}' for nme, stmt in fields.items()])
439
- stmt = f"struct_pack({cast_exprs})"
443
+ stmt = f"STRUCT_PACK({cast_exprs})"
440
444
  return stmt if not parent_element else _cast_as_ddb_type(stmt, type_annotation)
441
445
 
442
446
  if type_annotation is list:
@@ -447,18 +451,23 @@ def get_duckdb_cast_statement_from_annotation(
447
451
  raise ValueError(f"dict must be `typing.TypedDict` subclass, got {type_annotation!r}")
448
452
 
449
453
  for type_ in type_annotation.mro():
454
+ _date_format: str = getattr( # type: ignore
455
+ type_, "DATE_FORMAT", DEFAULT_ISO_FORMATS.get(type_, DEFAULT_ISO_FORMATS.get(datetime))
456
+ )
457
+ 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
458
+
450
459
  # datetime is subclass of date, so needs to be handled first
451
460
  if issubclass(type_, datetime):
452
- stmt = rf"CASE WHEN REGEXP_MATCHES(TRIM({quoted_name}), '{timestamp_regex}') THEN TRY_CAST(TRIM({quoted_name}) as TIMESTAMP) ELSE NULL END" # pylint: disable=C0301
461
+ stmt = rf"TRY_CAST({dt_cast_statement} as TIMESTAMP)"
453
462
  return stmt
454
463
  if issubclass(type_, date):
455
- stmt = rf"CASE WHEN REGEXP_MATCHES(TRIM({quoted_name}), '{date_regex}') THEN TRY_CAST(TRIM({quoted_name}) as DATE) ELSE NULL END" # pylint: disable=C0301
464
+ stmt = rf"TRY_CAST({dt_cast_statement} as DATE)"
456
465
  return stmt
457
466
  if issubclass(type_, time):
458
- stmt = rf"CASE WHEN REGEXP_MATCHES(TRIM({quoted_name}), '{time_regex}') THEN TRY_CAST(TRIM({quoted_name}) as TIME) ELSE NULL END" # pylint: disable=C0301
467
+ stmt = rf"TRY_CAST({dt_cast_statement} as TIME)"
459
468
  return stmt
460
469
  duck_type = get_duckdb_type_from_annotation(type_)
461
470
  if duck_type:
462
- stmt = f"trim({quoted_name})"
471
+ stmt = f"TRIM({quoted_name})"
463
472
  return _cast_as_ddb_type(stmt, type_) if parent_element else stmt
464
473
  raise ValueError(f"No equivalent DuckDB type for {type_annotation!r}")
@@ -7,6 +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
11
  from pyspark.sql import DataFrame, SparkSession
11
12
  from pyspark.sql import functions as sf
12
13
  from pyspark.sql.functions import col, lit
@@ -26,6 +27,7 @@ from dve.core_engine.backends.exceptions import (
26
27
  )
27
28
  from dve.core_engine.backends.implementations.spark.spark_helpers import (
28
29
  df_is_empty,
30
+ get_spark_cast_statement_from_annotation,
29
31
  get_type_from_annotation,
30
32
  spark_read_parquet,
31
33
  spark_record_index,
@@ -86,7 +88,7 @@ class SparkDataContract(BaseDataContract[DataFrame]):
86
88
  schema=get_type_from_annotation(schema),
87
89
  )
88
90
 
89
- # pylint: disable=R0915
91
+ # pylint: disable=R0914,R0915
90
92
  def apply_data_contract(
91
93
  self,
92
94
  working_dir: URI,
@@ -102,6 +104,7 @@ class SparkDataContract(BaseDataContract[DataFrame]):
102
104
 
103
105
  successful = True
104
106
  for entity_name, record_df in entities.items():
107
+ entity_fields: dict[str, ModelField] = contract_metadata.schemas[entity_name].__fields__
105
108
  spark_schema = get_type_from_annotation(contract_metadata.schemas[entity_name])
106
109
  spark_schema.add(StructField(RECORD_INDEX_COLUMN_NAME, LongType()))
107
110
  if df_is_empty(record_df):
@@ -144,12 +147,21 @@ class SparkDataContract(BaseDataContract[DataFrame]):
144
147
  self.logger.info(f"Data contract found {msg_count} issues in {entity_name}")
145
148
 
146
149
  try:
150
+ # TODO: will need to revisit in pydantic v2 bump as model field no longer available
147
151
  record_df = record_df.select(
148
- [
149
- col(column.name).cast(column.dataType)
150
- for column in spark_schema
151
- if column.name in record_df.columns
152
- ]
152
+ *[
153
+ (
154
+ get_spark_cast_statement_from_annotation(
155
+ fld, mdl_field.annotation
156
+ ).alias(fld)
157
+ if fld in record_df.columns
158
+ else lit(None).cast(
159
+ get_type_from_annotation(mdl_field.annotation).alias(fld)
160
+ )
161
+ )
162
+ for fld, mdl_field in entity_fields.items()
163
+ ],
164
+ col(RECORD_INDEX_COLUMN_NAME).cast(LongType()).alias(RECORD_INDEX_COLUMN_NAME),
153
165
  )
154
166
  except Exception as err: # pylint: disable=broad-except
155
167
  successful = False
@@ -28,6 +28,7 @@ from typing_extensions import Annotated, Protocol, TypedDict, get_args, get_orig
28
28
 
29
29
  from dve.common.error_utils import get_feedback_errors_uri
30
30
  from dve.core_engine.backends.base.utilities import _get_non_heterogenous_type
31
+ from dve.core_engine.backends.utilities import DEFAULT_ISO_FORMATS, datetime_format_to_regex
31
32
  from dve.core_engine.constants import RECORD_INDEX_COLUMN_NAME
32
33
  from dve.core_engine.type_hints import URI, EntityName
33
34
 
@@ -99,6 +100,21 @@ PYTHON_TYPE_TO_SPARK_TYPE: dict[type, st.DataType] = {
99
100
  }
100
101
  """A mapping of Python types to the equivalent Spark types."""
101
102
 
103
+ PYTHON_TO_JAVA_DT_FORMAT_MAP: dict[str, str] = {
104
+ "%Y": "yyyy",
105
+ "%y": "yy",
106
+ "%m": "MM",
107
+ "%d": "dd",
108
+ "%H": "HH",
109
+ "%M": "mm",
110
+ "%S": "ss",
111
+ "%z": "XX",
112
+ "%Z": "z",
113
+ }
114
+ """A mapping of python to java datetime component formats. Not exhaustive but will hopefully support
115
+ all requirements.
116
+ """
117
+
102
118
 
103
119
  PydanticModel = TypeVar("PydanticModel", bound=BaseModel)
104
120
  """An Pydantic model."""
@@ -272,6 +288,13 @@ def create_udf(function: Callable) -> Callable:
272
288
  return udf(function, returnType=return_type)
273
289
 
274
290
 
291
+ def python_to_java_datetime_format(datetime_fmt: str) -> str:
292
+ "Helper to convert python datetime formats to Java datetime formats"
293
+ for pt, jt in PYTHON_TO_JAVA_DT_FORMAT_MAP.items():
294
+ datetime_fmt = datetime_fmt.replace(pt, jt)
295
+ return datetime_fmt.replace("T", "'T'")
296
+
297
+
275
298
  SupportedBaseType = Union[str, int, bool, float, Decimal, dt.date, dt.datetime]
276
299
  """Supported base types for Spark literals."""
277
300
  SparkLiteralType = Union[ # type: ignore
@@ -506,11 +529,7 @@ def _spark_safely_quote_name(field_name: str) -> str:
506
529
 
507
530
  # pylint: disable=R0801
508
531
  def get_spark_cast_statement_from_annotation(
509
- element_name: str,
510
- type_annotation: Any,
511
- parent_element: bool = True,
512
- date_regex: str = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$",
513
- timestamp_regex: str = r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}((\\+|\\-)[0-9]{2}:[0-9]{2})?$", # pylint: disable=C0301
532
+ element_name: str, type_annotation: Any, parent_element: bool = True
514
533
  ):
515
534
  """Generate casting statements for spark dataframes based on type annotations"""
516
535
  type_origin = get_origin(type_annotation)
@@ -520,20 +539,18 @@ def get_spark_cast_statement_from_annotation(
520
539
  # An `Optional` or `Union` type, check to ensure non-heterogenity.
521
540
  if type_origin is Union:
522
541
  python_type = _get_non_heterogenous_type(get_args(type_annotation))
523
- return get_spark_cast_statement_from_annotation(
524
- element_name, python_type, parent_element, date_regex, timestamp_regex
525
- )
542
+ return get_spark_cast_statement_from_annotation(element_name, python_type, parent_element)
526
543
 
527
544
  # Type hint is e.g. `List[str]`, check to ensure non-heterogenity.
528
545
  if type_origin is list or (isinstance(type_origin, type) and issubclass(type_origin, list)):
529
546
  element_type = _get_non_heterogenous_type(get_args(type_annotation))
530
- stmt = f"transform({quoted_name}, x -> {get_spark_cast_statement_from_annotation('x',element_type, False, date_regex, timestamp_regex)})" # pylint: disable=C0301
547
+ stmt = f"TRANSFORM({quoted_name}, x -> {get_spark_cast_statement_from_annotation('x',element_type, False)})" # pylint: disable=C0301
531
548
  return stmt if not parent_element else _cast_as_spark_type(stmt, type_annotation)
532
549
 
533
550
  if type_origin is Annotated:
534
551
  python_type, *_ = get_args(type_annotation) # pylint: disable=unused-variable
535
552
  return get_spark_cast_statement_from_annotation(
536
- element_name, python_type, parent_element, date_regex, timestamp_regex
553
+ element_name, python_type, parent_element
537
554
  ) # add other expected params here
538
555
  # Ensure that we have a concrete type at this point.
539
556
  if not isinstance(type_annotation, type):
@@ -558,7 +575,7 @@ def get_spark_cast_statement_from_annotation(
558
575
  continue
559
576
 
560
577
  fields[field_name] = get_spark_cast_statement_from_annotation(
561
- f"{element_name}.{field_name}", field_annotation, False, date_regex, timestamp_regex
578
+ f"{element_name}.{field_name}", field_annotation, False
562
579
  )
563
580
 
564
581
  if not fields:
@@ -566,7 +583,7 @@ def get_spark_cast_statement_from_annotation(
566
583
  f"No type annotations in dict/dataclass type (got {type_annotation!r})"
567
584
  )
568
585
  cast_exprs = ",".join([f"{stmt} AS `{nme}`" for nme, stmt in fields.items()])
569
- stmt = f"struct({cast_exprs})"
586
+ stmt = f"STRUCT({cast_exprs})"
570
587
  return stmt if not parent_element else _cast_as_spark_type(stmt, type_annotation)
571
588
  if type_annotation is list:
572
589
  raise ValueError(
@@ -576,15 +593,29 @@ def get_spark_cast_statement_from_annotation(
576
593
  raise ValueError(f"dict must be `typing.TypedDict` subclass, got {type_annotation!r}")
577
594
 
578
595
  for type_ in type_annotation.mro():
596
+ _date_format: str = getattr( # type: ignore
597
+ type_,
598
+ "DATE_FORMAT",
599
+ DEFAULT_ISO_FORMATS.get(type_, DEFAULT_ISO_FORMATS.get(dt.datetime)),
600
+ )
601
+
602
+ # pylint: disable=C0301
603
+ dt_cast_statement = f"CASE WHEN REGEXP(TRIM({quoted_name}), '{datetime_format_to_regex(_date_format)}') THEN TRY_TO_TIMESTAMP(TRIM({quoted_name}), \"{python_to_java_datetime_format(_date_format)}\") ELSE NULL END" # pylint: disable=C0301
579
604
  # datetime is subclass of date, so needs to be handled first
580
605
  if issubclass(type_, dt.datetime):
581
- stmt = rf"CASE WHEN REGEXP(TRIM({quoted_name}), '{timestamp_regex}') THEN TRIM({quoted_name}) ELSE NULL END" # pylint: disable=C0301
582
- return _cast_as_spark_type(stmt, type_) if parent_element else stmt
606
+ return (
607
+ _cast_as_spark_type(dt_cast_statement, type_)
608
+ if parent_element
609
+ else dt_cast_statement
610
+ )
583
611
  if issubclass(type_, dt.date):
584
- stmt = rf"CASE WHEN REGEXP(TRIM({quoted_name}), '{date_regex}') THEN TRIM({quoted_name}) ELSE NULL END" # pylint: disable=C0301
585
- return _cast_as_spark_type(stmt, type_) if parent_element else stmt
612
+ return (
613
+ _cast_as_spark_type(dt_cast_statement, type_)
614
+ if parent_element
615
+ else dt_cast_statement
616
+ )
586
617
  spark_type = get_type_from_annotation(type_)
587
618
  if spark_type:
588
- stmt = f"trim({quoted_name})"
619
+ stmt = f"TRIM({quoted_name})"
589
620
  return _cast_as_spark_type(stmt, type_) if parent_element else stmt
590
621
  raise ValueError(f"No equivalent Spark type for {type_annotation!r}")
@@ -23,6 +23,49 @@ if sys.version_info[:2] <= (3, 7):
23
23
  else:
24
24
  from typing import Annotated, get_args, get_origin, get_type_hints
25
25
 
26
+ DEFAULT_ISO_FORMATS: dict[type, str] = {
27
+ date: "%Y-%m-%d",
28
+ datetime: "%Y-%m-%dT%H:%M:%S",
29
+ time: "%H:%M:%S",
30
+ }
31
+ """Mapping of default ISO formats to use when date format not supplied"""
32
+
33
+ PYTHON_DATE_FORMAT_REGEX_HELPER: dict[str, str] = {
34
+ "Y": r"[0-9]{4}",
35
+ "y": r"[0-9]{2}",
36
+ "m": r"[0-9]{2}",
37
+ "d": r"[0-9]{2}",
38
+ "H": r"[0-9]{2}",
39
+ "M": r"[0-9]{2}",
40
+ "S": r"[0-9]{2}",
41
+ "z": r"(\+|\-)?[0-9]+(\.[0-9]*)?",
42
+ "Z": r"[A-Z]{0,3}",
43
+ }
44
+
45
+ REGEXP_NEED_ESCAPE_CHARS: tuple[str, str, str] = ("+", "-", ".")
46
+ """Helper to map python date format to regexp expression. Not exhaustive, but aims to cover
47
+ all foreseen use cases."""
48
+
49
+
50
+ def datetime_format_to_regex(format_str: str) -> str:
51
+ """
52
+ Helper function to convert python datetime formats to regexp string checks for casting
53
+ purposes.
54
+ """
55
+
56
+ tokens = list(format_str.replace("%", ""))
57
+
58
+ for idx, tkn in enumerate(tokens):
59
+ if rpl := PYTHON_DATE_FORMAT_REGEX_HELPER.get(tkn):
60
+ tokens[idx] = rpl
61
+ elif tkn in REGEXP_NEED_ESCAPE_CHARS:
62
+ tokens[idx] = rf"\{tkn}"
63
+ else:
64
+ continue
65
+ tokens = [PYTHON_DATE_FORMAT_REGEX_HELPER.get(tkn, tkn) for tkn in tokens]
66
+ return "".join(["^", *tokens, "$"])
67
+
68
+
26
69
  PYTHON_TYPE_TO_POLARS_TYPE: dict[type, PolarsType] = {
27
70
  # issue with decimal conversion at the moment...
28
71
  str: pl.Utf8, # type: ignore