google-pso-data-validator 7.1.0__tar.gz → 7.2.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 (91) hide show
  1. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/PKG-INFO +9 -1
  2. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/__main__.py +4 -0
  3. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/cli_tools.py +61 -20
  4. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/combiner.py +6 -2
  5. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/config_manager.py +23 -6
  6. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/gcs_helper.py +22 -0
  7. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/state_manager.py +25 -1
  8. google_pso_data_validator-7.2.0/data_validation/util.py +46 -0
  9. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/google_pso_data_validator.egg-info/PKG-INFO +9 -1
  10. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/google_pso_data_validator.egg-info/SOURCES.txt +1 -0
  11. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/google_pso_data_validator.egg-info/requires.txt +9 -0
  12. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/setup.py +10 -1
  13. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_addon/operations.py +66 -108
  14. google_pso_data_validator-7.2.0/third_party/ibis/ibis_mssql/registry.py +98 -0
  15. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_oracle/registry.py +2 -1
  16. google_pso_data_validator-7.2.0/third_party/ibis/ibis_postgres/registry.py +28 -0
  17. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_teradata/registry.py +3 -3
  18. google_pso_data_validator-7.1.0/data_validation/util.py +0 -24
  19. google_pso_data_validator-7.1.0/third_party/ibis/ibis_mssql/registry.py +0 -49
  20. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/LICENSE +0 -0
  21. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/README.md +0 -0
  22. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/__init__.py +0 -0
  23. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/app.py +0 -0
  24. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/client_info.py +0 -0
  25. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/clients.py +0 -0
  26. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/consts.py +0 -0
  27. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/data_validation.py +0 -0
  28. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/exceptions.py +0 -0
  29. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/find_tables.py +0 -0
  30. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/jellyfish_distance.py +0 -0
  31. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/metadata.py +0 -0
  32. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/partition_builder.py +0 -0
  33. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/query_builder/__init__.py +0 -0
  34. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/query_builder/partition_row_builder.py +0 -0
  35. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/query_builder/query_builder.py +0 -0
  36. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/query_builder/random_row_builder.py +0 -0
  37. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/raw_query.py +0 -0
  38. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/result_handlers/__init__.py +0 -0
  39. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/result_handlers/bigquery.py +0 -0
  40. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/result_handlers/text.py +0 -0
  41. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/schema_validation.py +0 -0
  42. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/secret_manager.py +0 -0
  43. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/data_validation/validation_builder.py +0 -0
  44. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/google_pso_data_validator.egg-info/dependency_links.txt +0 -0
  45. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/google_pso_data_validator.egg-info/entry_points.txt +0 -0
  46. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/google_pso_data_validator.egg-info/top_level.txt +0 -0
  47. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/setup.cfg +0 -0
  48. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_addon/__init__.py +0 -0
  49. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_addon/api.py +0 -0
  50. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_biquery/__init__.py +0 -0
  51. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_biquery/api.py +0 -0
  52. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_cloud_spanner/__init__.py +0 -0
  53. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_cloud_spanner/api.py +0 -0
  54. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_cloud_spanner/client.py +0 -0
  55. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_cloud_spanner/compiler.py +0 -0
  56. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_cloud_spanner/datatypes.py +0 -0
  57. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_cloud_spanner/registry.py +0 -0
  58. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_cloud_spanner/tests/__init__.py +0 -0
  59. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_cloud_spanner/tests/conftest.py +0 -0
  60. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_cloud_spanner/to_pandas.py +0 -0
  61. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_db2/__init__.py +0 -0
  62. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_db2/api.py +0 -0
  63. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_db2/compiler.py +0 -0
  64. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_db2/datatypes.py +0 -0
  65. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_db2/registry.py +0 -0
  66. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_impala/__init__.py +0 -0
  67. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_impala/api.py +0 -0
  68. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_mssql/__init__.py +0 -0
  69. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_mssql/api.py +0 -0
  70. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_mssql/datatypes.py +0 -0
  71. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_mysql/__init__.py +0 -0
  72. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_mysql/base_sql_compiler/__init__.py +0 -0
  73. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_mysql/base_sql_compiler/select_builder.py +0 -0
  74. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_mysql/compiler.py +0 -0
  75. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_oracle/__init__.py +0 -0
  76. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_oracle/api.py +0 -0
  77. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_oracle/compiler.py +0 -0
  78. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_oracle/datatypes.py +0 -0
  79. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_postgres/__init__.py +0 -0
  80. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_postgres/client.py +0 -0
  81. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_postgres/datatypes.py +0 -0
  82. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_redshift/__init__.py +0 -0
  83. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_redshift/api.py +0 -0
  84. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_redshift/compiler.py +0 -0
  85. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_snowflake/__init__.py +0 -0
  86. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_snowflake/api.py +0 -0
  87. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_snowflake/datatypes.py +0 -0
  88. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_teradata/__init__.py +0 -0
  89. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_teradata/api.py +0 -0
  90. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_teradata/compiler.py +0 -0
  91. {google_pso_data_validator-7.1.0 → google_pso_data_validator-7.2.0}/third_party/ibis/ibis_teradata/datatypes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: google-pso-data-validator
