Sitemap

Great Expectations Data Quality Validation using GitHub— Part2

--

Press enter or click to view image in full size

Introduction

This is a part2 of Great Expectations with GitHub, as in earlier part we have seen how to start with Great Expectations usage with Databricks. Now let's check how we can leverage the same using GitHub to make it as Re-Usable component.

Imagine if your data quality checks ran automatically every time someone made changes to your code or data. No more manual testing, no more forgetting to run validations, and no more broken pipelines reaching production!

Press enter or click to view image in full size

Why Automate Data Quality?

Developer commits code GitHub Actions automatically triggers Great Expectations runs all checks finally Issues caught before production.

Complete CI/CD Architecture

when someone Pushes code to feature/dataquality branch GitHub Actions
Automatically triggers validation workflow. It will connect to real Databricks data may be on Azure SQL Server or Unity Catalog. Followed by Running comprehensive quality checks based on Business Rules/ Table rules etc (Defined in Part1). It will run Deep data analysis
with quality scores and generate Detailed JSON results
and insights. Finally your new Rules/Conditions will marked as Pass/Fail
Automatic approval or rejection.

The 7-Step Automation Process

Here are the steps to achieve Great expectations validations using Github actions. The workflow automatically starts when you push code to the feature/dataquality branch or manually trigger it.

Step 1: Trigger Setup

The workflow automatically starts when you push code to the feature/dataquality branch or manually trigger it.

Step 2: Environment Preparation

GitHub Actions sets up with Ubuntu environment with Python 3.12 and all required packages. Make sure to select latest version of Python packages.

      - name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.12'

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential tree

- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install databricks-sql-connector[pyarrow] great-expectations pandas sqlalchemy pyyaml
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

Here is my requirements.txt


great-expectations
pyodbc>=4.0.0
pandas>=2.0.0
sqlalchemy>=2.0.0
pyyaml>=6.0
numpy>=1.24.0


matplotlib>=3.5.0
seaborn>=0.11.0
plotly>=5.0.0
jinja2>=3.1.0


python-dotenv>=1.0.0
click>=8.0.0
pyyaml>=6.0.0
tabulate>=0.9.0


numpy>=1.21.0
scipy>=1.7.0

Step 3: Secure Connection to Databricks

Using GitHub Secrets, we securely connect to your Databricks Unity Catalog without exposing credentials.

          DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }}
DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
HTTP_PATH: ${{ secrets.HTTP_PATH }}

catalog = ${{ secrets.CATALOG}}
schema = ${{ secrets.SCHEMA}}
table = ${{ secrets.TABLE-NAME}}

Step 4: Data Loading & Validation

The system loads up to 1,000 records from Unity Catalog and runs comprehensive Great Expectations checks. only for testing I used 1000 records but in real time Ingested over 1 million records and took less than 2 hours to finish entire execution.

 cursor.execute(f"SELECT * FROM `{catalog}`.`{schema}`.`{table}` LIMIT 1000")


def load_unity_catalog_data():
host = os.getenv("DATABRICKS_HOST")
token = os.getenv("DATABRICKS_TOKEN")
http_path = os.getenv("HTTP_PATH")

catalog = "CATALOG-NAME"
schema = "SCHEMA-NAME"
table = "TABLE-NAME"

with sql.connect(
server_hostname=host,
http_path=http_path,
access_token=token
) as connection:
with connection.cursor() as cursor:
cursor.execute(f"USE CATALOG `{catalog}`")
cursor.execute(f"USE SCHEMA `{schema}`")
cursor.execute(f"DESCRIBE TABLE `{catalog}`.`{schema}`.`{table}`")
columns_info = cursor.fetchall()
column_names = [col[0] for col in columns_info]
cursor.execute(f"SELECT * FROM `{catalog}`.`{schema}`.`{table}` LIMIT 1000")
rows = cursor.fetchall()
df = pd.DataFrame(rows, columns=column_names)
return df, f"{catalog}.{schema}.{table}"

def perform_gx_data_profiling(context, df, table_name):

Step 5: Advanced Data Profiling

Great Expectations performs deep data profiling, analyzing every column and generating quality scores.

try:

datasource_name = "profiling_datasource"

