google-pso-data-validator 7.2.0__tar.gz → 7.3.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 (89) hide show
  1. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/PKG-INFO +1 -1
  2. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/__main__.py +0 -1
  3. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/combiner.py +135 -48
  4. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/consts.py +25 -14
  5. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/data_validation.py +6 -3
  6. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/result_handlers/bigquery.py +1 -1
  7. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/secret_manager.py +6 -2
  8. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/util.py +18 -0
  9. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/google_pso_data_validator.egg-info/PKG-INFO +1 -1
  10. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/setup.py +1 -1
  11. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_oracle/__init__.py +16 -1
  12. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_snowflake/api.py +4 -1
  13. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_teradata/__init__.py +3 -10
  14. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_teradata/api.py +5 -0
  15. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/LICENSE +0 -0
  16. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/README.md +0 -0
  17. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/__init__.py +0 -0
  18. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/app.py +0 -0
  19. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/cli_tools.py +0 -0
  20. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/client_info.py +0 -0
  21. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/clients.py +0 -0
  22. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/config_manager.py +0 -0
  23. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/exceptions.py +0 -0
  24. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/find_tables.py +0 -0
  25. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/gcs_helper.py +0 -0
  26. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/jellyfish_distance.py +0 -0
  27. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/metadata.py +0 -0
  28. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/partition_builder.py +0 -0
  29. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/query_builder/__init__.py +0 -0
  30. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/query_builder/partition_row_builder.py +0 -0
  31. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/query_builder/query_builder.py +0 -0
  32. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/query_builder/random_row_builder.py +0 -0
  33. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/raw_query.py +0 -0
  34. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/result_handlers/__init__.py +0 -0
  35. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/result_handlers/text.py +0 -0
  36. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/schema_validation.py +0 -0
  37. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/state_manager.py +0 -0
  38. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/data_validation/validation_builder.py +0 -0
  39. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/google_pso_data_validator.egg-info/SOURCES.txt +0 -0
  40. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/google_pso_data_validator.egg-info/dependency_links.txt +0 -0
  41. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/google_pso_data_validator.egg-info/entry_points.txt +0 -0
  42. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/google_pso_data_validator.egg-info/requires.txt +0 -0
  43. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/google_pso_data_validator.egg-info/top_level.txt +0 -0
  44. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/setup.cfg +0 -0
  45. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_addon/__init__.py +0 -0
  46. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_addon/api.py +0 -0
  47. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_addon/operations.py +0 -0
  48. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_biquery/__init__.py +0 -0
  49. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_biquery/api.py +0 -0
  50. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_cloud_spanner/__init__.py +0 -0
  51. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_cloud_spanner/api.py +0 -0
  52. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_cloud_spanner/client.py +0 -0
  53. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_cloud_spanner/compiler.py +0 -0
  54. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_cloud_spanner/datatypes.py +0 -0
  55. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_cloud_spanner/registry.py +0 -0
  56. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_cloud_spanner/tests/__init__.py +0 -0
  57. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_cloud_spanner/tests/conftest.py +0 -0
  58. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_cloud_spanner/to_pandas.py +0 -0
  59. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_db2/__init__.py +0 -0
  60. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_db2/api.py +0 -0
  61. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_db2/compiler.py +0 -0
  62. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_db2/datatypes.py +0 -0
  63. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_db2/registry.py +0 -0
  64. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_impala/__init__.py +0 -0
  65. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_impala/api.py +0 -0
  66. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_mssql/__init__.py +0 -0
  67. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_mssql/api.py +0 -0
  68. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_mssql/datatypes.py +0 -0
  69. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_mssql/registry.py +0 -0
  70. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_mysql/__init__.py +0 -0
  71. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_mysql/base_sql_compiler/__init__.py +0 -0
  72. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_mysql/base_sql_compiler/select_builder.py +0 -0
  73. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_mysql/compiler.py +0 -0
  74. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_oracle/api.py +0 -0
  75. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_oracle/compiler.py +0 -0
  76. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_oracle/datatypes.py +0 -0
  77. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_oracle/registry.py +0 -0
  78. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_postgres/__init__.py +0 -0
  79. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_postgres/client.py +0 -0
  80. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_postgres/datatypes.py +0 -0
  81. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_postgres/registry.py +0 -0
  82. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_redshift/__init__.py +0 -0
  83. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_redshift/api.py +0 -0
  84. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_redshift/compiler.py +0 -0
  85. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_snowflake/__init__.py +0 -0
  86. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_snowflake/datatypes.py +0 -0
  87. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_teradata/compiler.py +0 -0
  88. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_teradata/datatypes.py +0 -0
  89. {google_pso_data_validator-7.2.0 → google_pso_data_validator-7.3.0}/third_party/ibis/ibis_teradata/registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: google-pso-data-validator