3
- Version: 7.1.0
3
+ Version: 7.2.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
@@ -38,6 +38,14 @@ Provides-Extra: apache-airflow
38
38
  Requires-Dist: 1.10.11; extra == "apache-airflow"
39
39
  Provides-Extra: pyspark
40
40
  Requires-Dist: 3.0.0; extra == "pyspark"
41
+ Provides-Extra: develop
42
+ Requires-Dist: black==22.3.0; extra == "develop"
43
+ Requires-Dist: flake8; extra == "develop"
44
+ Requires-Dist: freezegun; extra == "develop"
45
+ Requires-Dist: pyfakefs==4.6.2; extra == "develop"
46
+ Requires-Dist: pytest; extra == "develop"
47
+ Requires-Dist: pytest-cov; extra == "develop"
48
+ Requires-Dist: pytest-timeout; extra == "develop"
41
49
  Dynamic: author
42
50
  Dynamic: author-email
43
51
  Dynamic: classifier
@@ -640,6 +640,10 @@ def run_connections(args):
640
640
  """Run commands related to connection management."""
641
641
  if args.connect_cmd == "list":
642
642
  cli_tools.list_connections()
643
+ elif args.connect_cmd == "delete":
644
+ cli_tools.delete_connection(args.connection_name)
645
+ elif args.connect_cmd == "describe":
646
+ cli_tools.describe_connection(args.connection_name, args.output_format)
643
647
  elif args.connect_cmd == "add":
644
648
  conn = cli_tools.get_connection_config_from_args(args)
645
649
  # Test getting a client to validate connection details
@@ -52,12 +52,18 @@ import sys
52
52
  import uuid
53
53
  import os
54
54
  import math
55
- import re
56
55
  from argparse import Namespace
57
56
  from typing import Dict, List, Optional
58
57
  from yaml import Dumper, Loader, dump, load
59
58
 
60
- from data_validation import clients, consts, find_tables, state_manager, gcs_helper
59
+ from data_validation import (
60
+ clients,
61
+ consts,
62
+ find_tables,
63
+ state_manager,
64
+ gcs_helper,
65
+ util,
66
+ )
61
67
  from data_validation.validation_builder import list_to_sublists
62
68
 
63
69
 
@@ -468,6 +474,27 @@ def _configure_connection_parser(subparsers):
468
474
  )
469
475
  _configure_database_specific_parsers(add_parser)
470
476
 
477
+ delete_parser = connect_subparsers.add_parser(
478
+ "delete", help="Delete an existing connection"
479
+ )
480
+ delete_parser.add_argument(
481
+ "--connection-name", "-c", required=True, help="Name of connection to delete"
482
+ )
483
+ describe_parser = connect_subparsers.add_parser(
484
+ "describe", help="Describe an existing connection"
485
+ )
486
+ describe_parser.add_argument(
487
+ "--connection-name", "-c", required=True, help="Name of connection to describe"
488
+ )
489
+ describe_parser.add_argument(
490
+ "--format",
491
+ "-f",
492
+ dest="output_format",
493
+ choices=["json", "yaml"],
494
+ default="yaml",
495
+ help="Output format for the configuration (default: yaml)",
496
+ )
497
+
471
498
 
