dequa-sdk 1.0.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.
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: dequa-sdk
3
+ Version: 1.0.0
4
+ Summary: Native Python SDK & Integrations for Dequa ETL Data Quality Engine
5
+ Author-email: Dequa ETL Team <support@dequa-etl.io>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/dequa-etl/dequa-etl-tool
8
+ Project-URL: Bug Tracker, https://github.com/dequa-etl/dequa-etl-tool/issues
9
+ Project-URL: Documentation, https://github.com/dequa-etl/dequa-etl-tool#readme
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Topic :: Database
16
+ Classifier: Topic :: Software Development :: Quality Assurance
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: requests>=2.28.0
20
+ Requires-Dist: pandas>=1.5.0
21
+ Requires-Dist: duckdb>=0.9.0
22
+ Requires-Dist: pydantic>=2.0.0
23
+ Provides-Extra: airflow
24
+ Requires-Dist: apache-airflow>=2.4.0; extra == "airflow"
25
+ Provides-Extra: dbt
26
+ Requires-Dist: dbt-core>=1.5.0; extra == "dbt"
27
+ Provides-Extra: all
28
+ Requires-Dist: apache-airflow>=2.4.0; extra == "all"
29
+ Requires-Dist: dbt-core>=1.5.0; extra == "all"
30
+
31
+ # ⚡ Dequa Python SDK
32
+
33
+ The official Python client and data validation SDK for **Dequa ETL**.
34
+
35
+ [![PyPI](https://img.shields.io/pypi/v/dequa-sdk.svg)](https://pypi.org/project/dequa-sdk/)
36
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
37
+
38
+ ---
39
+
40
+ ## 📦 Installation
41
+
42
+ ```bash
43
+ pip install dequa-sdk
44
+ ```
45
+
46
+ With optional integrations:
47
+ ```bash
48
+ # Airflow integration
49
+ pip install dequa-sdk[airflow]
50
+
51
+ # dbt Core integration
52
+ pip install dequa-sdk[dbt]
53
+ ```
54
+
55
+ ---
56
+
57
+ ## 🚀 Quick Usage
58
+
59
+ ### 1. Vectorized In-Memory DataFrame Reconciliation
60
+
61
+ ```python
62
+ import pandas as pd
63
+ from dequa_sdk import validate
64
+
65
+ source_df = pd.read_csv("bronze_orders.csv")
66
+ target_df = pd.read_csv("gold_orders.csv")
67
+
68
+ # Run zero-config reconciliation
69
+ report = validate(
70
+ source=source_df,
71
+ target=target_df,
72
+ key_columns=["order_id"],
73
+ compare_columns=["amount", "status"]
74
+ )
75
+
76
+ print(report.summary())
77
+ if report.has_discrepancies():
78
+ print("Mismatched records found:", report.value_mismatches)
79
+ ```
80
+
81
+ ---
82
+
83
+ ### 2. Airflow DAG Gating Operator
84
+
85
+ ```python
86
+ from airflow import DAG
87
+ from dequa_sdk.integrations.airflow import DequaCheckOperator
88
+ from datetime import datetime
89
+
90
+ with DAG("daily_orders_pipeline", start_date=datetime(2026, 1, 1)) as dag:
91
+ assert_orders = DequaCheckOperator(
92
+ task_id="assert_orders_reconciliation",
93
+ pipeline_id=1,
94
+ dequa_url="http://dequa-backend:8000",
95
+ fail_on_mismatch=True
96
+ )
97
+ ```
98
+
99
+ ---
100
+
101
+ ### 3. dbt Manifest Parser
102
+
103
+ ```python
104
+ from dequa_sdk.integrations.dbt import DbtManifestParser
105
+
106
+ parser = DbtManifestParser(manifest_path="target/manifest.json")
107
+ models = parser.extract_models()
108
+
109
+ for model in models:
110
+ config = parser.generate_pipeline_config(
111
+ model_name=model["model_name"],
112
+ source_conn_id=1,
113
+ target_conn_id=2
114
+ )
115
+ ```
@@ -0,0 +1,85 @@
1
+ # ⚡ Dequa Python SDK
2
+
3
+ The official Python client and data validation SDK for **Dequa ETL**.
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/dequa-sdk.svg)](https://pypi.org/project/dequa-sdk/)
6
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
7
+
8
+ ---
9
+
10
+ ## 📦 Installation
11
+
12
+ ```bash
13
+ pip install dequa-sdk
14
+ ```
15
+
16
+ With optional integrations:
17
+ ```bash
18
+ # Airflow integration
19
+ pip install dequa-sdk[airflow]
20
+
21
+ # dbt Core integration
22
+ pip install dequa-sdk[dbt]
23
+ ```
24
+
25
+ ---
26
+
27
+ ## 🚀 Quick Usage
28
+
29
+ ### 1. Vectorized In-Memory DataFrame Reconciliation
30
+
31
+ ```python
32
+ import pandas as pd
33
+ from dequa_sdk import validate
34
+
35
+ source_df = pd.read_csv("bronze_orders.csv")
36
+ target_df = pd.read_csv("gold_orders.csv")
37
+
38
+ # Run zero-config reconciliation
39
+ report = validate(
40
+ source=source_df,
41
+ target=target_df,
42
+ key_columns=["order_id"],
43
+ compare_columns=["amount", "status"]
44
+ )
45
+
46
+ print(report.summary())
47
+ if report.has_discrepancies():
48
+ print("Mismatched records found:", report.value_mismatches)
49
+ ```
50
+
51
+ ---
52
+
53
+ ### 2. Airflow DAG Gating Operator
54
+
55
+ ```python
56
+ from airflow import DAG
57
+ from dequa_sdk.integrations.airflow import DequaCheckOperator
58
+ from datetime import datetime
59
+
60
+ with DAG("daily_orders_pipeline", start_date=datetime(2026, 1, 1)) as dag:
61
+ assert_orders = DequaCheckOperator(
62
+ task_id="assert_orders_reconciliation",
63
+ pipeline_id=1,
64
+ dequa_url="http://dequa-backend:8000",
65
+ fail_on_mismatch=True
66
+ )
67
+ ```
68
+
69
+ ---
70
+
71
+ ### 3. dbt Manifest Parser
72
+
73
+ ```python
74
+ from dequa_sdk.integrations.dbt import DbtManifestParser
75
+
76
+ parser = DbtManifestParser(manifest_path="target/manifest.json")
77
+ models = parser.extract_models()
78
+
79
+ for model in models:
80
+ config = parser.generate_pipeline_config(
81
+ model_name=model["model_name"],
82
+ source_conn_id=1,
83
+ target_conn_id=2
84
+ )
85
+ ```
@@ -0,0 +1,10 @@
1
+ """
2
+ Dequa SDK — Native Data Quality & Validation Engine
3
+ """
4
+
5
+ from dequa_sdk.client import Client
6
+ from dequa_sdk.validators import validate, ValidationReport
7
+ from dequa_sdk.quarantine import QuarantineRouter
8
+
9
+ __version__ = "1.0.0"
10
+ __all__ = ["Client", "validate", "ValidationReport", "QuarantineRouter"]
@@ -0,0 +1,42 @@
1
+ """
2
+ Dequa SDK Client Core
3
+
4
+ Client interface for connecting Python data pipelines, PySpark jobs, and notebooks
5
+ to the Dequa ETL backend control plane.
6
+ """
7
+
8
+ from typing import Any, Dict, Optional
9
+ import os
10
+ import requests
11
+
12
+
13
+ class Client:
14
+ """Dequa SDK Client for pipeline validation and observability sync."""
15
+
16
+ def __init__(
17
+ self,
18
+ api_key: Optional[str] = None,
19
+ control_plane_url: Optional[str] = None,
20
+ offline_mode: bool = False
21
+ ):
22
+ self.api_key = api_key or os.getenv("DEQUA_API_KEY", "")
23
+ self.control_plane_url = (control_plane_url or os.getenv("DEQUA_API_URL", "http://localhost:8000/api/v1")).rstrip("/")
24
+ self.offline_mode = offline_mode
25
+
26
+ def _headers(self) -> Dict[str, str]:
27
+ headers = {"Content-Type": "application/json"}
28
+ if self.api_key:
29
+ headers["X-API-Key"] = self.api_key
30
+ return headers
31
+
32
+ def sync_validation_report(self, pipeline_id: str, report_data: Dict[str, Any]) -> bool:
33
+ """Sends inline SDK validation results to Dequa Control Plane."""
34
+ if self.offline_mode:
35
+ return True
36
+
37
+ endpoint = f"{self.control_plane_url}/pipelines/{pipeline_id}/sdk-report"
38
+ try:
39
+ resp = requests.post(endpoint, json=report_data, headers=self._headers(), timeout=10)
40
+ return resp.status_code in [200, 201]
41
+ except Exception:
42
+ return False
@@ -0,0 +1,115 @@
1
+ Metadata-Version: 2.4
2
+ Name: dequa-sdk
3
+ Version: 1.0.0
4
+ Summary: Native Python SDK & Integrations for Dequa ETL Data Quality Engine
5
+ Author-email: Dequa ETL Team <support@dequa-etl.io>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/dequa-etl/dequa-etl-tool
8
+ Project-URL: Bug Tracker, https://github.com/dequa-etl/dequa-etl-tool/issues
9
+ Project-URL: Documentation, https://github.com/dequa-etl/dequa-etl-tool#readme
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Topic :: Database
16
+ Classifier: Topic :: Software Development :: Quality Assurance
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: requests>=2.28.0
20
+ Requires-Dist: pandas>=1.5.0
21
+ Requires-Dist: duckdb>=0.9.0
22
+ Requires-Dist: pydantic>=2.0.0
23
+ Provides-Extra: airflow
24
+ Requires-Dist: apache-airflow>=2.4.0; extra == "airflow"
25
+ Provides-Extra: dbt
26
+ Requires-Dist: dbt-core>=1.5.0; extra == "dbt"
27
+ Provides-Extra: all
28
+ Requires-Dist: apache-airflow>=2.4.0; extra == "all"
29
+ Requires-Dist: dbt-core>=1.5.0; extra == "all"
30
+
31
+ # ⚡ Dequa Python SDK
32
+
33
+ The official Python client and data validation SDK for **Dequa ETL**.
34
+
35
+ [![PyPI](https://img.shields.io/pypi/v/dequa-sdk.svg)](https://pypi.org/project/dequa-sdk/)
36
+ [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
37
+
38
+ ---
39
+
40
+ ## 📦 Installation
41
+
42
+ ```bash
43
+ pip install dequa-sdk
44
+ ```
45
+
46
+ With optional integrations:
47
+ ```bash
48
+ # Airflow integration
49
+ pip install dequa-sdk[airflow]
50
+
51
+ # dbt Core integration
52
+ pip install dequa-sdk[dbt]
53
+ ```
54
+
55
+ ---
56
+
57
+ ## 🚀 Quick Usage
58
+
59
+ ### 1. Vectorized In-Memory DataFrame Reconciliation
60
+
61
+ ```python
62
+ import pandas as pd
63
+ from dequa_sdk import validate
64
+
65
+ source_df = pd.read_csv("bronze_orders.csv")
66
+ target_df = pd.read_csv("gold_orders.csv")
67
+
68
+ # Run zero-config reconciliation
69
+ report = validate(
70
+ source=source_df,
71
+ target=target_df,
72
+ key_columns=["order_id"],
73
+ compare_columns=["amount", "status"]
74
+ )
75
+
76
+ print(report.summary())
77
+ if report.has_discrepancies():
78
+ print("Mismatched records found:", report.value_mismatches)
79
+ ```
80
+
81
+ ---
82
+
83
+ ### 2. Airflow DAG Gating Operator
84
+
85
+ ```python
86
+ from airflow import DAG
87
+ from dequa_sdk.integrations.airflow import DequaCheckOperator
88
+ from datetime import datetime
89
+
90
+ with DAG("daily_orders_pipeline", start_date=datetime(2026, 1, 1)) as dag:
91
+ assert_orders = DequaCheckOperator(
92
+ task_id="assert_orders_reconciliation",
93
+ pipeline_id=1,
94
+ dequa_url="http://dequa-backend:8000",
95
+ fail_on_mismatch=True
96
+ )
97
+ ```
98
+
99
+ ---
100
+
101
+ ### 3. dbt Manifest Parser
102
+
103
+ ```python
104
+ from dequa_sdk.integrations.dbt import DbtManifestParser
105
+
106
+ parser = DbtManifestParser(manifest_path="target/manifest.json")
107
+ models = parser.extract_models()
108
+
109
+ for model in models:
110
+ config = parser.generate_pipeline_config(
111
+ model_name=model["model_name"],
112
+ source_conn_id=1,
113
+ target_conn_id=2
114
+ )
115
+ ```
@@ -0,0 +1,23 @@
1
+ README.md
2
+ __init__.py
3
+ client.py
4
+ magics.py
5
+ pyproject.toml
6
+ quarantine.py
7
+ validators.py
8
+ ./__init__.py
9
+ ./client.py
10
+ ./magics.py
11
+ ./quarantine.py
12
+ ./validators.py
13
+ ./integrations/__init__.py
14
+ ./integrations/airflow.py
15
+ ./integrations/dbt.py
16
+ dequa_sdk.egg-info/PKG-INFO
17
+ dequa_sdk.egg-info/SOURCES.txt
18
+ dequa_sdk.egg-info/dependency_links.txt
19
+ dequa_sdk.egg-info/requires.txt
20
+ dequa_sdk.egg-info/top_level.txt
21
+ integrations/__init__.py
22
+ integrations/airflow.py
23
+ integrations/dbt.py
@@ -0,0 +1,14 @@
1
+ requests>=2.28.0
2
+ pandas>=1.5.0
3
+ duckdb>=0.9.0
4
+ pydantic>=2.0.0
5
+
6
+ [airflow]
7
+ apache-airflow>=2.4.0
8
+
9
+ [all]
10
+ apache-airflow>=2.4.0
11
+ dbt-core>=1.5.0
12
+
13
+ [dbt]
14
+ dbt-core>=1.5.0
@@ -0,0 +1 @@
1
+ dequa_sdk
@@ -0,0 +1,8 @@
1
+ """
2
+ Dequa SDK — Modern Data Stack Integrations (Airflow, dbt)
3
+ """
4
+
5
+ from dequa_sdk.integrations.airflow import DequaCheckOperator
6
+ from dequa_sdk.integrations.dbt import DbtManifestParser
7
+
8
+ __all__ = ["DequaCheckOperator", "DbtManifestParser"]
@@ -0,0 +1,85 @@
1
+ """
2
+ Dequa ETL — Apache Airflow Integration Operator
3
+ Allows seamless gating and automated quality checks within Airflow DAGs.
4
+ """
5
+
6
+ from typing import Optional, List, Dict, Any
7
+ import time
8
+
9
+ try:
10
+ from airflow.models import BaseOperator
11
+ from airflow.exceptions import AirflowException
12
+ except ImportError:
13
+ # Graceful fallback for non-Airflow execution environments
14
+ class BaseOperator:
15
+ def __init__(self, **kwargs):
16
+ pass
17
+
18
+ class AirflowException(Exception):
19
+ pass
20
+
21
+ from dequa_sdk.client import Client
22
+
23
+
24
+ class DequaCheckOperator(BaseOperator):
25
+ """
26
+ Airflow Operator that triggers a Dequa pipeline validation and asserts quality SLA.
27
+
28
+ Usage:
29
+ from dequa_sdk.integrations.airflow import DequaCheckOperator
30
+
31
+ check_orders = DequaCheckOperator(
32
+ task_id="assert_orders_reconciliation",
33
+ pipeline_id=1,
34
+ fail_on_mismatch=True,
35
+ dequa_url="http://dequa-backend:8000",
36
+ api_key="your_api_key",
37
+ )
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ pipeline_id: int,
43
+ dequa_url: str = "http://localhost:8000",
44
+ api_key: Optional[str] = None,
45
+ fail_on_mismatch: bool = True,
46
+ timeout_seconds: int = 300,
47
+ poll_interval: int = 5,
48
+ **kwargs
49
+ ):
50
+ super().__init__(**kwargs)
51
+ self.pipeline_id = pipeline_id
52
+ self.dequa_url = dequa_url
53
+ self.api_key = api_key
54
+ self.fail_on_mismatch = fail_on_mismatch
55
+ self.timeout_seconds = timeout_seconds
56
+ self.poll_interval = poll_interval
57
+
58
+ def execute(self, context: Any) -> Dict[str, Any]:
59
+ self.log.info(f"Triggering Dequa Pipeline ID: {self.pipeline_id}")
60
+ client = Client(base_url=self.dequa_url, api_key=self.api_key)
61
+
62
+ job = client.trigger_pipeline(self.pipeline_id)
63
+ job_id = job.get("id") or job.get("job_id")
64
+ self.log.info(f"Dequa Job {job_id} initiated. Polling for completion...")
65
+
66
+ start_time = time.time()
67
+ while time.time() - start_time < self.timeout_seconds:
68
+ status_data = client.get_job_status(job_id)
69
+ status = status_data.get("status")
70
+
71
+ if status in ("passed", "success"):
72
+ self.log.info(f"Dequa Job {job_id} PASSED with 100% record parity.")
73
+ return status_data
74
+ elif status in ("failed", "warning"):
75
+ mismatch_count = status_data.get("value_mismatches_count", 0) + status_data.get("missing_in_target_count", 0)
76
+ msg = f"Dequa Job {job_id} failed with {mismatch_count} discrepancies."
77
+ if self.fail_on_mismatch:
78
+ raise AirflowException(msg)
79
+ else:
80
+ self.log.warning(msg)
81
+ return status_data
82
+
83
+ time.sleep(self.poll_interval)
84
+
85
+ raise AirflowException(f"Dequa Job {job_id} timed out after {self.timeout_seconds} seconds.")
@@ -0,0 +1,80 @@
1
+ """
2
+ Dequa ETL — dbt Core Integration
3
+ Parses dbt manifest artifacts (manifest.json, catalog.json) to automatically generate
4
+ Dequa schema reconciliation pipelines and assertions.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ from typing import Dict, List, Any, Optional
10
+
11
+
12
+ class DbtManifestParser:
13
+ """
14
+ Introspects compiled dbt project manifests to extract models, primary keys,
15
+ and column schemas for automated Dequa pipeline generation.
16
+ """
17
+
18
+ def __init__(self, manifest_path: str, catalog_path: Optional[str] = None):
19
+ self.manifest_path = manifest_path
20
+ self.catalog_path = catalog_path
21
+ self.manifest_data = self._load_json(manifest_path)
22
+ self.catalog_data = self._load_json(catalog_path) if catalog_path and os.path.exists(catalog_path) else {}
23
+
24
+ def _load_json(self, path: str) -> Dict[str, Any]:
25
+ with open(path, "r", encoding="utf-8") as f:
26
+ return json.load(f)
27
+
28
+ def extract_models(self) -> List[Dict[str, Any]]:
29
+ """
30
+ Extracts all model definitions and unique keys from the dbt manifest.
31
+ """
32
+ nodes = self.manifest_data.get("nodes", {})
33
+ models = []
34
+
35
+ for node_id, node in nodes.items():
36
+ if node.get("resource_type") == "model":
37
+ name = node.get("name")
38
+ schema = node.get("schema")
39
+ database = node.get("database")
40
+ alias = node.get("alias", name)
41
+
42
+ # Extract primary/unique keys if defined in dbt tests or meta
43
+ columns = list(node.get("columns", {}).keys())
44
+ meta = node.get("meta", {})
45
+ primary_keys = meta.get("primary_key", [])
46
+ if isinstance(primary_keys, str):
47
+ primary_keys = [primary_keys]
48
+
49
+ models.append({
50
+ "model_name": name,
51
+ "target_table": f"{schema}.{alias}" if schema else alias,
52
+ "database": database,
53
+ "columns": columns,
54
+ "primary_keys": primary_keys,
55
+ "tags": node.get("tags", []),
56
+ })
57
+
58
+ return models
59
+
60
+ def generate_pipeline_config(self, model_name: str, source_conn_id: int, target_conn_id: int) -> Optional[Dict[str, Any]]:
61
+ """
62
+ Generates a complete Dequa pipeline configuration dict for a specific dbt model.
63
+ """
64
+ models = self.extract_models()
65
+ target = next((m for m in models if m["model_name"] == model_name), None)
66
+ if not target:
67
+ return None
68
+
69
+ primary_keys = target["primary_keys"] or (["id"] if "id" in target["columns"] else target["columns"][:1])
70
+
71
+ return {
72
+ "name": f"dbt Reconciliation: {model_name}",
73
+ "source_conn_id": source_conn_id,
74
+ "target_conn_id": target_conn_id,
75
+ "source_table": f"stg_{model_name}",
76
+ "target_table": target["target_table"],
77
+ "key_columns": primary_keys,
78
+ "compare_columns": [{"source": c, "target": c} for c in target["columns"] if c not in primary_keys],
79
+ "tags": ["dbt-generated", *target["tags"]],
80
+ }
@@ -0,0 +1,67 @@
1
+ """
2
+ Dequa IPython Magic Extensions for Jupyter & Databricks Notebooks
3
+ """
4
+
5
+ from typing import Any
6
+ import pandas as pd
7
+
8
+ try:
9
+ from IPython.core.magic import Magics, magics_class, line_magic, cell_magic
10
+ except ImportError:
11
+ # Graceful fallback if IPython is not installed
12
+ class Magics:
13
+ pass
14
+ def magics_class(cls): return cls
15
+ def line_magic(func): return func
16
+ def cell_magic(func): return func
17
+
18
+ from dequa_sdk.validators import validate
19
+ from dequa_sdk.quarantine import QuarantineRouter
20
+
21
+
22
+ @magics_class
23
+ class DequaMagics(Magics):
24
+ """IPython magic commands for interactive data quality testing."""
25
+
26
+ @line_magic
27
+ def dequa_profile(self, line: str):
28
+ """Line magic to quickly profile a pandas dataframe variable: %dequa_profile my_df"""
29
+ var_name = line.strip()
30
+ if not var_name or var_name not in self.shell.user_ns:
31
+ print(f"Error: Variable '{var_name}' not found in notebook namespace.")
32
+ return
33
+
34
+ val = self.shell.user_ns[var_name]
35
+ if isinstance(val, pd.DataFrame):
36
+ print(f"━━━ Dequa Profile: {var_name} ━━━")
37
+ print(f"Shape: {val.shape[0]} rows × {val.shape[1]} columns")
38
+ print(f"Null Cells Total: {val.isna().sum().sum()}")
39
+ print(f"Duplicate Rows: {val.duplicated().sum()}")
40
+ print("\nColumn Null Ratios:")
41
+ print((val.isna().mean() * 100.0).round(2).to_string())
42
+
43
+ @cell_magic
44
+ def dequa_test(self, line: str, cell: str):
45
+ """Cell magic to run assertions on a dataframe variable."""
46
+ var_name = line.strip()
47
+ if not var_name or var_name not in self.shell.user_ns:
48
+ print(f"Error: Variable '{var_name}' not found.")
49
+ return
50
+
51
+ df = self.shell.user_ns[var_name]
52
+ rules = [line.strip() for line in cell.strip().split("\n") if line.strip() and not line.startswith("#")]
53
+
54
+ report = validate(df, rules, dataset_name=var_name)
55
+ print(f"━━━ Dequa Validation Results for '{var_name}' ━━━")
56
+ print(f"Status: {'PASS ✅' if report.is_success else 'FAIL ❌'}")
57
+ print(f"Pass Rate: {report.pass_rate}% ({len(report.passed_rules)}/{len(rules)} rules passed)")
58
+
59
+ if report.failed_rules:
60
+ print(f"\nFailed Rules ({len(report.failed_rules)}):")
61
+ for r in report.failed_rules:
62
+ print(f" - ❌ {r}")
63
+
64
+
65
+ def load_ipython_extension(ipython):
66
+ """Entry point for %load_ext dequa_sdk.magics"""
67
+ ipython.register_magics(DequaMagics)
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "dequa-sdk"
7
+ version = "1.0.0"
8
+ description = "Native Python SDK & Integrations for Dequa ETL Data Quality Engine"
9
+ readme = "README.md"
10
+ authors = [{ name = "Dequa ETL Team", email = "support@dequa-etl.io" }]
11
+ license = "Apache-2.0"
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.10",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Database",
19
+ "Topic :: Software Development :: Quality Assurance",
20
+ ]
21
+ requires-python = ">=3.9"
22
+ dependencies = [
23
+ "requests>=2.28.0",
24
+ "pandas>=1.5.0",
25
+ "duckdb>=0.9.0",
26
+ "pydantic>=2.0.0",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ airflow = [
31
+ "apache-airflow>=2.4.0",
32
+ ]
33
+ dbt = [
34
+ "dbt-core>=1.5.0",
35
+ ]
36
+ all = [
37
+ "apache-airflow>=2.4.0",
38
+ "dbt-core>=1.5.0",
39
+ ]
40
+
41
+ [tool.setuptools]
42
+ packages = ["dequa_sdk", "dequa_sdk.integrations"]
43
+ package-dir = {"dequa_sdk" = "."}
44
+
45
+ [project.urls]
46
+ "Homepage" = "https://github.com/dequa-etl/dequa-etl-tool"
47
+ "Bug Tracker" = "https://github.com/dequa-etl/dequa-etl-tool/issues"
48
+ "Documentation" = "https://github.com/dequa-etl/dequa-etl-tool#readme"
@@ -0,0 +1,44 @@
1
+ """
2
+ Dequa SDK Dynamic Quarantine Router
3
+
4
+ Splits clean rows from corrupted rows violating validation assertions,
5
+ routing invalid records to quarantine storage (DataFrame, CSV, or DB table).
6
+ """
7
+
8
+ from typing import Any, Tuple
9
+ import pandas as pd
10
+ from dequa_sdk.validators import ValidationReport
11
+
12
+
13
+ class QuarantineRouter:
14
+ """Routes invalid records into quarantine isolated data streams."""
15
+
16
+ @staticmethod
17
+ def route(df: Any, report: ValidationReport) -> Tuple[pd.DataFrame, pd.DataFrame]:
18
+ """
19
+ Splits input DataFrame into (clean_df, quarantined_df).
20
+ """
21
+ if hasattr(df, "toPandas"):
22
+ pdf = df.toPandas()
23
+ elif isinstance(df, pd.DataFrame):
24
+ pdf = df
25
+ else:
26
+ pdf = pd.DataFrame(df)
27
+
28
+ if not report.quarantine_indices:
29
+ return pdf, pd.DataFrame(columns=pdf.columns)
30
+
31
+ quarantine_set = set(report.quarantine_indices)
32
+ clean_mask = ~pdf.index.isin(quarantine_set)
33
+ quarantine_mask = pdf.index.isin(quarantine_set)
34
+
35
+ clean_df = pdf[clean_mask].copy()
36
+ quarantined_df = pdf[quarantine_mask].copy()
37
+
38
+ return clean_df, quarantined_df
39
+
40
+ @staticmethod
41
+ def export_quarantine(quarantined_df: pd.DataFrame, output_path: str) -> str:
42
+ """Exports quarantined records to CSV file path."""
43
+ quarantined_df.to_csv(output_path, index=False)
44
+ return output_path
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,150 @@
1
+ """
2
+ Dequa SDK DataFrame Validation Engine
3
+
4
+ Provides inline validation rules for Pandas, Polars, and PySpark DataFrames.
5
+ """
6
+
7
+ from typing import Any, Dict, List, Optional, Tuple, Union
8
+ import re
9
+ import pandas as pd
10
+
11
+
12
+ class ValidationReport:
13
+ """Validation execution result details."""
14
+
15
+ def __init__(self, dataset_name: str, total_rows: int):
16
+ self.dataset_name = dataset_name
17
+ self.total_rows = total_rows
18
+ self.passed_rules: List[str] = []
19
+ self.failed_rules: List[str] = []
20
+ self.rule_details: List[Dict[str, Any]] = []
21
+ self.quarantine_indices: List[int] = []
22
+
23
+ @property
24
+ def is_success(self) -> bool:
25
+ return len(self.failed_rules) == 0
26
+
27
+ @property
28
+ def pass_rate(self) -> float:
29
+ total = len(self.passed_rules) + len(self.failed_rules)
30
+ return (len(self.passed_rules) / total * 100.0) if total > 0 else 100.0
31
+
32
+ def to_dict(self) -> Dict[str, Any]:
33
+ return {
34
+ "dataset_name": self.dataset_name,
35
+ "total_rows": self.total_rows,
36
+ "is_success": self.is_success,
37
+ "pass_rate": round(self.pass_rate, 2),
38
+ "passed_rules_count": len(self.passed_rules),
39
+ "failed_rules_count": len(self.failed_rules),
40
+ "failed_rules": self.failed_rules,
41
+ "rule_details": self.rule_details,
42
+ "quarantined_rows_count": len(self.quarantine_indices)
43
+ }
44
+
45
+
46
+ def _parse_rule(rule_str: str) -> Tuple[str, List[str]]:
47
+ """Parse rule strings like 'not_null(customer_id)' into ('not_null', ['customer_id'])."""
48
+ match = re.match(r"^(\w+)\((.*)\)$", rule_str.strip())
49
+ if not match:
50
+ return rule_str.strip(), []
51
+ func = match.group(1).lower()
52
+ args_raw = match.group(2)
53
+ args = [arg.strip().strip("'\"") for arg in args_raw.split(",") if arg.strip()]
54
+ return func, args
55
+
56
+
57
+ def validate(
58
+ df: Any,
59
+ rules: List[str],
60
+ dataset_name: str = "dataset",
61
+ on_failure: str = "warn"
62
+ ) -> ValidationReport:
63
+ """
64
+ Validate a DataFrame against assertion rules.
65
+
66
+ :param df: Pandas DataFrame or PySpark DataFrame
67
+ :param rules: List of rule strings, e.g. ["not_null(id)", "min_value(amount, 0)", "unique(id)"]
68
+ :param dataset_name: Name identifier for the dataset
69
+ :param on_failure: Strategy when validation fails ("warn", "raise", "quarantine")
70
+ """
71
+ total_rows = len(df) if hasattr(df, "__len__") else 0
72
+ report = ValidationReport(dataset_name=dataset_name, total_rows=total_rows)
73
+
74
+ # Convert to Pandas if PySpark DataFrame passed
75
+ if hasattr(df, "toPandas"):
76
+ pdf = df.toPandas()
77
+ elif isinstance(df, pd.DataFrame):
78
+ pdf = df
79
+ else:
80
+ pdf = pd.DataFrame(df)
81
+
82
+ quarantine_indices = set()
83
+
84
+ for rule_str in rules:
85
+ func_name, args = _parse_rule(rule_str)
86
+ is_passed = True
87
+ failed_count = 0
88
+
89
+ if func_name == "not_null" and args:
90
+ col = args[0]
91
+ if col in pdf.columns:
92
+ null_mask = pdf[col].isna()
93
+ failed_count = int(null_mask.sum())
94
+ if failed_count > 0:
95
+ is_passed = False
96
+ quarantine_indices.update(pdf[null_mask].index.tolist())
97
+
98
+ elif func_name == "min_value" and len(args) >= 2:
99
+ col, min_val = args[0], float(args[1])
100
+ if col in pdf.columns:
101
+ min_mask = pdf[col] < min_val
102
+ failed_count = int(min_mask.sum())
103
+ if failed_count > 0:
104
+ is_passed = False
105
+ quarantine_indices.update(pdf[min_mask].index.tolist())
106
+
107
+ elif func_name == "max_value" and len(args) >= 2:
108
+ col, max_val = args[0], float(args[1])
109
+ if col in pdf.columns:
110
+ max_mask = pdf[col] > max_val
111
+ failed_count = int(max_mask.sum())
112
+ if failed_count > 0:
113
+ is_passed = False
114
+ quarantine_indices.update(pdf[max_mask].index.tolist())
115
+
116
+ elif func_name == "unique" and args:
117
+ col = args[0]
118
+ if col in pdf.columns:
119
+ dup_mask = pdf[col].duplicated(keep=False)
120
+ failed_count = int(dup_mask.sum())
121
+ if failed_count > 0:
122
+ is_passed = False
123
+ quarantine_indices.update(pdf[dup_mask].index.tolist())
124
+
125
+ elif func_name == "regex" and len(args) >= 2:
126
+ col, pattern = args[0], args[1]
127
+ if col in pdf.columns:
128
+ regex_mask = ~pdf[col].astype(str).str.contains(pattern, na=False, regex=True)
129
+ failed_count = int(regex_mask.sum())
130
+ if failed_count > 0:
131
+ is_passed = False
132
+ quarantine_indices.update(pdf[regex_mask].index.tolist())
133
+
134
+ if is_passed:
135
+ report.passed_rules.append(rule_str)
136
+ else:
137
+ report.failed_rules.append(rule_str)
138
+
139
+ report.rule_details.append({
140
+ "rule": rule_str,
141
+ "status": "PASS" if is_passed else "FAIL",
142
+ "failed_records_count": failed_count
143
+ })
144
+
145
+ report.quarantine_indices = sorted(list(quarantine_indices))
146
+
147
+ if not report.is_success and on_failure == "raise":
148
+ raise ValueError(f"Dequa Validation Failed for '{dataset_name}': {len(report.failed_rules)} rules violated.")
149
+
150
+ return report