try:

print(f"Creating new Fluent datasource: {datasource_name}")
datasource = context.data_sources.add_pandas(name=datasource_name)
except Exception as ds_error:

print(f"Datasource creation failed, trying to get existing: {ds_error}")
try:
datasource = context.data_sources.get(datasource_name)
print(f"Using existing datasource: {datasource_name}")
except Exception as get_ds_error:
print(f"Neither create nor get datasource worked: {get_ds_error}")
raise Exception(f"Cannot create or get datasource '{datasource_name}': create={ds_error}, get={get_ds_error}")


data_asset_name = "unity_catalog_data"
try:

print(f"Creating new asset: {data_asset_name}")
data_asset = datasource.add_dataframe_asset(name=data_asset_name)
except Exception as asset_error:

print(f"Asset creation failed, trying to get existing: {asset_error}")
try:
data_asset = datasource.get_asset(data_asset_name)
print(f"Using existing asset: {data_asset_name}")
except Exception as get_error:
print(f"Neither create nor get worked: {get_error}")
raise Exception(f"Cannot create or get asset '{data_asset_name}': create={asset_error}, get={get_error}")


batch_definition_name = "profiling_batch"
try:

print(f"Creating new batch definition: {batch_definition_name}")
batch_definition = data_asset.add_batch_definition_whole_dataframe(batch_definition_name)
except Exception as batch_def_error:

print(f"Batch definition creation failed, trying to get existing: {batch_def_error}")
try:
batch_definition = data_asset.get_batch_definition(batch_definition_name)
print(f"Using existing batch definition: {batch_definition_name}")
except Exception as get_batch_error:
print(f"Neither create nor get batch definition worked: {get_batch_error}")
raise Exception(f"Cannot create or get batch definition '{batch_definition_name}': create={batch_def_error}, get={get_batch_error}")


print("Getting batch with dataframe...")
batch = batch_definition.get_batch(batch_parameters={"dataframe": df})


print("Creating validator...")
validator = context.get_validator(batch=batch)
print("Great Expectations validator created successfully")


profile_results = {
"metadata": {
"table_name": table_name,
"profiling_timestamp": datetime.now().isoformat(),
"sample_size": len(df),
"profiling_method": "great_expectations"
},
"table_summary": {},
"column_profiles": {},
"gx_expectations_generated": [],
"data_quality_insights": []
}


print("Running table-level profiling...")

Step 6: Intelligent Reporting

Results are automatically formatted into easy-to-read reports with actionable insights.

  profile_results["table_summary"].update({
"total_gx_expectations": total_expectations,
"successful_gx_expectations": successful_expectations,
"gx_success_rate": round((successful_expectations / total_expectations * 100), 2) if total_expectations > 0 else 0,
"duplicate_rows": int(df.duplicated().sum()),
"duplicate_percentage": round(df.duplicated().sum() / len(df) * 100, 2),
"overall_completeness": round(sum(col["completeness_score"] for col in profile_results["column_profiles"].values()) / len(profile_results["column_profiles"]), 2)
})

# Save profiling results
with open("gx_data_profiling_results.json", "w") as f:
json.dump(profile_results, f, indent=2, default=str)

print(f"GX data profiling completed for {len(profile_results['column_profiles'])} columns")
print(f"Total GX expectations: {total_expectations}")
print(f"Successful expectations: {successful_expectations} ({profile_results['table_summary']['gx_success_rate']:.1f}%)")
print(f"Overall completeness: {profile_results['table_summary']['overall_completeness']:.1f}%")
print(f"Quality insights: {len(profile_results['data_quality_insights'])} issues identified")

return profile_results

Step 7: Automatic Artifacts & Results

All results are automatically saved as downloadable artifacts that persist for 30 days based on the retention policy.

 - name: Upload Great Expectations artifacts
uses: actions/upload-artifact@v4
if: always()
with:
name: "great-expectations-artifacts-gx-profiling"
path: |
great_expectations/
data_quality_report.json
gx_data_profiling_results.json
unity_catalog_data_quality.py
retention-days: 30

Here is the full YML file for anyone to use. (FYI.I just used some dummy data to run validation)

name: Databricks Unity Catalog with profiling