472
499
  def _configure_database_specific_parsers(parser):
473
500
  """Configure a separate subparser for each supported DB."""
@@ -1085,13 +1112,29 @@ def store_connection(connection_name, conn):
1085
1112
  mgr.create_connection(connection_name, conn)
1086
1113
 
1087
1114
 
1115
+ def delete_connection(connection_name):
1116
+ """Delete the connection config under the given name."""
1117
+ mgr = state_manager.StateManager()
1118
+ mgr.delete_connection(connection_name)
1119
+
1120
+
1088
1121
  def list_connections():
1089
1122
  """List all saved connections."""
1090
1123
  mgr = state_manager.StateManager()
1091
1124
  connections = mgr.list_connections()
1092
1125
  for conn_name in connections:
1093
1126
  source_type = mgr.get_connection_config(conn_name).get("source_type")
1094
- logging.info(f"Connection Name: {conn_name} : {source_type}")
1127
+ print(f"Connection Name: {conn_name}")
1128
+ print(f"Source Type: {source_type}\n")
1129
+ return connections
1130
+
1131
+
1132
+ def describe_connection(connection_name, output_format):
1133
+ """Return yaml connection details for a specific connection"""
1134
+ mgr = state_manager.StateManager()
1135
+ connection_details = mgr.describe_connection(connection_name, output_format)
1136
+ print(connection_details)
1137
+ return connection_details
1095
1138
 
1096
1139
 
1097
1140
  def get_connection(connection_name):
@@ -1165,31 +1208,29 @@ def get_filters(filter_value: str) -> List[Dict]:
1165
1208
  If only one filter is specified, it applies to both source and target
1166
1209
  For a doc on regular expression for filters see docs/internal/filters_regex.md