3
- Version: 7.2.0
3
+ Version: 7.3.0
4
4
  Summary: A package to enable easy data validation
5
5
  Author: PSO DVT Engineering team
6
6
  Author-email: data-validator-eng@google.com
@@ -570,7 +570,6 @@ def run_validations(args, config_managers):
570
570
  Args:
571
571
  config_managers (list[ConfigManager]): List of config manager instances.
572
572
  """
573
- # TODO(issue/31): Add parallel execution logic
574
573
  for config_manager in config_managers:
575
574
  run_validation(config_manager, dry_run=args.dry_run, verbose=args.verbose)
576
575
 
@@ -25,17 +25,20 @@ import logging
25
25
  import ibis
26
26
  import ibis.expr.datatypes as dt
27
27
 
28
+ from typing import TYPE_CHECKING
28
29
  from data_validation import consts
29
30
 
30
- DEFAULT_SOURCE = "source"
31
- DEFAULT_TARGET = "target"
31
+ if TYPE_CHECKING:
32
+ from pandas import DataFrame
33
+ import ibis.expr.types.relations.table as IbisTable
34
+ from data_validation.metadata import RunMetadata, ValidationMetadata
32
35
 
33
36
 
34
37
  def generate_report(
35
38
  client,
36
- run_metadata,
37
- source,
38
- target,
39
+ run_metadata: "RunMetadata",
40
+ source: "IbisTable",
41
+ target: "IbisTable",
39
42
  join_on_fields=(),
40
43
  is_value_comparison=False,
41
44
  verbose=False,
@@ -44,8 +47,7 @@ def generate_report(
44
47
 
45
48
  Args:
46
49
  client (ibis.client.Client): Ibis client used to combine results.
47
- run_metadata (data_validation.metadata.RunMetadata):
48
- Metadata about the run and validations.
50
+ run_metadata: Metadata about the run and validations.
49
51
  source (ibis.QUERY): Ibis query / table object.
50
52
  target (ibis.QUERY): Ibis query / table object.
51
53
  join_on_fields (Sequence[str]):
@@ -69,7 +71,7 @@ def generate_report(
69
71
  if source_names != target_names:
70
72
  raise ValueError(
71
73
  "Expected source and target to have same schema, got "
72
- f"source: {source_names} target: {target_names}"
74
+ f"{consts.RESULT_TYPE_SOURCE}: {source_names}; {consts.RESULT_TYPE_TARGET}: {target_names}"
73
75
  )
74
76
  differences_pivot = _calculate_differences(
75
77
  source, target, join_on_fields, run_metadata.validations, is_value_comparison
@@ -87,13 +89,17 @@ def generate_report(
87
89
  target_df = client.execute(target_pivot)
88
90
 
89
91
  con = ibis.pandas.connect(
90
- {"source": source_df, "differences": differences_df, "target": target_df}
92
+ {
93
+ consts.RESULT_TYPE_SOURCE: source_df,
94
+ "differences": differences_df,
95
+ consts.RESULT_TYPE_TARGET: target_df,
96
+ }
91
97
  )
92
98
  joined = _join_pivots(
93
99
  con.tables.source, con.tables.target, con.tables.differences, join_on_fields
94
100
  )
95
101
 
96
- documented = _add_metadata(joined, run_metadata)
102
+ documented, run_metadata = _add_metadata(joined, run_metadata)
97
103
 
98
104
  if verbose:
99
105
  logging.debug("-- ** Combiner Query ** --")
@@ -112,15 +118,17 @@ def generate_report(
112
118
  first.get_table_name(consts.RESULT_TYPE_TARGET), inplace=True
113
119
  )
114
120
 
121
+ _get_summary(run_metadata, result_df, source_df, target_df)
122
+
115
123
  return result_df
116
124
 
117
125
 
118
126
  def _calculate_difference(
119
- field_differences,
127
+ field_differences: "IbisTable",
120
128
  datatype: dt.DataType,
121
129
  target_type: dt.DataType,
122
- validation,
123
- is_value_comparison,
130
+ validation: "ValidationMetadata",
131
+ is_value_comparison: bool,
124
132
  ):
125
133
  pct_threshold = ibis.literal(validation.threshold)
126
134
  if datatype.is_timestamp() or datatype.is_date():
@@ -210,12 +218,16 @@ def _calculate_difference(
210
218
  difference.name("difference"),
211
219
  pct_difference.name("pct_difference"),
212
220
  pct_threshold.name("pct_threshold"),
213
- validation_status.name("validation_status"),
221
+ validation_status.name(consts.VALIDATION_STATUS),
214
222
  )
215
223
 
216
224
 
217
225
  def _calculate_differences(
218
- source, target, join_on_fields, validations, is_value_comparison
226
+ source: "IbisTable",
227
+ target: "IbisTable",
228
+ join_on_fields: tuple,
229
+ validations: "dict[ValidationMetadata]",
230
+ is_value_comparison: bool,
219
231
  ):
220
232
  """Calculate differences between source and target fields.