on:
push:
branches:
- feature/dataquality
workflow_dispatch:

jobs:
Query-Databricks:
runs-on: ubuntu-latest
environment: develop

steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.12'

- name: Cache pip dependencies
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-

- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential tree

- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install databricks-sql-connector[pyarrow] great-expectations pandas sqlalchemy pyyaml
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

- name: Create and Run Great Expectations Validation
env:
DATABRICKS_HOST: ${{ secrets.DATABRICKS_HOST }}
DATABRICKS_TOKEN: ${{ secrets.DATABRICKS_TOKEN }}
HTTP_PATH: ${{ secrets.HTTP_PATH }}
run: |
cat << 'EOF' > unity_catalog_data_quality.py
import os
import sys
import pandas as pd
import json
from datetime import datetime
import great_expectations as gx
from databricks import sql

def create_gx_context():
os.makedirs("./great_expectations", exist_ok=True)
context = gx.get_context(context_root_dir="./great_expectations")
return context

def load_unity_catalog_data():
host = os.getenv("DATABRICKS_HOST")
token = os.getenv("DATABRICKS_TOKEN")
http_path = os.getenv("HTTP_PATH")

catalog = "CATALOG-NAME"
schema = "SCHEMA-NAME"
table = "TABLE-NAME"

with sql.connect(
server_hostname=host,
http_path=http_path,
access_token=token
) as connection:
with connection.cursor() as cursor:
cursor.execute(f"USE CATALOG `{catalog}`")
cursor.execute(f"USE SCHEMA `{schema}`")
cursor.execute(f"DESCRIBE TABLE `{catalog}`.`{schema}`.`{table}`")
columns_info = cursor.fetchall()
column_names = [col[0] for col in columns_info]
cursor.execute(f"SELECT * FROM `{catalog}`.`{schema}`.`{table}` LIMIT 1000")
rows = cursor.fetchall()
df = pd.DataFrame(rows, columns=column_names)
return df, f"{catalog}.{schema}.{table}"

def perform_gx_data_profiling(context, df, table_name):



try:

datasource_name = "profiling_datasource"

try:

print(f"Creating new Fluent datasource: {datasource_name}")
datasource = context.data_sources.add_pandas(name=datasource_name)
except Exception as ds_error:
# If creation fails, try to get existing
print(f"Datasource creation failed, trying to get existing: {ds_error}")
try:
datasource = context.data_sources.get(datasource_name)
print(f"Using existing datasource: {datasource_name}")
except Exception as get_ds_error:
print(f"Neither create nor get datasource worked: {get_ds_error}")
raise Exception(f"Cannot create or get datasource '{datasource_name}': create={ds_error}, get={get_ds_error}")

# Create data asset from the dataframe
data_asset_name = "unity_catalog_data"
try:
# Create new asset (don't try to get existing first)
print(f"Creating new asset: {data_asset_name}")
data_asset = datasource.add_dataframe_asset(name=data_asset_name)
except Exception as asset_error:
# If creation fails, try to get existing
print(f"Asset creation failed, trying to get existing: {asset_error}")
try:
data_asset = datasource.get_asset(data_asset_name)
print(f"Using existing asset: {data_asset_name}")
except Exception as get_error:
print(f"Neither create nor get worked: {get_error}")
raise Exception(f"Cannot create or get asset '{data_asset_name}': create={asset_error}, get={get_error}")

# Create batch definition and get batch using Fluent API
batch_definition_name = "profiling_batch"
try:
# Create new batch definition (don't try to get existing first)
print(f"Creating new batch definition: {batch_definition_name}")
batch_definition = data_asset.add_batch_definition_whole_dataframe(batch_definition_name)
except Exception as batch_def_error:
# If creation fails, try to get existing
print(f"Batch definition creation failed, trying to get existing: {batch_def_error}")
try:
batch_definition = data_asset.get_batch_definition(batch_definition_name)
print(f"Using existing batch definition: {batch_definition_name}")
except Exception as get_batch_error:
print(f"Neither create nor get batch definition worked: {get_batch_error}")
raise Exception(f"Cannot create or get batch definition '{batch_definition_name}': create={batch_def_error}, get={get_batch_error}")