1167
1210
  """
1168
-
1169
- single_filter = r"([^':]*('[^']*')*)*"
1170
- double_filter = (
1171
- r"(?P<source>" + single_filter + r"):(?P<target>" + single_filter + r")"
1172
- )
1173
- filter_config = []
1174
- if result := re.fullmatch(single_filter, filter_value):
1175
- if result.group(0) == "":
1211
+ filters = util.split_not_in_quotes(filter_value, ":")
1212
+ if len(filters) not in (1, 2):
1213
+ raise argparse.ArgumentTypeError("Unable to parse filter arguments.")
1214
+ filters = [_.strip() for _ in filters]
1215
+ if len(filters) == 1:
1216
+ if not filters[0]:
1176
1217
  raise argparse.ArgumentTypeError("Empty string not allowed in filter")
1177
1218
  filter_dict = {
1178
1219
  "type": "custom",
1179
- "source": result.group(0),
1180
- "target": result.group(0),
1220
+ "source": filters[0],
1221
+ "target": filters[0],
1181
1222
  }
1182
- elif result := re.fullmatch(double_filter, filter_value):
1183
- if result.group("source") == "" or result.group("target") == "":
1223
+ elif len(filters) == 2:
1224
+ if not filters[0] or not filters[1]:
1184
1225
  raise argparse.ArgumentTypeError("Empty string not allowed in filter")
1185
1226
  filter_dict = {
1186
1227
  "type": "custom",
1187
- "source": result.group("source"),
1188
- "target": result.group("target"),
1228
+ "source": filters[0],
1229
+ "target": filters[1],
1189
1230
  }
1190
- else:
1191
- raise argparse.ArgumentTypeError("Unable to parse filter arguments.")
1192
- filter_config.append(filter_dict)
1231
+ filter_config = [
1232
+ filter_dict,
1233
+ ]
1193
1234
  return filter_config
1194
1235
 
1195
1236
 
@@ -124,8 +124,12 @@ def _calculate_difference(
124
124
  ):
125
125
  pct_threshold = ibis.literal(validation.threshold)
126
126
  if datatype.is_timestamp() or datatype.is_date():
127
- source_value = field_differences["differences_source_value"].epoch_seconds()
128
- target_value = field_differences["differences_target_value"].epoch_seconds()
127
+ source_value = (
128
+ field_differences["differences_source_value"].epoch_seconds().cast("int64")
129
+ )
130
+ target_value = (
131
+ field_differences["differences_target_value"].epoch_seconds().cast("int64")
132
+ )
129
133
  elif datatype.is_boolean() or (target_type and target_type.is_boolean()):
130
134
  source_value = field_differences["differences_source_value"].cast("boolean")
131
135
  target_value = field_differences["differences_target_value"].cast("boolean")
@@ -784,7 +784,9 @@ class ConfigManager(object):
784
784
  column_position: int,
785
785
  ) -> dict:
786
786
  """Append calculated field for length() or epoch_seconds(timestamp) for preprocessing before column validation aggregation."""
787
- depth, cast_type = 0, None
787
+ depth = 0
788
+ cast_type = None
789
+ final_cast_type = None
788
790
  if any(_ in ["json", "!json"] for _ in [column_type, target_column_type]):
789
791
  # JSON data which needs casting to string before we apply a length function.
790
792
  pre_calculated_config = self.build_and_append_pre_agg_calc_config(
@@ -829,6 +831,10 @@ class ConfigManager(object):
829
831
  depth = 1
830
832
 
831
833
  calc_func = "epoch_seconds"
834
+ if agg_type == consts.CONFIG_TYPE_SUM:
835
+ # It is possible to exceed int64 when summing epoch_seconds therefore cast to string.
836
+ # See issue 1391 for details.
837
+ final_cast_type = "string"
832
838
 
833
839
  elif column_type == "int32" or column_type == "!int32":
834
840
  calc_func = consts.CONFIG_CAST
@@ -856,6 +862,9 @@ class ConfigManager(object):
856
862
  ),
857
863
  consts.CONFIG_TYPE: agg_type,
858
864
  }
865
+ if final_cast_type:
866
+ # Adding to dict this way to avoid adding a lot of empty cast attributes.
867
+ aggregate_config[consts.CONFIG_CAST] = final_cast_type
859
868
 
860
869
  return aggregate_config
861
870
 
@@ -863,22 +872,28 @@ class ConfigManager(object):
863
872
  self,
864
873
  source_column_ibis_type: dt.DataType,
865
874
  target_column_ibis_type: dt.DataType,
875
+ margin: int = 0,
866
876
  ) -> bool:
867
- """Identifies Decimal columns that will cause problems in a Pandas Dataframe, i.e.
868
- is of greater precision than a 64bit int/real can hold."""
877
+ """Identifies Decimal columns that will cause problems in a Pandas Dataframe.
878
+
879
+ i.e. are of greater precision than a 64bit int/real can hold.
880
+
881
+ margin: Allows us to lower the precision threshold. This is helpful when summing column
882
+ values that are okay by themselves but cumulativaly could overflow a 64bit value.
883
+ """
869
884
  return bool(
870
885
  (
871
886
  isinstance(source_column_ibis_type, dt.Decimal)
872
887
  and (
873
888
  source_column_ibis_type.precision is None
874
- or source_column_ibis_type.precision > 18
889
+ or source_column_ibis_type.precision > (18 - margin)
875
890
  )
876
891
  )
877
892
  and (
878
893
  isinstance(target_column_ibis_type, dt.Decimal)
879
894
  and (
880
895
  target_column_ibis_type.precision is None
881
- or target_column_ibis_type.precision > 18
896
+ or target_column_ibis_type.precision > (18 - margin)
882
897
  )
883
898
  )
884
899
  )
@@ -1042,7 +1057,9 @@ class ConfigManager(object):
1042
1057
  consts.CONFIG_TYPE: agg_type,
1043
1058
  }
1044
1059
  if self._decimal_column_too_big_for_pandas(
1045
- source_column_ibis_type, target_column_ibis_type
1060
+ source_column_ibis_type,
1061
+ target_column_ibis_type,
1062
+ margin=(2 if agg_type == consts.CONFIG_TYPE_SUM else 0),
1046
1063
  ):
1047
1064
  aggregate_config[consts.CONFIG_CAST] = "string"
1048
1065
 
@@ -21,6 +21,7 @@ from data_validation import client_info
21
21
 
22
22
 
23
23
  WRITE_SUCCESS_STRING = "Success! Config output written to"
24
+ DELETE_SUCCESS_STRING = "Successfully deleted"
24
25
 
25
26
 
26
27
  def _is_gcs_path(file_path: str) -> bool:
@@ -67,6 +68,13 @@ def _write_gcs_file(file_path: str, data: str):
67
68
  blob.upload_from_string(data)
68
69
 
69
70
 
71
+ def _delete_gcs_file(file_path: str):
72
+ """Delete a file stored in GCS."""
73
+ gcs_bucket = get_gcs_bucket(file_path)
74
+ blob = gcs_bucket.blob(_get_gcs_file_path(file_path))
75
+ blob.delete()
76
+
77
+
70
78
  def read_file(file_path: str, download_as_text: bool = False):
71
79
  if _is_gcs_path(file_path):
72
80
  return _read_gcs_file(file_path, download_as_text)
@@ -87,6 +95,20 @@ def write_file(file_path: str, data: str, include_log: bool = True):
87
95
  logging.info(f"{WRITE_SUCCESS_STRING} {file_path}")
88
96
 
89
97
 
98
+ def delete_file(file_path: str, include_log: bool = True):
99
+ """Delete a file from GCS or local filesystem, depending on the path."""
100
+ if _is_gcs_path(file_path):
101
+ _delete_gcs_file(file_path)
102
+ else:
103
+ if os.path.exists(file_path):
104
+ os.remove(file_path)
105
+ else:
106
+ raise FileNotFoundError(f"File not found: {file_path}")
107
+
108
+ if include_log:
109
+ logging.info(f"{DELETE_SUCCESS_STRING}: {file_path}")
110
+
111
+
90
112
  def list_gcs_directory(directory_path: str) -> List[str]:
91
113
  gcs_prefix = _get_gcs_file_path(directory_path)
92
114
  gcs_bucket = get_gcs_bucket(directory_path)
@@ -19,8 +19,9 @@ and validation files.
19
19
 
20
20
  import enum
21
21
  import json
22
+ import yaml
22
23
  import os
23
- from typing import Dict, List
24
+ from typing import Dict, List, Union
24
25
 
25
26
  from data_validation import consts, gcs_helper
26
27
 
@@ -59,6 +60,15 @@ class StateManager(object):
59
60
  connection_path = self._get_connection_path(name)
60
61
  gcs_helper.write_file(connection_path, json.dumps(config))
61
62
 
63
+ def delete_connection(self, name: str):
64
+ """Delete an existing connection file by its connection name.
65
+
66
+ Args:
67
+ name (String): The name of the connection.
68
+ """
69
+ connection_path = self._get_connection_path(name)
70
+ gcs_helper.delete_file(connection_path)
71
+
62
72
  def get_connection_config(self, name: str) -> Dict[str, str]:
63
73
  """Get a connection configuration from the expected file.
