google-pso-data-validator 7.3.0__tar.gz → 7.5.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.
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/PKG-INFO +5 -2
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/README.md +2 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/__main__.py +8 -5
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/cli_tools.py +17 -7
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/combiner.py +156 -63
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/config_manager.py +67 -16
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/consts.py +10 -2
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/data_validation.py +37 -65
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/partition_builder.py +21 -14
- google_pso_data_validator-7.5.0/data_validation/raw_query.py +74 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/result_handlers/bigquery.py +3 -3
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/result_handlers/text.py +6 -4
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/google_pso_data_validator.egg-info/PKG-INFO +5 -2
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/setup.py +1 -1
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_oracle/__init__.py +35 -1
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_postgres/client.py +51 -1
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_postgres/registry.py +8 -2
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_teradata/__init__.py +30 -0
- google_pso_data_validator-7.3.0/data_validation/raw_query.py +0 -47
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/LICENSE +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/__init__.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/app.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/client_info.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/clients.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/exceptions.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/find_tables.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/gcs_helper.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/jellyfish_distance.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/metadata.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/query_builder/__init__.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/query_builder/partition_row_builder.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/query_builder/query_builder.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/query_builder/random_row_builder.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/result_handlers/__init__.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/schema_validation.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/secret_manager.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/state_manager.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/util.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/validation_builder.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/google_pso_data_validator.egg-info/SOURCES.txt +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/google_pso_data_validator.egg-info/dependency_links.txt +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/google_pso_data_validator.egg-info/entry_points.txt +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/google_pso_data_validator.egg-info/requires.txt +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/google_pso_data_validator.egg-info/top_level.txt +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/setup.cfg +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_addon/__init__.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_addon/api.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_addon/operations.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_biquery/__init__.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_biquery/api.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/__init__.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/api.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/client.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/compiler.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/datatypes.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/registry.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/tests/__init__.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/tests/conftest.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_cloud_spanner/to_pandas.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_db2/__init__.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_db2/api.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_db2/compiler.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_db2/datatypes.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_db2/registry.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_impala/__init__.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_impala/api.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_mssql/__init__.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_mssql/api.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_mssql/datatypes.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_mssql/registry.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_mysql/__init__.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_mysql/base_sql_compiler/__init__.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_mysql/base_sql_compiler/select_builder.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_mysql/compiler.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_oracle/api.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_oracle/compiler.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_oracle/datatypes.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_oracle/registry.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_postgres/__init__.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_postgres/datatypes.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_redshift/__init__.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_redshift/api.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_redshift/compiler.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_snowflake/__init__.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_snowflake/api.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_snowflake/datatypes.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_teradata/api.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_teradata/compiler.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_teradata/datatypes.py +0 -0
- {google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/third_party/ibis/ibis_teradata/registry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: google-pso-data-validator
|
|
3
|
-
Version: 7.
|
|
3
|
+
Version: 7.5.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
|
|
@@ -51,6 +51,7 @@ Dynamic: author-email
|
|
|
51
51
|
Dynamic: classifier
|
|
52
52
|
Dynamic: description
|
|
53
53
|
Dynamic: description-content-type
|
|
54
|
+
Dynamic: license-file
|
|
54
55
|
Dynamic: provides-extra
|
|
55
56
|
Dynamic: requires-dist
|
|
56
57
|
Dynamic: requires-python
|
|
@@ -651,6 +652,8 @@ data-validation query
|
|
|
651
652
|
The connection name to be queried
|
|
652
653
|
--query or -q QUERY
|
|
653
654
|
The raw query to run against the supplied connection
|
|
655
|
+
[--format or -f {minimal,python}]
|
|
656
|
+
Format for query output (default: python)
|
|
654
657
|
```
|
|
655
658
|
|
|
656
659
|
### Building Matched Table Lists
|
|
@@ -593,6 +593,8 @@ data-validation query
|
|
|
593
593
|
The connection name to be queried
|
|
594
594
|
--query or -q QUERY
|
|
595
595
|
The raw query to run against the supplied connection
|
|
596
|
+
[--format or -f {minimal,python}]
|
|
597
|
+
Format for query output (default: python)
|
|
596
598
|
```
|
|
597
599
|
|
|
598
600
|
### Building Matched Table Lists
|
{google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/__main__.py
RENAMED
|
@@ -16,9 +16,9 @@ import json
|
|
|
16
16
|
import logging
|
|
17
17
|
import os
|
|
18
18
|
import sys
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
19
20
|
|
|
20
21
|
from yaml import Dumper, dump
|
|
21
|
-
from argparse import Namespace
|
|
22
22
|
from typing import List
|
|
23
23
|
from data_validation import (
|
|
24
24
|
cli_tools,
|
|
@@ -34,6 +34,9 @@ from data_validation.data_validation import DataValidation
|
|
|
34
34
|
from data_validation.find_tables import find_tables_using_string_matching
|
|
35
35
|
from data_validation.partition_builder import PartitionBuilder
|
|
36
36
|
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from argparse import Namespace
|
|
39
|
+
|
|
37
40
|
# by default yaml dumps lists as pointers. This disables that feature
|
|
38
41
|
Dumper.ignore_aliases = lambda *args: True
|
|
39
42
|
|
|
@@ -246,7 +249,7 @@ def _get_comparison_config(
|
|
|
246
249
|
return config_manager.build_config_comparison_fields(comparison_fields)
|
|
247
250
|
|
|
248
251
|
|
|
249
|
-
def build_config_from_args(args: Namespace, config_manager: ConfigManager):
|
|
252
|
+
def build_config_from_args(args: "Namespace", config_manager: ConfigManager):
|
|
250
253
|
"""This function is used to append build configs to the config manager for all validation commands and generate-table-partitions.
|
|
251
254
|
Instead of having two separate commands, e.g. validate row and validate custom-query row, generate-table-partitions
|
|
252
255
|
uses implicit choice of table or custom-query. A user can specify either tables or source/target query/file,
|
|
@@ -337,7 +340,7 @@ def build_config_from_args(args: Namespace, config_manager: ConfigManager):
|
|
|
337
340
|
|
|
338
341
|
|
|
339
342
|
def build_config_managers_from_args(
|
|
340
|
-
args: Namespace, validate_cmd: str = None
|
|
343
|
+
args: "Namespace", validate_cmd: str = None
|
|
341
344
|
) -> List[ConfigManager]:
|
|
342
345
|
"""Return a list of config managers ready to execute."""
|
|
343
346
|
|
|
@@ -596,7 +599,7 @@ def store_json_config_file(args, config_managers):
|
|
|
596
599
|
cli_tools.store_validation(config_file_path, json_config)
|
|
597
600
|
|
|
598
601
|
|
|
599
|
-
def partition_and_store_config_files(args: Namespace) -> None:
|
|
602
|
+
def partition_and_store_config_files(args: "Namespace") -> None:
|
|
600
603
|
"""Build multiple YAML Config files using user specified partition logic
|
|
601
604
|
|
|
602
605
|
Args:
|
|
@@ -691,7 +694,7 @@ def main():
|
|
|
691
694
|
print(find_tables_using_string_matching(args))
|
|
692
695
|
elif args.command == "query":
|
|
693
696
|
raw_query.print_raw_query_output(
|
|
694
|
-
raw_query.run_raw_query_against_connection(args)
|
|
697
|
+
raw_query.run_raw_query_against_connection(args), format=args.output_format
|
|
695
698
|
)
|
|
696
699
|
elif args.command == "validate":
|
|
697
700
|
validate(args)
|
{google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/cli_tools.py
RENAMED
|
@@ -52,8 +52,7 @@ import sys
|
|
|
52
52
|
import uuid
|
|
53
53
|
import os
|
|
54
54
|
import math
|
|
55
|
-
from
|
|
56
|
-
from typing import Dict, List, Optional
|
|
55
|
+
from typing import Dict, List, Optional, TYPE_CHECKING
|
|
57
56
|
from yaml import Dumper, Loader, dump, load
|
|
58
57
|
|
|
59
58
|
from data_validation import (
|
|
@@ -66,6 +65,9 @@ from data_validation import (
|
|
|
66
65
|
)
|
|
67
66
|
from data_validation.validation_builder import list_to_sublists
|
|
68
67
|
|
|
68
|
+
if TYPE_CHECKING:
|
|
69
|
+
from argparse import Namespace
|
|
70
|
+
|
|
69
71
|
|
|
70
72
|
CONNECTION_SOURCE_FIELDS = {
|
|
71
73
|
"BigQuery": [
|
|
@@ -191,7 +193,7 @@ VALIDATE_SCHEMA_HELP_TEXT = "Run a schema validation"
|
|
|
191
193
|
VALIDATE_CUSTOM_QUERY_HELP_TEXT = "Run a custom query validation"
|
|
192
194
|
|
|
193
195
|
|
|
194
|
-
def _check_custom_query_args(parser: argparse.ArgumentParser, parsed_args: Namespace):
|
|
196
|
+
def _check_custom_query_args(parser: argparse.ArgumentParser, parsed_args: "Namespace"):
|
|
195
197
|
# This is where we make additional checks if the arguments provided are what we expect
|
|
196
198
|
# For example, only one of -tbls and custom query options can be provided
|
|
197
199
|
if hasattr(parsed_args, "tables_list") and hasattr(
|
|
@@ -223,7 +225,7 @@ def _check_custom_query_args(parser: argparse.ArgumentParser, parsed_args: Names
|
|
|
223
225
|
return # old format - only one of them is present
|
|
224
226
|
|
|
225
227
|
|
|
226
|
-
def get_parsed_args() -> Namespace:
|
|
228
|
+
def get_parsed_args() -> "Namespace":
|
|
227
229
|
"""Return ArgParser with configured CLI arguments."""
|
|
228
230
|
parser = configure_arg_parser()
|
|
229
231
|
args = ["--help"] if len(sys.argv) == 1 else None
|
|
@@ -393,6 +395,14 @@ def _configure_raw_query(subparsers):
|
|
|
393
395
|
)
|
|
394
396
|
query_parser.add_argument("--conn", "-c", help="Connection name to query")
|
|
395
397
|
query_parser.add_argument("--query", "-q", help="Raw query to execute")
|
|
398
|
+
query_parser.add_argument(
|
|
399
|
+
"--format",
|
|
400
|
+
"-f",
|
|
401
|
+
dest="output_format",
|
|
402
|
+
choices=consts.RAW_QUERY_FORMAT_TYPES,
|
|
403
|
+
default=consts.FORMAT_TYPE_PYTHON,
|
|
404
|
+
help=f"Format for query output (default: {consts.FORMAT_TYPE_PYTHON})",
|
|
405
|
+
)
|
|
396
406
|
|
|
397
407
|
|
|
398
408
|
def _configure_validation_config_parser(subparsers):
|
|
@@ -1027,7 +1037,7 @@ def _add_common_arguments(
|
|
|
1027
1037
|
optional_arguments.add_argument(
|
|
1028
1038
|
"--format",
|
|
1029
1039
|
"-fmt",
|
|
1030
|
-
default=
|
|
1040
|
+
default=consts.FORMAT_TYPE_TABLE,
|
|
1031
1041
|
help="Set the format for printing command output, Supported formats are (text, csv, json, table). Defaults "
|
|
1032
1042
|
"to table",
|
|
1033
1043
|
)
|
|
@@ -1443,7 +1453,7 @@ def _concat_column_count_configs(
|
|
|
1443
1453
|
return return_list
|
|
1444
1454
|
|
|
1445
1455
|
|
|
1446
|
-
def get_pre_build_configs(args: Namespace, validate_cmd: str) -> List[Dict]:
|
|
1456
|
+
def get_pre_build_configs(args: "Namespace", validate_cmd: str) -> List[Dict]:
|
|
1447
1457
|
"""Return a dict of configurations to build ConfigManager object"""
|
|
1448
1458
|
|
|
1449
1459
|
def cols_from_arg(concat_arg: str, client, table_obj: dict, query_str: str) -> list:
|
|
@@ -1503,7 +1513,7 @@ def get_pre_build_configs(args: Namespace, validate_cmd: str) -> List[Dict]:
|
|
|
1503
1513
|
target_client = clients.get_data_client(mgr.get_connection_config(args.target_conn))
|
|
1504
1514
|
|
|
1505
1515
|
# Get format: text, csv, json, table. Default is table
|
|
1506
|
-
format = args.format if args.format else
|
|
1516
|
+
format = args.format if args.format else consts.FORMAT_TYPE_TABLE
|
|
1507
1517
|
|
|
1508
1518
|
# Get random row arguments. Only in row validations these attributes can be present.
|
|
1509
1519
|
# Bad coding here, but keeping it so as not to introduce a breaking change. See
|
{google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/combiner.py
RENAMED
|
@@ -22,10 +22,12 @@ import datetime
|
|
|
22
22
|
import functools
|
|
23
23
|
import json
|
|
24
24
|
import logging
|
|
25
|
+
from typing import TYPE_CHECKING
|
|
26
|
+
|
|
25
27
|
import ibis
|
|
26
28
|
import ibis.expr.datatypes as dt
|
|
29
|
+
import pandas
|
|
27
30
|
|
|
28
|
-
from typing import TYPE_CHECKING
|
|
29
31
|
from data_validation import consts
|
|
30
32
|
|
|
31
33
|
if TYPE_CHECKING:
|
|
@@ -34,22 +36,109 @@ if TYPE_CHECKING:
|
|
|
34
36
|
from data_validation.metadata import RunMetadata, ValidationMetadata
|
|
35
37
|
|
|
36
38
|
|
|
39
|
+
# At around 140 columns we hit RecursionError when unioning Ibis subqueries.
|
|
40
|
+
# This constant is a threshold at which we slice up the input Dataframes
|
|
41
|
+
# and stitch them back together again after Ibis processing.
|
|
42
|
+
COMBINER_COLUMN_SLICE_WIDTH = 120
|
|
43
|
+
|
|
44
|
+
COMBINER_GET_SUMMARY_EXC_TEXT = (
|
|
45
|
+
"Error while generating summary report of row validation results"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
37
49
|
def generate_report(
|
|
38
|
-
client,
|
|
39
50
|
run_metadata: "RunMetadata",
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
source_df: "DataFrame",
|
|
52
|
+
target_df: "DataFrame",
|
|
42
53
|
join_on_fields=(),
|
|
43
54
|
is_value_comparison=False,
|
|
44
55
|
verbose=False,
|
|
45
|
-
):
|
|
56
|
+
) -> "DataFrame":
|
|
57
|
+
"""Combine results into a report.
|
|
58
|
+
|
|
59
|
+
This function is a wrapper around _generate_report_slice(). _generate_report_slice() does the main work, this
|
|
60
|
+
wrapper simply manages the input columns and stitches the results back together.
|
|
61
|
+
This is because validations of > 140(ish) columns trigger a RecursionError when unioning Ibis subqueries.
|
|
62
|
+
In this method we pass in column slices of the incoming Dataframes and combine the results.
|
|
63
|
+
|
|
64
|
+
It is a bit of a hack but I cannot find a way to optimize the Ibis processing. It appears to be
|
|
65
|
+
inefficient in that we create a subquery for each validation (column) in _calculate_differences() and
|
|
66
|
+
then union them all. We then do the same on the source/target table expressions to join it all back
|
|
67
|
+
together again. I (nj1973) spent a singificant amount of time trying to understand/optimize the Ibis
|
|
68
|
+
processing but fell back on this simpler (less risky) workaround.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
pandas.DataFrame:
|
|
72
|
+
A pandas DataFrame with the results of the validation in the same
|
|
73
|
+
schema as the report table.
|
|
74
|
+
"""
|
|
75
|
+
_check_schema_names(source_df, target_df)
|
|
76
|
+
|
|
77
|
+
join_on_fields = tuple(join_on_fields)
|
|
78
|
+
|
|
79
|
+
validation_columns = run_metadata.validations.keys()
|
|
80
|
+
# slice_thresholds is a list of points at which we should break up the Dataframe by column.
|
|
81
|
+
# e.g. [10, 20, 30] would mean process columns 0-9, 10-19 and 20-the max column.
|
|
82
|
+
# 1. len(...) / COMBINER_COLUMN_SLICE_WIDTH: Divides total columns by the slice width to get the number of slices.
|
|
83
|
+
# 2. int(...) + 1: int()+1 is effectively ceil() which is what we want to get the actual whole number of slices
|
|
84
|
+
# 3. _ * COMBINER_COLUMN_SLICE_WIDTH: Multiplies each number by the slice width to get actual column counts for each slice.
|
|
85
|
+
slice_thresholds = [
|
|
86
|
+
(_ * COMBINER_COLUMN_SLICE_WIDTH)
|
|
87
|
+
for _ in range(int(len(validation_columns) / COMBINER_COLUMN_SLICE_WIDTH) + 1)
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
result_df = None
|
|
91
|
+
# Process the input Dataframes in slices of columns to avoid "RecursionError"s.
|
|
92
|
+
for slice_start in slice_thresholds:
|
|
93
|
+
columns_in_vertical_slice = list(validation_columns)[
|
|
94
|
+
slice_start : slice_start + COMBINER_COLUMN_SLICE_WIDTH
|
|
95
|
+
]
|
|
96
|
+
# Ensure any join columns are included in the column slice.
|
|
97
|
+
columns_in_vertical_slice.extend(
|
|
98
|
+
set(join_on_fields) - set(columns_in_vertical_slice)
|
|
99
|
+
)
|
|
100
|
+
interim_result_df = _generate_report_slice(
|
|
101
|
+
run_metadata,
|
|
102
|
+
source_df[columns_in_vertical_slice],
|
|
103
|
+
target_df[columns_in_vertical_slice],
|
|
104
|
+
join_on_fields=join_on_fields,
|
|
105
|
+
is_value_comparison=is_value_comparison,
|
|
106
|
+
verbose=verbose,
|
|
107
|
+
)
|
|
108
|
+
if result_df is None:
|
|
109
|
+
result_df = interim_result_df
|
|
110
|
+
else:
|
|
111
|
+
result_df = pandas.concat([result_df, interim_result_df])
|
|
112
|
+
|
|
113
|
+
# Get the first validation metadata object to fill source and/or target empty table names.
|
|
114
|
+
first = run_metadata.validations[next(iter(run_metadata.validations))]
|
|
115
|
+
if first.validation_type != consts.CUSTOM_QUERY:
|
|
116
|
+
result_df.source_table_name.fillna(
|
|
117
|
+
first.get_table_name(consts.RESULT_TYPE_SOURCE), inplace=True
|
|
118
|
+
)
|
|
119
|
+
result_df.target_table_name.fillna(
|
|
120
|
+
first.get_table_name(consts.RESULT_TYPE_TARGET), inplace=True
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
_get_summary(run_metadata, result_df, source_df, target_df)
|
|
124
|
+
|
|
125
|
+
return result_df
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _generate_report_slice(
|
|
129
|
+
run_metadata: "RunMetadata",
|
|
130
|
+
source_df: "DataFrame",
|
|
131
|
+
target_df: "DataFrame",
|
|
132
|
+
join_on_fields=(),
|
|
133
|
+
is_value_comparison=False,
|
|
134
|
+
verbose=False,
|
|
135
|
+
) -> "DataFrame":
|
|
46
136
|
"""Combine results into a report.
|
|
47
137
|
|
|
48
138
|
Args:
|
|
49
|
-
client (ibis.client.Client): Ibis client used to combine results.
|
|
50
139
|
run_metadata: Metadata about the run and validations.
|
|
51
|
-
|
|
52
|
-
|
|
140
|
+
source_df: Dataframe contains results of source query.
|
|
141
|
+
target_df: Dataframe contains results of target query.
|
|
53
142
|
join_on_fields (Sequence[str]):
|
|
54
143
|
A collection of column names to use to join source and target.
|
|
55
144
|
These are the columns that both the source and target queries
|
|
@@ -63,16 +152,15 @@ def generate_report(
|
|
|
63
152
|
A pandas DataFrame with the results of the validation in the same
|
|
64
153
|
schema as the report table.
|
|
65
154
|
"""
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
155
|
+
client = ibis.pandas.connect(
|
|
156
|
+
{
|
|
157
|
+
consts.RESULT_TYPE_SOURCE: source_df,
|
|
158
|
+
consts.RESULT_TYPE_TARGET: target_df,
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
source = client.table(consts.RESULT_TYPE_SOURCE)
|
|
162
|
+
target = client.table(consts.RESULT_TYPE_TARGET)
|
|
70
163
|
|
|
71
|
-
if source_names != target_names:
|
|
72
|
-
raise ValueError(
|
|
73
|
-
"Expected source and target to have same schema, got "
|
|
74
|
-
f"{consts.RESULT_TYPE_SOURCE}: {source_names}; {consts.RESULT_TYPE_TARGET}: {target_names}"
|
|
75
|
-
)
|
|
76
164
|
differences_pivot = _calculate_differences(
|
|
77
165
|
source, target, join_on_fields, run_metadata.validations, is_value_comparison
|
|
78
166
|
)
|
|
@@ -81,18 +169,18 @@ def generate_report(
|
|
|
81
169
|
source_pivot = _pivot_result(
|
|
82
170
|
source, join_on_fields, run_metadata.validations, consts.RESULT_TYPE_SOURCE
|
|
83
171
|
)
|
|
84
|
-
|
|
172
|
+
source_pivot_df = client.execute(source_pivot)
|
|
85
173
|
|
|
86
174
|
target_pivot = _pivot_result(
|
|
87
175
|
target, join_on_fields, run_metadata.validations, consts.RESULT_TYPE_TARGET
|
|
88
176
|
)
|
|
89
|
-
|
|
177
|
+
target_pivot_df = client.execute(target_pivot)
|
|
90
178
|
|
|
91
179
|
con = ibis.pandas.connect(
|
|
92
180
|
{
|
|
93
|
-
consts.RESULT_TYPE_SOURCE:
|
|
94
|
-
|
|
95
|
-
consts.RESULT_TYPE_TARGET:
|
|
181
|
+
consts.RESULT_TYPE_SOURCE: source_pivot_df,
|
|
182
|
+
consts.RESULT_TYPE_DIFFERENCES: differences_df,
|
|
183
|
+
consts.RESULT_TYPE_TARGET: target_pivot_df,
|
|
96
184
|
}
|
|
97
185
|
)
|
|
98
186
|
joined = _join_pivots(
|
|
@@ -107,19 +195,6 @@ def generate_report(
|
|
|
107
195
|
|
|
108
196
|
result_df = client.execute(documented)
|
|
109
197
|
result_df.validation_status.fillna(consts.VALIDATION_STATUS_FAIL, inplace=True)
|
|
110
|
-
|
|
111
|
-
# get the first validation metadata object to fill source and/or target empty table names
|
|
112
|
-
first = run_metadata.validations[next(iter(run_metadata.validations))]
|
|
113
|
-
if first.validation_type != consts.CUSTOM_QUERY:
|
|
114
|
-
result_df.source_table_name.fillna(
|
|
115
|
-
first.get_table_name(consts.RESULT_TYPE_SOURCE), inplace=True
|
|
116
|
-
)
|
|
117
|
-
result_df.target_table_name.fillna(
|
|
118
|
-
first.get_table_name(consts.RESULT_TYPE_TARGET), inplace=True
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
_get_summary(run_metadata, result_df, source_df, target_df)
|
|
122
|
-
|
|
123
198
|
return result_df
|
|
124
199
|
|
|
125
200
|
|
|
@@ -279,6 +354,18 @@ def _calculate_differences(
|
|
|
279
354
|
return differences_pivot
|
|
280
355
|
|
|
281
356
|
|
|
357
|
+
def _check_schema_names(source_df, target_df):
|
|
358
|
+
"""Check that the two input Dataframes have matching column names."""
|
|
359
|
+
source_names = tuple(source_df.columns)
|
|
360
|
+
target_names = tuple(target_df.columns)
|
|
361
|
+
|
|
362
|
+
if source_names != target_names:
|
|
363
|
+
raise ValueError(
|
|
364
|
+
"Expected source and target to have same schema, got "
|
|
365
|
+
f"{consts.RESULT_TYPE_SOURCE}: {source_names}; {consts.RESULT_TYPE_TARGET}: {target_names}"
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
282
369
|
def _pivot_result(
|
|
283
370
|
result: "IbisTable",
|
|
284
371
|
join_on_fields: tuple,
|
|
@@ -441,12 +528,19 @@ def _get_summary(
|
|
|
441
528
|
):
|
|
442
529
|
"""Logs a summary report/stats of row validation results."""
|
|
443
530
|
try:
|
|
444
|
-
if result_df.
|
|
445
|
-
|
|
531
|
+
if result_df.empty:
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
if (result_df.loc[0, consts.VALIDATION_TYPE] == consts.ROW_VALIDATION) or (
|
|
535
|
+
# Check for custom-query row validation which always should have primary keys (not null)
|
|
536
|
+
result_df.loc[0, consts.VALIDATION_TYPE] == consts.CUSTOM_QUERY
|
|
537
|
+
and result_df.loc[0, consts.CONFIG_PRIMARY_KEYS]
|
|
538
|
+
):
|
|
539
|
+
# Vectorized calculations for all counts.
|
|
446
540
|
success_condition = (
|
|
447
541
|
result_df[consts.VALIDATION_STATUS] == consts.VALIDATION_STATUS_SUCCESS
|
|
448
542
|
)
|
|
449
|
-
fail_condition = ~success_condition # Invert success for fail condition
|
|
543
|
+
fail_condition = ~success_condition # Invert success for fail condition.
|
|
450
544
|
|
|
451
545
|
source_not_in_target = (
|
|
452
546
|
result_df[consts.SOURCE_AGG_VALUE].notnull()
|
|
@@ -462,33 +556,32 @@ def _get_summary(
|
|
|
462
556
|
)
|
|
463
557
|
|
|
464
558
|
logging.info(
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
559
|
+
json.dumps(
|
|
560
|
+
{
|
|
561
|
+
consts.CONFIG_RUN_ID: run_metadata.run_id,
|
|
562
|
+
consts.CONFIG_START_TIME: run_metadata.start_time.isoformat(),
|
|
563
|
+
consts.CONFIG_END_TIME: run_metadata.end_time.isoformat(),
|
|
564
|
+
# Explicit conversion of numpy's int64 values to int for JSON serializability
|
|
565
|
+
consts.TOTAL_SOURCE_ROWS: int(source_df.shape[0]),
|
|
566
|
+
consts.TOTAL_TARGET_ROWS: int(target_df.shape[0]),
|
|
567
|
+
consts.TOTAL_ROWS_VALIDATED: int(result_df.shape[0]),
|
|
568
|
+
# Using .sum() on boolean Series for much faster counting
|
|
569
|
+
consts.TOTAL_ROWS_SUCCESS: int(success_condition.sum()),
|
|
570
|
+
consts.TOTAL_ROWS_FAIL: int(fail_condition.sum()),
|
|
571
|
+
consts.FAILED_SOURCE_NOT_IN_TARGET: int(
|
|
572
|
+
(fail_condition & source_not_in_target).sum()
|
|
573
|
+
),
|
|
574
|
+
consts.FAILED_TARGET_NOT_IN_SOURCE: int(
|
|
575
|
+
(fail_condition & target_not_in_source).sum()
|
|
576
|
+
),
|
|
577
|
+
consts.FAILED_PRESENT_IN_BOTH_TABLES: int(
|
|
578
|
+
(fail_condition & present_in_both_tables).sum()
|
|
579
|
+
),
|
|
580
|
+
}
|
|
581
|
+
)
|
|
489
582
|
)
|
|
490
583
|
except Exception as e:
|
|
491
584
|
logging.warning(
|
|
492
|
-
f"
|
|
585
|
+
f"{COMBINER_GET_SUMMARY_EXC_TEXT}: {e}",
|
|
493
586
|
exc_info=True,
|
|
494
587
|
)
|
|
@@ -16,7 +16,7 @@ import copy
|
|
|
16
16
|
import logging
|
|
17
17
|
import string
|
|
18
18
|
import random
|
|
19
|
-
from typing import TYPE_CHECKING, Optional, Union,
|
|
19
|
+
from typing import TYPE_CHECKING, Dict, List, Optional, Union, Tuple
|
|
20
20
|
|
|
21
21
|
import google.oauth2.service_account
|
|
22
22
|
import ibis.expr.datatypes as dt
|
|
@@ -66,6 +66,9 @@ class ConfigManager(object):
|
|
|
66
66
|
if self.validation_type not in consts.CONFIG_TYPES:
|
|
67
67
|
raise ValueError(f"Unknown Configuration Type: {self.validation_type}")
|
|
68
68
|
self._comparison_max_col_length = None
|
|
69
|
+
# For some engines we need to know the actual raw data type rather than the Ibis canonical type.
|
|
70
|
+
self._source_raw_data_types = None
|
|
71
|
+
self._target_raw_data_types = None
|
|
69
72
|
|
|
70
73
|
@property
|
|
71
74
|
def config(self):
|
|
@@ -94,6 +97,50 @@ class ConfigManager(object):
|
|
|
94
97
|
|
|
95
98
|
return self._target_conn
|
|
96
99
|
|
|
100
|
+
def get_source_raw_data_types(self) -> Dict[str, Tuple]:
|
|
101
|
+
"""Return raw data type information from source system.
|
|
102
|
+
|
|
103
|
+
The raw data type is the source/target engine type, for example it might
|
|
104
|
+
be "NCLOB" or "char" when the Ibis type simply states "string".
|
|
105
|
+
The data is cached in state when fetched for the first time.
|
|
106
|
+
The retuen value is keyed on the casefolded column name and the tuple is
|
|
107
|
+
the remaining 6 elements of the DB API cursor description specification."""
|
|
108
|
+
if self._source_raw_data_types is None:
|
|
109
|
+
if hasattr(self.source_client, "raw_column_metadata"):
|
|
110
|
+
raw_data_types = self.source_client.raw_column_metadata(
|
|
111
|
+
database=self.source_schema,
|
|
112
|
+
table=self.source_table,
|
|
113
|
+
query=self.source_query,
|
|
114
|
+
)
|
|
115
|
+
self._source_raw_data_types = {
|
|
116
|
+
_[0].casefold(): _[1:] for _ in raw_data_types
|
|
117
|
+
}
|
|
118
|
+
else:
|
|
119
|
+
self._source_raw_data_types = {}
|
|
120
|
+
return self._source_raw_data_types
|
|
121
|
+
|
|
122
|
+
def get_target_raw_data_types(self) -> Dict[str, Tuple]:
|
|
123
|
+
"""Return raw data type information from target system.
|
|
124
|
+
|
|
125
|
+
The raw data type is the source/target engine type, for example it might
|
|
126
|
+
be "NCLOB" or "char" when the Ibis type simply states "string".
|
|
127
|
+
The data is cached in state when fetched for the first time.
|
|
128
|
+
The retuen value is keyed on the casefolded column name and the tuple is
|
|
129
|
+
the remaining 6 elements of the DB API cursor description specification."""
|
|
130
|
+
if self._target_raw_data_types is None:
|
|
131
|
+
if hasattr(self.target_client, "raw_column_metadata"):
|
|
132
|
+
raw_data_types = self.target_client.raw_column_metadata(
|
|
133
|
+
database=self.target_schema,
|
|
134
|
+
table=self.target_table,
|
|
135
|
+
query=self.target_query,
|
|
136
|
+
)
|
|
137
|
+
self._target_raw_data_types = {
|
|
138
|
+
_[0].casefold(): _[1:] for _ in raw_data_types
|
|
139
|
+
}
|
|
140
|
+
else:
|
|
141
|
+
self._target_raw_data_types = {}
|
|
142
|
+
return self._target_raw_data_types
|
|
143
|
+
|
|
97
144
|
def close_client_connections(self):
|
|
98
145
|
"""Attempt to clean up any source/target connections, based on the client types.
|
|
99
146
|
|
|
@@ -138,10 +185,6 @@ class ConfigManager(object):
|
|
|
138
185
|
"""Return if the validation should perform a case insensitive match."""
|
|
139
186
|
return self._config.get(consts.CONFIG_CASE_INSENSITIVE_MATCH) or False
|
|
140
187
|
|
|
141
|
-
def process_in_memory(self):
|
|
142
|
-
"""Return whether to process in memory or on a remote platform."""
|
|
143
|
-
return True
|
|
144
|
-
|
|
145
188
|
@property
|
|
146
189
|
def max_recursive_query_size(self):
|
|
147
190
|
"""Return Aggregates from Config"""
|
|
@@ -459,7 +502,7 @@ class ConfigManager(object):
|
|
|
459
502
|
cols_filter_list = consts.COLUMN_FILTER_LIST
|
|
460
503
|
# handler that display results either to output or in a file
|
|
461
504
|
return TextResultHandler(
|
|
462
|
-
self._config.get(consts.CONFIG_FORMAT,
|
|
505
|
+
self._config.get(consts.CONFIG_FORMAT, consts.FORMAT_TYPE_TABLE),
|
|
463
506
|
self.filter_status,
|
|
464
507
|
cols_filter_list,
|
|
465
508
|
)
|
|
@@ -486,7 +529,9 @@ class ConfigManager(object):
|
|
|
486
529
|
table_id=table_id,
|
|
487
530
|
credentials=credentials,
|
|
488
531
|
api_endpoint=api_endpoint,
|
|
489
|
-
text_format=self._config.get(
|
|
532
|
+
text_format=self._config.get(
|
|
533
|
+
consts.CONFIG_FORMAT, consts.FORMAT_TYPE_TABLE
|
|
534
|
+
),
|
|
490
535
|
)
|
|
491
536
|
else:
|
|
492
537
|
raise ValueError(f"Unknown ResultHandler Class: {result_type}")
|
|
@@ -874,7 +919,7 @@ class ConfigManager(object):
|
|
|
874
919
|
target_column_ibis_type: dt.DataType,
|
|
875
920
|
margin: int = 0,
|
|
876
921
|
) -> bool:
|
|
877
|
-
"""Identifies
|
|
922
|
+
"""Identifies numeric columns that will cause problems in a Pandas Dataframe.
|
|
878
923
|
|
|
879
924
|
i.e. are of greater precision than a 64bit int/real can hold.
|
|
880
925
|
|
|
@@ -883,17 +928,23 @@ class ConfigManager(object):
|
|
|
883
928
|
"""
|
|
884
929
|
return bool(
|
|
885
930
|
(
|
|
886
|
-
isinstance(source_column_ibis_type, dt.
|
|
887
|
-
|
|
888
|
-
source_column_ibis_type.
|
|
889
|
-
|
|
931
|
+
(isinstance(source_column_ibis_type, dt.Int64) and margin > 0)
|
|
932
|
+
or (
|
|
933
|
+
isinstance(source_column_ibis_type, dt.Decimal)
|
|
934
|
+
and (
|
|
935
|
+
source_column_ibis_type.precision is None
|
|
936
|
+
or source_column_ibis_type.precision > (18 - margin)
|
|
937
|
+
)
|
|
890
938
|
)
|
|
891
939
|
)
|
|
892
940
|
and (
|
|
893
|
-
isinstance(target_column_ibis_type, dt.
|
|
894
|
-
|
|
895
|
-
target_column_ibis_type.
|
|
896
|
-
|
|
941
|
+
(isinstance(target_column_ibis_type, dt.Int64) and margin > 0)
|
|
942
|
+
or (
|
|
943
|
+
isinstance(target_column_ibis_type, dt.Decimal)
|
|
944
|
+
and (
|
|
945
|
+
target_column_ibis_type.precision is None
|
|
946
|
+
or target_column_ibis_type.precision > (18 - margin)
|
|
947
|
+
)
|
|
897
948
|
)
|
|
898
949
|
)
|
|
899
950
|
)
|
{google_pso_data_validator-7.3.0 → google_pso_data_validator-7.5.0}/data_validation/consts.py
RENAMED
|
@@ -166,8 +166,16 @@ COUNT_STAR = "{count_star}"
|
|
|
166
166
|
# Validation metadata
|
|
167
167
|
RESULT_TYPE_SOURCE = "source"
|
|
168
168
|
RESULT_TYPE_TARGET = "target"
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
RESULT_TYPE_DIFFERENCES = "differences"
|
|
170
|
+
|
|
171
|
+
FORMAT_TYPE_CSV = "csv"
|
|
172
|
+
FORMAT_TYPE_JSON = "json"
|
|
173
|
+
FORMAT_TYPE_MINIMAL = "minimal"
|
|
174
|
+
FORMAT_TYPE_PYTHON = "python"
|
|
175
|
+
FORMAT_TYPE_TABLE = "table"
|
|
176
|
+
FORMAT_TYPE_TEXT = "text"
|
|
177
|
+
FORMAT_TYPES = [FORMAT_TYPE_CSV, FORMAT_TYPE_JSON, FORMAT_TYPE_TABLE, FORMAT_TYPE_TEXT]
|
|
178
|
+
RAW_QUERY_FORMAT_TYPES = [FORMAT_TYPE_MINIMAL, FORMAT_TYPE_PYTHON]
|
|
171
179
|
|
|
172
180
|
# Text Result Handler column filter list
|
|
173
181
|
COLUMN_FILTER_LIST = [
|