# Get the actual batch with our dataframe
print("Getting batch with dataframe...")
batch = batch_definition.get_batch(batch_parameters={"dataframe": df})

# Get validator for profiling
print("Creating validator...")
validator = context.get_validator(batch=batch)
print("Great Expectations validator created successfully")

# Use Great Expectations profiler
profile_results = {
"metadata": {
"table_name": table_name,
"profiling_timestamp": datetime.now().isoformat(),
"sample_size": len(df),
"profiling_method": "great_expectations"
},
"table_summary": {},
"column_profiles": {},
"gx_expectations_generated": [],
"data_quality_insights": []
}

# Basic table profiling using GX expectations
print("Running table-level profiling...")

# Table row count
row_count_result = validator.expect_table_row_count_to_be_between(min_value=0, max_value=1000000)
profile_results["table_summary"]["row_count"] = len(df)
profile_results["table_summary"]["row_count_validation"] = row_count_result.success

# Table column count
column_count_result = validator.expect_table_column_count_to_equal(value=len(df.columns))
profile_results["table_summary"]["column_count"] = len(df.columns)
profile_results["table_summary"]["column_count_validation"] = column_count_result.success

# Profile each column using GX
print("Running column-level profiling...")
for column in df.columns:
col_profile = {
"column_name": column,
"data_type": str(df[column].dtype),
"gx_expectations": []
}

# Column exists check
exists_result = validator.expect_column_to_exist(column=column)
col_profile["gx_expectations"].append({
"expectation": "expect_column_to_exist", #GX library
"success": exists_result.success,
"result": exists_result.to_json_dict()
})

# Null value profiling
null_result = validator.expect_column_values_to_not_be_null(column=column)
col_profile["gx_expectations"].append({
"expectation": "expect_column_values_to_not_be_null", #GX library
"success": null_result.success,
"result": null_result.to_json_dict()
})

# Extract key metrics from GX results
null_count = null_result.result.get("unexpected_count", 0)
total_count = null_result.result.get("element_count", len(df))
null_percentage = (null_count / total_count * 100) if total_count > 0 else 0

col_profile.update({
"null_count": null_count,
"null_percentage": round(null_percentage, 2),
"non_null_count": total_count - null_count,
"completeness_score": round(100 - null_percentage, 2)
})

# Unique values profiling
try:
unique_result = validator.expect_column_values_to_be_unique(column=column)
col_profile["gx_expectations"].append({
"expectation": "expect_column_values_to_be_unique", #GX library
"success": unique_result.success,
"result": unique_result.to_json_dict()
})

# Extract unique count from result
unique_count = df[column].nunique()
col_profile["unique_count"] = unique_count
col_profile["unique_percentage"] = round((unique_count / len(df)) * 100, 2)

except Exception as e:
print(f"Warning: Could not profile uniqueness for {column}: {e}")

# Numeric column profiling
if pd.api.types.is_numeric_dtype(df[column]):
try:
# Min/Max validation
min_val = float(df[column].min())
max_val = float(df[column].max())

range_result = validator.expect_column_values_to_be_between(
column=column,
min_value=min_val,
max_value=max_val
)
col_profile["gx_expectations"].append({
"expectation": "expect_column_values_to_be_between", #GX library
"success": range_result.success,
"result": range_result.to_json_dict()
})

col_profile.update({
"min_value": min_val,
"max_value": max_val,
"mean": float(df[column].mean()),
"median": float(df[column].median())
})

except Exception as e:
print(f"Warning: Could not profile numeric range for {column}: {e}")

# String column profiling
elif pd.api.types.is_string_dtype(df[column]) or pd.api.types.is_object_dtype(df[column]):
try:
# Length validation
lengths = df[column].dropna().astype(str).str.len()
if len(lengths) > 0:
min_len = int(lengths.min())
max_len = int(lengths.max())

length_result = validator.expect_column_value_lengths_to_be_between(
column=column,
min_value=min_len,
max_value=max_len
)
col_profile["gx_expectations"].append({
"expectation": "expect_column_value_lengths_to_be_between", #GX library
"success": length_result.success,
"result": length_result.to_json_dict()
})

