table-validator 0.1.2__tar.gz → 0.1.4__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.
- {table_validator-0.1.2/table_validator.egg-info → table_validator-0.1.4}/PKG-INFO +9 -1
- {table_validator-0.1.2 → table_validator-0.1.4}/README.md +8 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/cli/main.py +23 -4
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/connectors/databricks_connector.py +29 -5
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/models.py +34 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/validators/catalog_validator.py +422 -119
- {table_validator-0.1.2 → table_validator-0.1.4/table_validator.egg-info}/PKG-INFO +9 -1
- table_validator-0.1.4/table_validator.egg-info/scm_version.json +8 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/tests/test_catalog_validator.py +144 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/tests/test_cli.py +100 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/tests/test_databricks_connector.py +78 -0
- table_validator-0.1.2/table_validator.egg-info/scm_version.json +0 -8
- {table_validator-0.1.2 → table_validator-0.1.4}/LICENSE +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/pyproject.toml +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/setup.cfg +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/__init__.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/auth/__init__.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/auth/azure_auth.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/auth/databricks_auth.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/cli/__init__.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/cli/partition_prompt.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/cli/summary_table.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/cli/wizard.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/config/__init__.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/config/manager.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/config/schema.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/connectors/__init__.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/connectors/azure_connector.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/engine/__init__.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/engine/comparison_engine.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/reports/__init__.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/reports/excel_report.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/validators/__init__.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/validators/blob_discovery.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator/validators/row_validator.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator.egg-info/SOURCES.txt +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator.egg-info/dependency_links.txt +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator.egg-info/entry_points.txt +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator.egg-info/requires.txt +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator.egg-info/scm_file_list.json +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/table_validator.egg-info/top_level.txt +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/tests/__init__.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/tests/test_blob_discovery.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/tests/test_excel_report.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/tests/test_partition_prompt.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/tests/test_report_command.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/tests/test_row_validator.py +0 -0
- {table_validator-0.1.2 → table_validator-0.1.4}/tests/test_wizard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: table-validator
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: CLI tool for validating data migrations between Azure (Blob Storage / SQL Database) and Databricks Delta Lake catalogs
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -174,6 +174,14 @@ Everything lives outside the repo, under your home directory:
|
|
|
174
174
|
by construction, since it's written under your home directory rather
|
|
175
175
|
than the working directory.
|
|
176
176
|
|
|
177
|
+
Optional: `DATABRICKS_RETRY_TIMEOUT_SECONDS` (a plain environment variable,
|
|
178
|
+
not part of `config.yaml`) raises the CloudFetch HTTP retry timeout above
|
|
179
|
+
its 300-second default. On a slow or unstable network, downloading a
|
|
180
|
+
large row-hash result set can legitimately take longer than that, causing
|
|
181
|
+
a `Retry request would exceed Retry policy max retry duration` failure
|
|
182
|
+
even though the query itself succeeded. Set it higher (e.g. `900` for 15
|
|
183
|
+
minutes) if you hit this on large tables.
|
|
184
|
+
|
|
177
185
|
A future version will replace manual credential entry with Azure CLI /
|
|
178
186
|
Service Principal auth and Databricks CLI / OAuth login, without changing
|
|
179
187
|
the config file format or any command usage above.
|
|
@@ -147,6 +147,14 @@ Everything lives outside the repo, under your home directory:
|
|
|
147
147
|
by construction, since it's written under your home directory rather
|
|
148
148
|
than the working directory.
|
|
149
149
|
|
|
150
|
+
Optional: `DATABRICKS_RETRY_TIMEOUT_SECONDS` (a plain environment variable,
|
|
151
|
+
not part of `config.yaml`) raises the CloudFetch HTTP retry timeout above
|
|
152
|
+
its 300-second default. On a slow or unstable network, downloading a
|
|
153
|
+
large row-hash result set can legitimately take longer than that, causing
|
|
154
|
+
a `Retry request would exceed Retry policy max retry duration` failure
|
|
155
|
+
even though the query itself succeeded. Set it higher (e.g. `900` for 15
|
|
156
|
+
minutes) if you hit this on large tables.
|
|
157
|
+
|
|
150
158
|
A future version will replace manual credential entry with Azure CLI /
|
|
151
159
|
Service Principal auth and Databricks CLI / OAuth login, without changing
|
|
152
160
|
the config file format or any command usage above.
|
|
@@ -420,20 +420,37 @@ def _run_databricks_validation(
|
|
|
420
420
|
matching" for that level - CatalogValidator already performs the
|
|
421
421
|
list-and-intersect discovery internally whenever request.schemas/
|
|
422
422
|
tables is left unrestricted (None), so leaving the restriction off
|
|
423
|
-
IS the discovery trigger; there is no separate discovery step to call.
|
|
423
|
+
IS the discovery trigger; there is no separate discovery step to call.
|
|
424
|
+
|
|
425
|
+
If the user explicitly named BOTH a source and target schema/table,
|
|
426
|
+
compare that exact pair directly - schema_map/table_map bypass
|
|
427
|
+
name-based matching entirely, so the two sides don't need to share a
|
|
428
|
+
name (a typo, a rename, different casing). schemas_restriction/
|
|
429
|
+
tables_restriction scope by the SOURCE-side name, matching
|
|
430
|
+
CatalogValidator._validate_schema/compare_catalogs' filtering
|
|
431
|
+
convention (mirrors _run_sql_validation's identical pattern below).
|
|
432
|
+
When names are identical (today's common case) schema_map/table_map
|
|
433
|
+
end up empty and behavior is unchanged."""
|
|
434
|
+
schema_map: Dict[str, str] = {}
|
|
424
435
|
schemas_restriction = None
|
|
425
436
|
if config.source_table.schema_name and config.target_table.schema_name:
|
|
426
|
-
schemas_restriction = [config.
|
|
437
|
+
schemas_restriction = [config.source_table.schema_name]
|
|
438
|
+
if config.source_table.schema_name.lower() != config.target_table.schema_name.lower():
|
|
439
|
+
schema_map = {config.source_table.schema_name: config.target_table.schema_name}
|
|
427
440
|
|
|
441
|
+
table_map: Dict[str, str] = {}
|
|
428
442
|
tables_restriction = None
|
|
429
443
|
if config.source_table.table and config.target_table.table:
|
|
430
|
-
tables_restriction = [config.
|
|
444
|
+
tables_restriction = [config.source_table.table]
|
|
445
|
+
if config.source_table.table.lower() != config.target_table.table.lower():
|
|
446
|
+
table_map = {config.source_table.table: config.target_table.table}
|
|
431
447
|
|
|
432
448
|
# A configured primary key only applies to the single named table
|
|
433
449
|
# (not a catalog-wide sweep) - CatalogValidator looks it up by
|
|
434
450
|
# "schema.table" first, falling back to a bare table name, so provide
|
|
435
451
|
# both forms when a schema is known; falls back to row-number matching
|
|
436
|
-
# as before when unset.
|
|
452
|
+
# as before when unset. Keyed by the TARGET-side name, since that's
|
|
453
|
+
# what _lookup_primary_key resolves against.
|
|
437
454
|
primary_keys: Dict[str, list] = {}
|
|
438
455
|
if config.primary_key and config.target_table.table:
|
|
439
456
|
primary_keys[config.target_table.table] = config.primary_key
|
|
@@ -447,7 +464,9 @@ def _run_databricks_validation(
|
|
|
447
464
|
source_catalog=config.source_table.catalog or "",
|
|
448
465
|
target_catalog=config.target_table.catalog or "",
|
|
449
466
|
schemas=schemas_restriction,
|
|
467
|
+
schema_map=schema_map,
|
|
450
468
|
tables=tables_restriction,
|
|
469
|
+
table_map=table_map,
|
|
451
470
|
enabled_validations=set(config.validations),
|
|
452
471
|
primary_keys=primary_keys,
|
|
453
472
|
max_tier=max_tier,
|
{table_validator-0.1.2 → table_validator-0.1.4}/table_validator/connectors/databricks_connector.py
RENAMED
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
import datetime
|
|
15
15
|
import logging
|
|
16
16
|
import numbers
|
|
17
|
+
import os
|
|
17
18
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence, Tuple
|
|
18
19
|
|
|
19
20
|
import pandas as pd
|
|
@@ -97,6 +98,7 @@ class DatabricksConnector:
|
|
|
97
98
|
host: Optional[str] = None,
|
|
98
99
|
token: Optional[str] = None,
|
|
99
100
|
http_path: Optional[str] = None,
|
|
101
|
+
retry_timeout_seconds: Optional[float] = None,
|
|
100
102
|
) -> None:
|
|
101
103
|
"""
|
|
102
104
|
host/token/http_path must be resolved by the caller before
|
|
@@ -104,11 +106,27 @@ class DatabricksConnector:
|
|
|
104
106
|
get_databricks_token() for the token, and config.databricks.
|
|
105
107
|
workspace_url/http_path for the rest. This connector does not
|
|
106
108
|
read credentials from the environment itself.
|
|
109
|
+
|
|
110
|
+
retry_timeout_seconds overrides databricks-sql-connector's
|
|
111
|
+
CloudFetch HTTP retry policy (_retry_stop_after_attempts_duration),
|
|
112
|
+
which otherwise defaults to 300 seconds. On a slow or unstable
|
|
113
|
+
network, downloading a large row-hash result set can legitimately
|
|
114
|
+
take longer than that, causing a hard
|
|
115
|
+
"Retry request would exceed Retry policy max retry duration"
|
|
116
|
+
failure even though the query itself succeeded - raising this
|
|
117
|
+
gives a slow-but-working connection more time instead of giving
|
|
118
|
+
up. Falls back to the DATABRICKS_RETRY_TIMEOUT_SECONDS environment
|
|
119
|
+
variable, then the connector's own default, if not given.
|
|
107
120
|
"""
|
|
108
121
|
|
|
109
122
|
self._host = host
|
|
110
123
|
self._token = token
|
|
111
124
|
self._http_path = http_path
|
|
125
|
+
self._retry_timeout_seconds = retry_timeout_seconds or (
|
|
126
|
+
float(os.environ["DATABRICKS_RETRY_TIMEOUT_SECONDS"])
|
|
127
|
+
if os.environ.get("DATABRICKS_RETRY_TIMEOUT_SECONDS")
|
|
128
|
+
else None
|
|
129
|
+
)
|
|
112
130
|
|
|
113
131
|
if not self._host or not self._token:
|
|
114
132
|
raise ValueError(
|
|
@@ -138,11 +156,17 @@ class DatabricksConnector:
|
|
|
138
156
|
|
|
139
157
|
try:
|
|
140
158
|
|
|
141
|
-
|
|
142
|
-
server_hostname
|
|
143
|
-
http_path
|
|
144
|
-
access_token
|
|
145
|
-
|
|
159
|
+
connect_kwargs: Dict[str, Any] = {
|
|
160
|
+
"server_hostname": self._host,
|
|
161
|
+
"http_path": self._http_path,
|
|
162
|
+
"access_token": self._token,
|
|
163
|
+
}
|
|
164
|
+
if self._retry_timeout_seconds is not None:
|
|
165
|
+
connect_kwargs["_retry_stop_after_attempts_duration"] = (
|
|
166
|
+
self._retry_timeout_seconds
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
self._connection = sql.connect(**connect_kwargs)
|
|
146
170
|
|
|
147
171
|
with self._connection.cursor() as cursor:
|
|
148
172
|
cursor.execute("SELECT 1")
|
|
@@ -451,6 +451,19 @@ class CatalogValidationRequest(BaseModel):
|
|
|
451
451
|
),
|
|
452
452
|
)
|
|
453
453
|
|
|
454
|
+
schema_map: Dict[str, str] = Field(
|
|
455
|
+
default_factory=dict,
|
|
456
|
+
description=(
|
|
457
|
+
"Optional map of source-catalog schema name -> target-catalog "
|
|
458
|
+
"schema name, for when the user has explicitly named a source "
|
|
459
|
+
"and target schema that don't share the same name. An explicit "
|
|
460
|
+
"pair like this is compared directly, bypassing name-based "
|
|
461
|
+
"schema matching entirely (unlike `schemas`, which still "
|
|
462
|
+
"requires the name to appear in the intersection). Unmapped "
|
|
463
|
+
"schemas are matched by identical name as usual."
|
|
464
|
+
),
|
|
465
|
+
)
|
|
466
|
+
|
|
454
467
|
tables: Optional[List[str]] = Field(
|
|
455
468
|
default=None,
|
|
456
469
|
description=(
|
|
@@ -460,6 +473,19 @@ class CatalogValidationRequest(BaseModel):
|
|
|
460
473
|
),
|
|
461
474
|
)
|
|
462
475
|
|
|
476
|
+
table_map: Dict[str, str] = Field(
|
|
477
|
+
default_factory=dict,
|
|
478
|
+
description=(
|
|
479
|
+
"Optional map of source-catalog table name -> target-catalog "
|
|
480
|
+
"table name, for when the user has explicitly named a source "
|
|
481
|
+
"and target table that don't share the same name - an explicit "
|
|
482
|
+
"pair like this is compared directly, bypassing name-based "
|
|
483
|
+
"table matching entirely (unlike `tables`, which still requires "
|
|
484
|
+
"the name to appear in the intersection). Unmapped tables are "
|
|
485
|
+
"matched by identical name as usual."
|
|
486
|
+
),
|
|
487
|
+
)
|
|
488
|
+
|
|
463
489
|
ignore_columns: List[str] = Field(default_factory=list)
|
|
464
490
|
|
|
465
491
|
case_sensitive_columns: bool = Field(
|
|
@@ -835,6 +861,14 @@ class TableValidationResult(BaseModel):
|
|
|
835
861
|
table: str
|
|
836
862
|
status: ValidationStatus = ValidationStatus.SKIPPED
|
|
837
863
|
|
|
864
|
+
# Populated by CatalogValidator/AzureSqlValidator whenever the
|
|
865
|
+
# source-side schema/table name differs from the target-side name
|
|
866
|
+
# shown above (e.g. an explicit schema_map/table_map pair) - lets a
|
|
867
|
+
# future report enhancement show both names. schema_name/table above
|
|
868
|
+
# always reflect the TARGET-side name (the reporting convention).
|
|
869
|
+
source_schema_name: Optional[str] = None
|
|
870
|
+
source_table_name: Optional[str] = None
|
|
871
|
+
|
|
838
872
|
exists_in_source: bool = True
|
|
839
873
|
exists_in_target: bool = True
|
|
840
874
|
|