64
74
 
@@ -72,6 +82,20 @@ class StateManager(object):
72
82
 
73
83
  return json.loads(conn_str)
74
84
 
85
+ def describe_connection(
86
+ self, connection_name: str, output_format: str
87
+ ) -> Union[dict, str]:
88
+ """Describe an existing connection by returning connection details in yaml format.
89
+
90
+ Args:
91
+ args (): The name of the connection.
92
+ """
93
+ conn_data = self.get_connection_config(connection_name)
94
+ if output_format == "json":
95
+ return conn_data
96
+ elif output_format == "yaml":
97
+ return yaml.dump(conn_data, default_flow_style=False, sort_keys=False)
98
+
75
99
  def list_connections(self) -> List[str]:
76
100
  """Returns a list of the connection names that exist."""
77
101
  file_names = self._list_directory(self._get_connections_directory())
@@ -0,0 +1,46 @@
1
+ # Copyright 2024 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import logging
16
+ import re
17
+ import time
18
+
19
+
20
+ def timed_call(log_txt, fn, *args, **kwargs):
21
+ t0 = time.time()
22
+ result = fn(*args, **kwargs)
23
+ elapsed = time.time() - t0
24
+ logging.debug(f"{log_txt} elapsed: {round(elapsed, 2)}s")
25
+ return result
26
+
27
+
28
+ def split_not_in_quotes(
29
+ to_split: str, sep: str = " ", exclude_empty_tokens: bool = False
30
+ ) -> list:
31
+ """Split a string by a separator but only when the separator is not inside quotes.
32
+ re pattern taken from this comment:
33
+ https://stackoverflow.com/a/2787979/10979853
34
+ The commenter's words should the link ever go stale:
35
+ Each time it finds a semicolon, the lookahead scans the entire remaining string,
36
+ making sure there's an even number of single-quotes and an even number of double-quotes.
37
+ (Single-quotes inside double-quoted fields, or vice-versa, are ignored.) If the
38
+ lookahead succeeds, the semicolon is a delimiter.
39
+ The pattern doesn't cope with whitespace as sep, back to back spaces are multiple seps, therefore
40
+ we have exclude_empty_tokens parameter.
41
+ """
42
+ pattern = r"""%(sep)s(?=(?:[^'"]|'[^']*'|"[^"]*")*$)""" % {"sep": sep}
43
+ if exclude_empty_tokens:
44
+ return [t for t in re.split(pattern, to_split) if t]
45
+ else:
46
+ return re.split(pattern, to_split)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: google-pso-data-validator
3
- Version: 7.1.0
3
+ Version: 7.2.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
@@ -38,6 +38,14 @@ Provides-Extra: apache-airflow
38
38
  Requires-Dist: 1.10.11; extra == "apache-airflow"