col_profile.update({
"min_length": min_len,
"max_length": max_len,
"avg_length": round(lengths.mean(), 2)
})
except Exception as e:
print(f"Warning: Could not profile string length for {column}: {e}")

# Generate insights based on GX results
if null_percentage > 50:
profile_results["data_quality_insights"].append({
"type": "HIGH_NULL_RATE",
"column": column,
"severity": "warning",
"description": f"Column {column} has {null_percentage:.1f}% null values",
"gx_based": True
})

if col_profile.get("unique_count") == 1:
profile_results["data_quality_insights"].append({
"type": "CONSTANT_VALUE",
"column": column,
"severity": "info",
"description": f"Column {column} has constant value",
"gx_based": True
})

profile_results["column_profiles"][column] = col_profile

# Calculate overall metrics
total_expectations = sum(len(col["gx_expectations"]) for col in profile_results["column_profiles"].values())
successful_expectations = sum(
sum(1 for exp in col["gx_expectations"] if exp["success"])
for col in profile_results["column_profiles"].values()
)

profile_results["table_summary"].update({
"total_gx_expectations": total_expectations,
"successful_gx_expectations": successful_expectations,
"gx_success_rate": round((successful_expectations / total_expectations * 100), 2) if total_expectations > 0 else 0,
"duplicate_rows": int(df.duplicated().sum()),
"duplicate_percentage": round(df.duplicated().sum() / len(df) * 100, 2),
"overall_completeness": round(sum(col["completeness_score"] for col in profile_results["column_profiles"].values()) / len(profile_results["column_profiles"]), 2)
})

# Save profiling results
with open("gx_data_profiling_results.json", "w") as f:
json.dump(profile_results, f, indent=2, default=str)

print(f"GX data profiling completed for {len(profile_results['column_profiles'])} columns")
print(f"Total GX expectations: {total_expectations}")
print(f"Successful expectations: {successful_expectations} ({profile_results['table_summary']['gx_success_rate']:.1f}%)")
print(f"Overall completeness: {profile_results['table_summary']['overall_completeness']:.1f}%")
print(f"Quality insights: {len(profile_results['data_quality_insights'])} issues identified")

return profile_results

except Exception as e:


import traceback
traceback.print_exc()
# Do not return None - raise the exception to fail the workflow
raise Exception(f"Great Expectations profiling failed (mandatory): {e}")

def run_data_quality_validation():



try:
context = create_gx_context()
df, table_name = load_unity_catalog_data()

print(f"Data Quality Validation: {table_name}")
print(f"Rows: {df.shape[0]:,} | Columns: {df.shape[1]}")
print(f"Validation Engine: Great Expectations (Fluent API) - MANDATORY")

# Step 1: Perform Great Expectations data profiling (MANDATORY)
profiling_results = perform_gx_data_profiling(context, df, table_name)

# ENFORCE: Great Expectations is mandatory - fail if profiling failed
if profiling_results is None:
raise Exception("CRITICAL: Great Expectations profiling failed. GX is mandatory for this workflow.")

print("Great Expectations profiling completed successfully")

# Step 2: Great Expectations validation using the SAME Fluent API approach
results = []

print("Running Great Expectations validations using Fluent API...")

# Use the same Fluent Datasource approach as profiling (NO FALLBACK)
try:
# Get the same datasource used for profiling
datasource_name = "profiling_datasource"
datasource = context.data_sources.get(datasource_name)

# Get the same asset and batch definition created during profiling
data_asset = datasource.get_asset("unity_catalog_data")
batch_definition = data_asset.get_batch_definition("profiling_batch")

# Create a new batch with the same dataframe for validation
batch = batch_definition.get_batch(batch_parameters={"dataframe": df})
validator = context.get_validator(batch=batch)

print("Great Expectations validator created using Fluent API")

# Run Great Expectations validations (MANDATORY)
result1 = validator.expect_table_row_count_to_be_between(min_value=1, max_value=100000)
results.append(result1)
print(f"Table row count validation: {'PASSED' if result1.success else 'FAILED'}")

critical_columns = ["WellCode", "Process", "record_create_date"]
for col in critical_columns:
if col in df.columns:
result = validator.expect_column_values_to_not_be_null(column=col)
results.append(result)
print(f"Column '{col}' null check: {'PASSED' if result.success else 'FAILED'}")