221
233
 
@@ -267,7 +279,12 @@ def _calculate_differences(
267
279
  return differences_pivot
268
280
 
269
281
 
270
- def _pivot_result(result, join_on_fields, validations, result_type):
282
+ def _pivot_result(
283
+ result: "IbisTable",
284
+ join_on_fields: tuple,
285
+ validations: "dict[ValidationMetadata]",
286
+ result_type: str,
287
+ ):
271
288
  all_fields = frozenset(result.schema().names)
272
289
  validation_fields = (
273
290
  all_fields - frozenset(join_on_fields)
@@ -285,19 +302,21 @@ def _pivot_result(result, join_on_fields, validations, result_type):
285
302
  ibis.literal("{")
286
303
  + ibis.literal(", ").join(validation.primary_keys)
287
304
  + ibis.literal("}")
288
- ).name("primary_keys")
305
+ ).name(consts.CONFIG_PRIMARY_KEYS)
289
306
  else:
290
- primary_keys = ibis.literal(None).cast("string").name("primary_keys")
307
+ primary_keys = (
308
+ ibis.literal(None).cast("string").name(consts.CONFIG_PRIMARY_KEYS)
309
+ )
291
310
 
292
311
  pivots.append(
293
312
  result.projection(
294
313
  (
295
314
  ibis.literal(field).name("validation_name"),
296
315
  ibis.literal(validation.validation_type).name(
297
- "validation_type"
316
+ consts.VALIDATION_TYPE
298
317
  ),
299
318
  ibis.literal(validation.aggregation_type).name(
300
- "aggregation_type"
319
+ consts.AGGREGATION_TYPE
301
320
  ),
302
321
  ibis.literal(validation.get_table_name(result_type)).name(
303
322
  "table_name"
@@ -333,7 +352,12 @@ def _as_json(expr):
333
352
  )
334
353
 
335
354
 
336
- def _join_pivots(source, target, differences, join_on_fields):
355
+ def _join_pivots(
356
+ source: "IbisTable",
357
+ target: "IbisTable",
358
+ differences: "IbisTable",
359
+ join_on_fields: tuple,
360
+ ):
337
361
  if join_on_fields:
338
362
  join_values = []
339
363
  for field in join_on_fields:
@@ -346,62 +370,125 @@ def _join_pivots(source, target, differences, join_on_fields):
346
370
 
347
371
  group_by_columns = (
348
372
  ibis.literal("{") + ibis.literal(", ").join(join_values) + ibis.literal("}")
349
- ).name("group_by_columns")
373
+ ).name(consts.GROUP_BY_COLUMNS)
350
374
  else:
351
- group_by_columns = ibis.literal(None).cast("string").name("group_by_columns")
375
+ group_by_columns = (
376
+ ibis.literal(None).cast("string").name(consts.GROUP_BY_COLUMNS)
377
+ )
352
378
 
353
379
  join_keys = ("validation_name",) + join_on_fields
354
380
  source_difference = source.join(differences, join_keys, how="outer")[
355
381
  [source[field] for field in join_keys]
356
382
  + [
357
- source["validation_type"],
358
- source["aggregation_type"],
359
- source["table_name"],
383
+ source[consts.VALIDATION_TYPE],
384
+ source[consts.AGGREGATION_TYPE],
385
+ source[consts.CONFIG_TABLE_NAME],
360
386
  source["column_name"],
361
- source["primary_keys"],
387
+ source[consts.CONFIG_PRIMARY_KEYS],
362
388
  source["num_random_rows"],
363
389
  source["agg_value"],
364
390
  differences["difference"],
365
391
  differences["pct_difference"],
366
392
  differences["pct_threshold"],
367
- differences["validation_status"],
393
+ differences[consts.VALIDATION_STATUS],
368
394
  ]
369
395
  ]
370
396
  joined = source_difference.join(target, join_keys, how="outer")[
371
397
  source_difference["validation_name"],
372
- source_difference["validation_type"]
373
- .fillna(target["validation_type"])
374
- .name("validation_type"),
375
- source_difference["aggregation_type"]
376
- .fillna(target["aggregation_type"])
377
- .name("aggregation_type"),
378
- source_difference["table_name"].name("source_table_name"),
379
- source_difference["column_name"].name("source_column_name"),
380
- source_difference["agg_value"].name("source_agg_value"),
381
- target["table_name"].name("target_table_name"),
382
- target["column_name"].name("target_column_name"),
383
- target["agg_value"].name("target_agg_value"),
398
+ source_difference[consts.VALIDATION_TYPE]
399
+ .fillna(target[consts.VALIDATION_TYPE])
400
+ .name(consts.VALIDATION_TYPE),
401
+ source_difference[consts.AGGREGATION_TYPE]
402
+ .fillna(target[consts.AGGREGATION_TYPE])
403
+ .name(consts.AGGREGATION_TYPE),
404
+ source_difference["table_name"].name(consts.SOURCE_TABLE_NAME),
405
+ source_difference["column_name"].name(consts.SOURCE_COLUMN_NAME),
406
+ source_difference["agg_value"].name(consts.SOURCE_AGG_VALUE),
407
+ target["table_name"].name(consts.TARGET_TABLE_NAME),
408
+ target["column_name"].name(consts.TARGET_COLUMN_NAME),
409
+ target["agg_value"].name(consts.TARGET_AGG_VALUE),
384
410
  group_by_columns,
385
- source_difference["primary_keys"],
411
+ source_difference[consts.CONFIG_PRIMARY_KEYS],
386
412
  source_difference["num_random_rows"],
387
413
  source_difference["difference"],
388
414
  source_difference["pct_difference"],
389
415
  source_difference["pct_threshold"],
390
- source_difference["validation_status"],
416
+ source_difference[consts.VALIDATION_STATUS],
391
417
  ]
392
418
  return joined
393
419
 
394
420
 
395
- def _add_metadata(joined, run_metadata):
421
+ def _add_metadata(joined: "IbisTable", run_metadata: "RunMetadata"):
396
422
  # TODO: Add source and target queries to metadata
397
423
  run_metadata.end_time = datetime.datetime.now(datetime.timezone.utc)
398
424
 
399
425
  joined = joined[
400
426
  joined,
401
- ibis.literal(run_metadata.run_id).name("run_id"),
402
- ibis.literal(run_metadata.labels).name("labels"),
403
- ibis.literal(run_metadata.start_time).name("start_time"),
404
- ibis.literal(run_metadata.end_time).name("end_time"),
427
+ ibis.literal(run_metadata.run_id).name(consts.CONFIG_RUN_ID),
428
+ ibis.literal(run_metadata.labels).name(consts.CONFIG_LABELS),
429
+ ibis.literal(run_metadata.start_time).name(consts.CONFIG_START_TIME),
430
+ ibis.literal(run_metadata.end_time).name(consts.CONFIG_END_TIME),
405
431
  ]
406
432
 
407
- return joined
433
+ return (joined, run_metadata)
434
+
435
+
436
+ def _get_summary(
437
+ run_metadata: "RunMetadata",
438
+ result_df: "DataFrame",
439
+ source_df: "DataFrame",
440
+ target_df: "DataFrame",
441
+ ):
442
+ """Logs a summary report/stats of row validation results."""
443
+ try:
444
+ if result_df.loc[0, consts.VALIDATION_TYPE] == consts.ROW_VALIDATION:
445
+ # Vectorized calculations for all counts
446
+ success_condition = (
447
+ result_df[consts.VALIDATION_STATUS] == consts.VALIDATION_STATUS_SUCCESS
448
+ )
449
+ fail_condition = ~success_condition # Invert success for fail condition
450
+
451
+ source_not_in_target = (
452
+ result_df[consts.SOURCE_AGG_VALUE].notnull()
453
+ & result_df[consts.TARGET_AGG_VALUE].isnull()
454
+ )
455
+ target_not_in_source = (
456
+ result_df[consts.SOURCE_AGG_VALUE].isnull()
457
+ & result_df[consts.TARGET_AGG_VALUE].notnull()
458
+ )
459
+ present_in_both_tables = (
460
+ result_df[consts.SOURCE_AGG_VALUE].notnull()
461
+ & result_df[consts.TARGET_AGG_VALUE].notnull()
462
+ )
463
+
464
+ logging.info(
465
+ {
466
+ consts.CONFIG_RUN_ID: run_metadata.run_id,
467
+ consts.CONFIG_START_TIME: run_metadata.start_time.strftime(
468
+ "%Y-%m-%d %H:%M:%S %Z"
469
+ ),
470
+ consts.CONFIG_END_TIME: run_metadata.end_time.strftime(
471
+ "%Y-%m-%d %H:%M:%S %Z"
472
+ ),
473
+ consts.TOTAL_SOURCE_ROWS: source_df.shape[0],
474
+ consts.TOTAL_TARGET_ROWS: target_df.shape[0],
475
+ consts.TOTAL_ROWS_VALIDATED: result_df.shape[0],
476
+ # Using .sum() on boolean Series for much faster counting
477
+ consts.TOTAL_ROWS_SUCCESS: success_condition.sum(),
478
+ consts.TOTAL_ROWS_FAIL: fail_condition.sum(),
479
+ consts.FAILED_SOURCE_NOT_IN_TARGET: (
480
+ fail_condition & source_not_in_target
481
+ ).sum(),
482
+ consts.FAILED_TARGET_NOT_IN_SOURCE: (
483
+ fail_condition & target_not_in_source
484
+ ).sum(),
485
+ consts.FAILED_PRESENT_IN_BOTH_TABLES: (
486
+ fail_condition & present_in_both_tables
487
+ ).sum(),
488
+ }
489
+ )
490
+ except Exception as e:
491
+ logging.warning(
492
+ f"Error while generating summary report of row validation results: {e}",
493
+ exc_info=True,
494
+ )
@@ -56,6 +56,8 @@ CONFIG_CASE_INSENSITIVE_MATCH = "case_insensitive_match"
56
56
  CONFIG_ROW_CONCAT = "concat"
57
57
  CONFIG_ROW_HASH = "hash"
58
58
  CONFIG_RUN_ID = "run_id"
59
+ CONFIG_START_TIME = "start_time"
60
+ CONFIG_END_TIME = "end_time"
59
61
  CONFIG_SOURCE_COLUMN = "source_column"
60
62
  CONFIG_TARGET_COLUMN = "target_column"
61
63
  CONFIG_THRESHOLD = "threshold"
@@ -142,12 +144,21 @@ TARGET_AGG_VALUE = "target_agg_value"
142
144
  VALIDATION_STATUS = "validation_status"
143
145
  VALIDATION_STATUS_SUCCESS = "success"
144
146
  VALIDATION_STATUS_FAIL = "fail"
145
- # TODO: update if we start to support other statuses
146
147
  VALIDATION_STATUSES = [
147
148
  VALIDATION_STATUS_SUCCESS,
148
149
  VALIDATION_STATUS_FAIL,
149
150
  ]
150
151
 
152
+ # Summary stats of Row Validation results
153
+ TOTAL_SOURCE_ROWS = "total_source_rows"
154
+ TOTAL_TARGET_ROWS = "total_target_rows"
155
+ TOTAL_ROWS_VALIDATED = "total_rows_validated"
156
+ TOTAL_ROWS_SUCCESS = "total_rows_success_validation_status"
157
+ TOTAL_ROWS_FAIL = "total_rows_fail_validation_status"
158
+ FAILED_SOURCE_NOT_IN_TARGET = "failed_rows_present_in_source_not_in_target"
159
+ FAILED_TARGET_NOT_IN_SOURCE = "failed_rows_present_in_target_not_in_source"
160
+ FAILED_PRESENT_IN_BOTH_TABLES = "failed_rows_present_in_both_source_and_target"
161
+
151
162
  # SQL Template Formatting
152
163
  # TODO: should this be managed in query_builder if that is the only place its used?
153
164
  COUNT_STAR = "{count_star}"
@@ -160,25 +171,25 @@ FORMAT_TYPES = ["csv", "json", "table", "text"]
160
171
 
161
172
  # Text Result Handler column filter list
162
173
  COLUMN_FILTER_LIST = [
163
- "aggregation_type",
164
- "end_time",
165
- "labels",
174
+ AGGREGATION_TYPE,
175
+ CONFIG_END_TIME,
176
+ CONFIG_LABELS,
166
177
  "pct_threshold",
167
- "start_time",
168
- "target_table_name",
169
- "target_column_name",
178
+ CONFIG_START_TIME,
179
+ TARGET_TABLE_NAME,
180
+ TARGET_COLUMN_NAME,
170
181
  "difference",
171
- "primary_keys",
172
- "group_by_columns",
182
+ CONFIG_PRIMARY_KEYS,
183
+ GROUP_BY_COLUMNS,
173
184
  "num_random_rows",
174
185
  ]
175
186
  SCHEMA_VALIDATION_COLUMN_FILTER_LIST = [
176
- "start_time",
177
- "end_time",
178
- "aggregation_type",
187
+ CONFIG_START_TIME,
188
+ CONFIG_END_TIME,
189
+ AGGREGATION_TYPE,
179
190
  "difference",
180
- "primary_keys",
181
- "group_by_columns",
191
+ CONFIG_PRIMARY_KEYS,
192
+ GROUP_BY_COLUMNS,
182
193
  "num_random_rows",
183
194
  "pct_threshold",
184
195
  ]
@@ -353,7 +353,10 @@ class DataValidation(object):
353
353
  target_df = futures[1].result()
354
354
 
355
355
  pandas_client = ibis.pandas.connect(
356
- {combiner.DEFAULT_SOURCE: source_df, combiner.DEFAULT_TARGET: target_df}
356
+ {
357
+ consts.RESULT_TYPE_SOURCE: source_df,
358
+ consts.RESULT_TYPE_TARGET: target_df,
359
+ }
357
360
  )
358
361
 
359
362
  try:
@@ -362,8 +365,8 @@ class DataValidation(object):
362
365
  combiner.generate_report,
363
366
  pandas_client,
364
367
  self.run_metadata,
365
- pandas_client.table(combiner.DEFAULT_SOURCE),
366
- pandas_client.table(combiner.DEFAULT_TARGET),
368
+ pandas_client.table(consts.RESULT_TYPE_SOURCE),
369
+ pandas_client.table(consts.RESULT_TYPE_TARGET),
367
370
  join_on_fields=join_on_fields,
368
371
  is_value_comparison=is_value_comparison,
369
372
  verbose=self.verbose,
@@ -113,7 +113,7 @@ class BigQueryResultHandler(object):
113
113
  else:
114
114
  logging.info(f'{BQRH_WRITE_MESSAGE}, run id: {result_df.iloc[0]["run_id"]}')
115
115
 
116
- # Handler also logs results after saving to BigQuery.
116
+ # Handler can also output results after saving to BigQuery.
117
117
  logger = logging.getLogger()
118
118
  if logger.isEnabledFor(logging.DEBUG):
119
119
  # Checking log level to avoid evaluating a large Dataframe that will never be logged.
@@ -53,6 +53,10 @@ class GCPSecretManager:
53
53
  # Return the decoded payload.
54
54
  payload = response.payload.data.decode("UTF-8")
55
55
  return payload
56
- except (exceptions.NotFound, exceptions.InvalidArgument):
57
- # The secret does not exist and therefore we can assume it is just a token to be returned.
56
+ except (
57
+ exceptions.NotFound,
58
+ exceptions.InvalidArgument,
59
+ exceptions.PermissionDenied,
60
+ ):
61
+ # The secret does not exist or is inaccessible and therefore we can assume it is just a token to be returned.
58
62
  return secret_id
@@ -12,10 +12,13 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ import json
15
16
  import logging
16
17
  import re
17
18
  import time
18
19
 
20
+ from data_validation import exceptions
21
+
19
22
 
20
23
  def timed_call(log_txt, fn, *args, **kwargs):
21
24
  t0 = time.time()
@@ -44,3 +47,18 @@ def split_not_in_quotes(
44
47
  return [t for t in re.split(pattern, to_split) if t]
45
48
  else:
46
49
  return re.split(pattern, to_split)
50
+
51
+
52
+ def dvt_config_string_to_dict(config_string: str) -> dict:
53
+ """Convert JSON in a string to a dict."""
54
+ if not config_string:
55
+ return None
56
+ if isinstance(config_string, dict):
57
+ return config_string
58
+ try:
59
+ param_dict = json.loads(config_string.replace("'", '"'))
60
+ return param_dict
61
+ except json.JSONDecodeError as exc:
62
+ raise exceptions.ValidationException(
63
+ f"Invalid JSON format in connection parameter dictionary string: {config_string}"
64
+ ) from exc
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: google-pso-data-validator
3
- Version: 7.2.0
3
+ Version: 7.3.0
4
4
  Summary: A package to enable easy data validation
5
5
  Author: PSO DVT Engineering team
6
6
  Author-email: data-validator-eng@google.com
@@ -18,7 +18,7 @@ import setuptools
18
18
 
19
19
  name = "google-pso-data-validator"
20
20
  description = "A package to enable easy data validation"
21
- version = "7.2.0"
21
+ version = "7.3.0"
22
22
  release_status = "Development Status :: 4 - Beta"
23
23
 
24
24
  with open("README.md", "r") as fh:
@@ -12,7 +12,10 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  import sqlalchemy as sa
15
- from sqlalchemy.dialects.oracle.base import OracleIdentifierPreparer
15
+ from sqlalchemy.dialects.oracle.base import (
16
+ OracleIdentifierPreparer,
17
+ RESERVED_WORDS as ORACLE_RESERVED_WORDS,
18
+ )
16
19
  from sqlalchemy.dialects.oracle.cx_oracle import OracleDialect_cx_oracle
17
20
  import re
18
21
 
@@ -23,6 +26,13 @@ from third_party.ibis.ibis_oracle.compiler import OracleCompiler
23
26
  from third_party.ibis.ibis_oracle.datatypes import _get_type
24
27
 
25
28
 
29
+ EXTRA_RESERVED_WORDS = set(
30
+ [
31
+ "COLUMN",
32
+ ]
33
+ )
34
+
35
+
26
36
  def _ora_denormalize_name(self, name):
27
37
  """Oracle specific version of sqlalchemy/engine/default.py.denormalize_name()
28
38
 
@@ -47,6 +57,11 @@ OracleDialect_cx_oracle.denormalize_name = _ora_denormalize_name
47
57
 
48
58
 
49
59
  class DVTOracleIdentifierPreparer(OracleIdentifierPreparer):
60
+
61
+ reserved_words = {
62
+ x.lower() for x in ORACLE_RESERVED_WORDS.union(EXTRA_RESERVED_WORDS)
63
+ }
64
+
50
65
  def quote_identifier(self, value):
51
66
  """Quote an identifier.
52
67
 
@@ -22,6 +22,7 @@ import snowflake.sqlalchemy
22
22
 
23
23
  logging.getLogger("snowflake.connector").setLevel(logging.WARNING)
24
24
 
25
+ from data_validation.util import dvt_config_string_to_dict
25
26
  import third_party.ibis.ibis_snowflake.datatypes
26
27
 
27
28
 
@@ -30,8 +31,10 @@ def snowflake_connect(
30
31
  password: str,
31
32
  account: str,
32
33
  database: str,
33
- connect_args: Mapping[str, Any] = None,
34
+ connect_args: str = None,
34
35
  ):
36
+ if connect_args:
37
+ connect_args = dvt_config_string_to_dict(connect_args)
35
38
  return ibis.snowflake.connect(
36
39
  user=user,
37
40
  password=password,
@@ -14,13 +14,13 @@
14
14
  import pandas
15
15
  import warnings
16
16
 
17
- import json
18
17
  import teradatasql
19
18
  import ibis.expr.datatypes as dt
20
19
  import ibis.expr.types as ir
21
20
  from typing import Mapping, Any
22
21
  import ibis.expr.schema as sch
23
22
  from ibis.backends.base.sql import BaseSQLBackend
23
+
24
24
  from third_party.ibis.ibis_teradata.compiler import TeradataCompiler
25
25
  from third_party.ibis.ibis_teradata.datatypes import (
26
26
  TeradataTypeTranslator,
@@ -41,7 +41,7 @@ class Backend(BaseSQLBackend):
41
41
  port: int = 1025,
42
42
  logmech: str = "TD2",
43
43
  use_no_lock_tables: str = "False",
44
- json_params: str = None,
44
+ json_params: Mapping[str, Any] = None,
45
45
  ) -> None:
46
46
  self.teradata_config = {
47
47
  "host": host,
@@ -50,15 +50,8 @@ class Backend(BaseSQLBackend):
50
50
  "dbs_port": port,
51
51
  "logmech": logmech,
52
52
  }
53
-
54
53
  if json_params:
55
- try:
56
- param_dict = json.loads(json_params.replace("'", '"'))
57
- self.teradata_config.update(param_dict)
58
- except json.JSONDecodeError:
59
- print(
60
- f"Invalid JSON format in the parameter dictionary string: {json_params}"
61
- )
54
+ self.teradata_config.update(json_params)
62
55
 
63
56
  self.client = teradatasql.connect(**self.teradata_config)
64
57
  self.con = self.client.cursor()
@@ -14,6 +14,8 @@
14
14
  from third_party.ibis.ibis_teradata import Backend as TeradataBackend
15
15
  import teradatasql # NOQA fail early if the package is missing
16
16
 
17
+ from data_validation.util import dvt_config_string_to_dict
18
+
17
19
 
18
20
  def teradata_connect(
19
21
  host: str = "localhost",
@@ -25,6 +27,9 @@ def teradata_connect(
25
27
  json_params: str = None,
26
28
  ):
27
29
  backend = TeradataBackend()
30
+ if json_params:
31
+ json_params = dvt_config_string_to_dict(json_params)
32
+
28
33
  backend.do_connect(
29
34
  host=host,
30
35
  user_name=user_name,