39
39
  Provides-Extra: pyspark
40
40
  Requires-Dist: 3.0.0; extra == "pyspark"
41
+ Provides-Extra: develop
42
+ Requires-Dist: black==22.3.0; extra == "develop"
43
+ Requires-Dist: flake8; extra == "develop"
44
+ Requires-Dist: freezegun; extra == "develop"
45
+ Requires-Dist: pyfakefs==4.6.2; extra == "develop"
46
+ Requires-Dist: pytest; extra == "develop"
47
+ Requires-Dist: pytest-cov; extra == "develop"
48
+ Requires-Dist: pytest-timeout; extra == "develop"
41
49
  Dynamic: author
42
50
  Dynamic: author-email
43
51
  Dynamic: classifier
@@ -74,6 +74,7 @@ third_party/ibis/ibis_oracle/registry.py
74
74
  third_party/ibis/ibis_postgres/__init__.py
75
75
  third_party/ibis/ibis_postgres/client.py
76
76
  third_party/ibis/ibis_postgres/datatypes.py
77
+ third_party/ibis/ibis_postgres/registry.py
77
78
  third_party/ibis/ibis_redshift/__init__.py
78
79
  third_party/ibis/ibis_redshift/api.py
79
80
  third_party/ibis/ibis_redshift/compiler.py
@@ -22,5 +22,14 @@ tabulate>=0.9.0
22
22
  [apache-airflow]
23
23
  1.10.11
24
24
 
25
+ [develop]
26
+ black==22.3.0
27
+ flake8
28
+ freezegun
29
+ pyfakefs==4.6.2
30
+ pytest
31
+ pytest-cov
32
+ pytest-timeout
33
+
25
34
  [pyspark]
26
35
  3.0.0
@@ -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.1.0"
21
+ version = "7.2.0"
22
22
  release_status = "Development Status :: 4 - Beta"
23
23
 
24
24
  with open("README.md", "r") as fh:
@@ -50,6 +50,15 @@ dependencies = [
50
50
  extras_require = {
51
51
  "apache-airflow": "1.10.11",
52
52
  "pyspark": "3.0.0",
53
+ "develop": [
54
+ "black==22.3.0",
55
+ "flake8",
56
+ "freezegun",
57
+ "pyfakefs==4.6.2",
58
+ "pytest",
59
+ "pytest-cov",
60
+ "pytest-timeout",
61
+ ],
53
62
  }
54
63
 
55
64
  packages = [