for col in critical_columns:
if col in df.columns:
result = validator.expect_column_to_exist(column=col)
results.append(result)
print(f"Column '{col}' exists check: {'PASSED' if result.success else 'FAILED'}")

print(f"All Great Expectations validations completed using Fluent API")

except Exception as gx_error:
print(f"CRITICAL ERROR: Great Expectations validation failed: {gx_error}")
import traceback
traceback.print_exc()
raise Exception(f"Great Expectations is mandatory but failed: {gx_error}")


success_count = sum(1 for r in results if r.get("success", False))
total_count = len(results)
success_rate = float(success_count / total_count * 100) if total_count > 0 else 0.0
overall_success = all(r.get("success", False) for r in results)

print(f"Validation Results: {success_count}/{total_count} passed ({success_rate:.1f}%)")
print(f"Status: {'PASSED' if overall_success else 'FAILED'}")

# Enhanced report with profiling data
report = {
"timestamp": datetime.now().isoformat(),
"table": table_name,
"validation_method": "great_expectations_fluent_api_mandatory",
"gx_enforcement": "",
"validation_summary": {
"total_checks": total_count,
"passed_checks": success_count,
"success_rate": success_rate,
"overall_success": bool(overall_success),
"gx_api_used": "Fluent API",
"gx_validation_engine": "Great Expectations (enforced)"
},
"data_summary": {
"row_count": int(len(df)),
"column_count": int(len(df.columns)),
"critical_columns": ["WellCode", "Process", "record_create_date"],
"available_columns": list(df.columns)
},
"profiling_summary": {}
}

# Add profiling summary if available
if profiling_results:
report["profiling_summary"] = {
"profiling_method": "great_expectations",
"table_completeness": profiling_results["table_summary"]["overall_completeness"],
"duplicate_percentage": profiling_results["table_summary"]["duplicate_percentage"],
"gx_success_rate": profiling_results["table_summary"]["gx_success_rate"],
"total_gx_expectations": profiling_results["table_summary"]["total_gx_expectations"],
"quality_issues_count": len(profiling_results["data_quality_insights"]),
"column_quality_scores": {}
}

# Add column quality scores based on GX results
for col_name, col_profile in profiling_results["column_profiles"].items():
quality_score = col_profile["completeness_score"]
# Bonus for successful GX expectations
successful_expectations = sum(1 for exp in col_profile["gx_expectations"] if exp["success"])
total_expectations = len(col_profile["gx_expectations"])
if total_expectations > 0:
gx_bonus = (successful_expectations / total_expectations) * 10 # Up to 10 bonus points
quality_score = min(100, quality_score + gx_bonus)

report["profiling_summary"]["column_quality_scores"][col_name] = round(quality_score, 1)

with open("data_quality_report.json", "w") as f:
json.dump(report, f, indent=2)

return overall_success

except Exception as e:
print(f"Validation failed: {str(e)}")
return False

if __name__ == "__main__":
success = run_data_quality_validation()
sys.exit(0 if success else 1)
EOF

echo "Running Great Expectations validation..."
python unity_catalog_data_quality.py

- name: Display validation results
if: always()
run: |


if [ -f "data_quality_report.json" ]; then

cat data_quality_report.json | python -c "
import json, sys
data = json.load(sys.stdin)
vs = data.get('validation_summary', {})
ds = data.get('data_summary', {})
ps = data.get('profiling_summary', {})

print(f\"Status: {vs.get('overall_success', 'Unknown')}\")
print(f\"Success Rate: {vs.get('success_rate', 0):.1f}% ({vs.get('passed_checks', 0)}/{vs.get('total_checks', 0)})\")
print(f\"Table: {data.get('table', 'N/A')}\")
print(f\"Rows: {ds.get('row_count', 'N/A'):,}\")
print(f\"Columns: {ds.get('column_count', 'N/A')}\")
print(f\"Validation Method: {data.get('validation_method', 'N/A')}\")
print(f\"GX Enforcement: {data.get('gx_enforcement', 'N/A')}\")
print(f\"GX API Used: {vs.get('gx_api_used', 'N/A')}\")
print(f\"Validation Engine: {vs.get('gx_validation_engine', 'N/A')}\")
print(f\"\\nGREAT EXPECTATIONS STATUS: ENFORCED\")

if ps:
print(f\"Profiling Method: {ps.get('profiling_method', 'N/A')}\")
print(f\"Table Completeness: {ps.get('table_completeness', 0):.1f}%\")
print(f\"Duplicate Rate: {ps.get('duplicate_percentage', 0):.1f}%\")
print(f\"GX Success Rate: {ps.get('gx_success_rate', 0):.1f}%\")
print(f\"Total GX Expectations: {ps.get('total_gx_expectations', 0)}\")
print(f\"Quality Issues: {ps.get('quality_issues_count', 0)}\")

if ps.get('column_quality_scores'):
print(f\"\\nCOLUMN QUALITY SCORES (GX-based)\")
for col, score in ps['column_quality_scores'].items():
status = 'GOOD' if score >= 80 else 'MEDIUM' if score >= 60 else 'POOR'
print(f\" {col}: {score}% ({status})\")
"
else
echo "No validation report found"
fi

echo ""
if [ -f "gx_data_profiling_results.json" ]; then

python -c "
import json
with open('gx_data_profiling_results.json', 'r') as f:
profile = json.load(f)

insights = profile.get('data_quality_insights', [])
if insights:
print('Quality Issues Found (GX-based):')
for insight in insights[:5]: # Show first 5 issues
severity_icon = 'ERROR' if insight['severity'] == 'error' else 'WARNING' if insight['severity'] == 'warning' else 'INFO'
gx_marker = ' [GX]' if insight.get('gx_based') else ''
print(f\" {severity_icon}: {insight['type']}: {insight['description']}{gx_marker}\")
if len(insights) > 5:
print(f\" ... and {len(insights) - 5} more issues (see GX profiling artifact)\")
else:
print('No significant quality issues detected by Great Expectations')

# Show some column details
columns = profile.get('column_profiles', {})
if columns:
print(f\"\\nColumn GX Expectations Summary:\")
for col_name, col_data in list(columns.items())[:3]: # Show first 3 columns
gx_expectations = col_data.get('gx_expectations', [])
successful = sum(1 for exp in gx_expectations if exp['success'])
total = len(gx_expectations)
print(f\" {col_name}: {successful}/{total} GX expectations passed\")
if len(columns) > 3:
print(f\" ... and {len(columns) - 3} more columns (see full artifact)\")
"
else
echo "No GX profiling results found"
fi

- name: Upload Great Expectations artifacts
uses: actions/upload-artifact@v4
if: always()
with:
name: "great-expectations-artifacts-gx-profiling"
path: |
great_expectations/
data_quality_report.json
gx_data_profiling_results.json
unity_catalog_data_quality.py
retention-days: 30

Workflow Output:

Press enter or click to view image in full size

Validation Results output

Press enter or click to view image in full size

Key Benefits:

Automation Benefits

  • Zero Manual Work: Runs automatically on code changes
  • Instant Feedback: Know within minutes if data quality is good
  • Prevent Issues: Catch problems before they reach production
  • Team Collaboration: Everyone sees the same results
  • Historical Tracking: See data quality trends over time

Technical Advantages

  • Great Expectations Enforced: No fallback — ensures quality
  • Unity Catalog Integration: Works with real enterprise data
  • Fluent API: Uses latest GX capabilities
  • Comprehensive Profiling: Deep analysis of every column
  • Secure by Design: GitHub Secrets protect credentials

Best Practices & Tips

Do →

  • Run validation on every data quality change may be schedule cron.
  • Review artifacts before merging.
  • Set up notifications for failed workflows.
  • Use workflow_dispatch for ad-hoc testing.
  • Monitor trends in quality scores to check variance.

Avoid →

  • Ignoring failed validations, see why it is failing and try to fine tune it.
  • Skipping artifact review, check generated artifacts for any Anamolies.

References

Documentation

Tools & Integration

--

--

Prashanth Kumar
Prashanth Kumar

Written by Prashanth Kumar

IT professional with 20+ years experience, feel free to contact me at: Prashanth.kumar.ms@outlook.com