Skip to content

Commit 87c482a

Browse files
address ci/cd issues and Dennis Feedback
1 parent 4743399 commit 87c482a

17 files changed

Lines changed: 70 additions & 156 deletions

File tree

examples/data-quality-patterns/README.md

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ A comprehensive Dagster project demonstrating data quality validation across all
66

77
This project covers all 7 dimensions of data quality:
88

9-
| Dimension | Description | Implementation |
10-
|-----------|-------------|----------------|
11-
| **Accuracy** | Data correctly represents real-world entities | Python checks for name patterns, age ranges |
12-
| **Completeness** | All required data is present | Python + dbt `not_null` tests |
13-
| **Consistency** | Data is uniform across datasets | Python checks for region code formats |
14-
| **Timeliness** | Data is up-to-date and available | `FreshnessPolicy` on assets |
15-
| **Validity** | Data conforms to formats and rules | Python + Great Expectations checks |
16-
| **Uniqueness** | No unwanted duplicates | Python + dbt `unique` tests |
17-
| **Integrity** | Relationships between data maintained | Python + dbt `relationships` tests |
9+
| Dimension | Description | Implementation |
10+
| ---------------- | --------------------------------------------- | ------------------------------------------- |
11+
| **Accuracy** | Data correctly represents real-world entities | Python checks for name patterns, age ranges |
12+
| **Completeness** | All required data is present | Python + dbt `not_null` tests |
13+
| **Consistency** | Data is uniform across datasets | Python checks for region code formats |
14+
| **Timeliness** | Data is up-to-date and available | `FreshnessPolicy` on assets |
15+
| **Validity** | Data conforms to formats and rules | Python + Great Expectations checks |
16+
| **Uniqueness** | No unwanted duplicates | Python + dbt `unique` tests |
17+
| **Integrity** | Relationships between data maintained | Python + dbt `relationships` tests |
1818

1919
## Project Structure
2020

@@ -94,6 +94,7 @@ Open http://localhost:3000 to view the Dagster UI.
9494
### 4. Materialize Assets
9595

9696
In the Dagster UI:
97+
9798
1. Navigate to the **Assets** page
9899
2. Select `raw_customers`, `raw_orders`, and `raw_products`
99100
3. Click **Materialize** to generate data with quality issues
@@ -105,29 +106,29 @@ In the Dagster UI:
105106

106107
Located in `src/data_quality_patterns/defs/assets/python_checks.py`:
107108

108-
| Check | Dimension | Description |
109-
|-------|-----------|-------------|
110-
| `check_accuracy_names` | Accuracy | Detects placeholder/test names |
111-
| `check_accuracy_age` | Accuracy | Validates age ranges (0-120) |
112-
| `check_completeness_email` | Completeness | Requires 95% email coverage |
113-
| `check_completeness_amount` | Completeness | Requires 98% amount coverage |
114-
| `check_consistency_region` | Consistency | Validates region codes (US, EU, APAC, LATAM) |
115-
| `check_validity_email` | Validity | Validates email format |
116-
| `check_validity_amount` | Validity | Validates positive amounts |
117-
| `check_uniqueness_customer_id` | Uniqueness | Detects duplicate customers |
118-
| `check_uniqueness_order_id` | Uniqueness | Detects duplicate orders |
119-
| `check_integrity_customer_ref` | Integrity | Validates customer foreign keys |
109+
| Check | Dimension | Description |
110+
| ------------------------------ | ------------ | -------------------------------------------- |
111+
| `check_accuracy_names` | Accuracy | Detects placeholder/test names |
112+
| `check_accuracy_age` | Accuracy | Validates age ranges (0-120) |
113+
| `check_completeness_email` | Completeness | Requires 95% email coverage |
114+
| `check_completeness_amount` | Completeness | Requires 98% amount coverage |
115+
| `check_consistency_region` | Consistency | Validates region codes (US, EU, APAC, LATAM) |
116+
| `check_validity_email` | Validity | Validates email format |
117+
| `check_validity_amount` | Validity | Validates positive amounts |
118+
| `check_uniqueness_customer_id` | Uniqueness | Detects duplicate customers |
119+
| `check_uniqueness_order_id` | Uniqueness | Detects duplicate orders |
120+
| `check_integrity_customer_ref` | Integrity | Validates customer foreign keys |
120121

121122
### Great Expectations Checks
122123

123124
Located in `src/data_quality_patterns/defs/assets/ge_checks.py`:
124125

125-
| Check | Dimension | Description |
126-
|-------|-----------|-------------|
127-
| `ge_check_sku_unique` | Uniqueness | Product SKU uniqueness |
128-
| `ge_check_name_not_null` | Completeness | Product name presence |
129-
| `ge_check_price_positive` | Validity | Price range validation |
130-
| `ge_check_category_valid` | Consistency | Category value validation |
126+
| Check | Dimension | Description |
127+
| ------------------------- | ------------ | ------------------------- |
128+
| `ge_check_sku_unique` | Uniqueness | Product SKU uniqueness |
129+
| `ge_check_name_not_null` | Completeness | Product name presence |
130+
| `ge_check_price_positive` | Validity | Price range validation |
131+
| `ge_check_category_valid` | Consistency | Category value validation |
131132

132133
### dbt Tests as Asset Checks
133134

@@ -141,6 +142,7 @@ Defined in `dbt_project/models/*/schema.yml`:
141142
### Freshness Policies
142143

143144
Raw data assets have `FreshnessPolicy` attached for timeliness monitoring:
145+
144146
- Fail window: 24 hours
145147
- Warn window: 12 hours
146148

@@ -156,6 +158,7 @@ df = generate_customers(n=100, failure_rate=0.15, seed=42)
156158
```
157159

158160
Quality issues introduced:
161+
159162
- **Uniqueness**: Duplicate customer IDs
160163
- **Completeness**: Missing email addresses
161164
- **Validity**: Invalid email formats
@@ -175,6 +178,7 @@ pytest tests/test_asset_checks.py -v
175178
```
176179

177180
Tests verify that:
181+
178182
- Quality checks correctly identify issues in random data
179183
- Clean data passes all checks
180184
- Validators work as expected
@@ -202,6 +206,7 @@ ruff check . --fix --isolated # Auto-fix issues
202206
### Project Configuration
203207

204208
The project uses:
209+
205210
- `pyproject.toml` for dependency management (uv/hatch)
206211
- `tool.dagster` for Dagster configuration
207212
- `tool.dg` for dg CLI configuration

examples/data-quality-patterns/src/data_quality_patterns/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,3 @@
44
covering native asset checks, Great Expectations, dbt tests, and freshness checks
55
across all 7 data quality dimensions.
66
"""
7-
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
"""Dagster definitions for data quality patterns."""
2-
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
"""Asset definitions for data quality patterns."""
2-

examples/data-quality-patterns/src/data_quality_patterns/defs/assets/ge_checks.py

Lines changed: 5 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111

1212
import dagster as dg
1313
import pandas as pd
14+
from great_expectations.dataset import PandasDataset
1415

1516
from data_quality_patterns.defs.assets.raw_data import raw_products
16-
from data_quality_patterns.lib.expectations import GE_AVAILABLE, create_ge_dataset
1717

1818

1919
@dg.asset_check(asset=raw_products, name="ge_check_sku_unique")
@@ -22,21 +22,7 @@ def ge_check_sku_unique(
2222
raw_products: pd.DataFrame,
2323
) -> dg.AssetCheckResult:
2424
"""Great Expectations check: SKU values are unique."""
25-
if not GE_AVAILABLE:
26-
return dg.AssetCheckResult(
27-
passed=True,
28-
severity=dg.AssetCheckSeverity.WARN,
29-
description="Great Expectations not installed, skipping check",
30-
)
31-
32-
ge_df = create_ge_dataset(raw_products)
33-
if ge_df is None:
34-
return dg.AssetCheckResult(
35-
passed=False,
36-
severity=dg.AssetCheckSeverity.WARN,
37-
description="Failed to create Great Expectations dataset",
38-
)
39-
25+
ge_df = PandasDataset(raw_products)
4026
result = ge_df.expect_column_values_to_be_unique("sku")
4127

4228
if result["success"]:
@@ -62,21 +48,7 @@ def ge_check_name_not_null(
6248
raw_products: pd.DataFrame,
6349
) -> dg.AssetCheckResult:
6450
"""Great Expectations check: Product names are not null (completeness)."""
65-
if not GE_AVAILABLE:
66-
return dg.AssetCheckResult(
67-
passed=True,
68-
severity=dg.AssetCheckSeverity.WARN,
69-
description="Great Expectations not installed, skipping check",
70-
)
71-
72-
ge_df = create_ge_dataset(raw_products)
73-
if ge_df is None:
74-
return dg.AssetCheckResult(
75-
passed=False,
76-
severity=dg.AssetCheckSeverity.WARN,
77-
description="Failed to create Great Expectations dataset",
78-
)
79-
51+
ge_df = PandasDataset(raw_products)
8052
result = ge_df.expect_column_values_to_not_be_null("name")
8153

8254
if result["success"]:
@@ -101,21 +73,7 @@ def ge_check_price_positive(
10173
raw_products: pd.DataFrame,
10274
) -> dg.AssetCheckResult:
10375
"""Great Expectations check: Prices are positive (validity/accuracy)."""
104-
if not GE_AVAILABLE:
105-
return dg.AssetCheckResult(
106-
passed=True,
107-
severity=dg.AssetCheckSeverity.WARN,
108-
description="Great Expectations not installed, skipping check",
109-
)
110-
111-
ge_df = create_ge_dataset(raw_products)
112-
if ge_df is None:
113-
return dg.AssetCheckResult(
114-
passed=False,
115-
severity=dg.AssetCheckSeverity.WARN,
116-
description="Failed to create Great Expectations dataset",
117-
)
118-
76+
ge_df = PandasDataset(raw_products)
11977
result = ge_df.expect_column_values_to_be_between(
12078
"price", min_value=0.01, max_value=10000, mostly=0.9
12179
)
@@ -142,21 +100,7 @@ def ge_check_category_valid(
142100
raw_products: pd.DataFrame,
143101
) -> dg.AssetCheckResult:
144102
"""Great Expectations check: Categories are from valid set (consistency)."""
145-
if not GE_AVAILABLE:
146-
return dg.AssetCheckResult(
147-
passed=True,
148-
severity=dg.AssetCheckSeverity.WARN,
149-
description="Great Expectations not installed, skipping check",
150-
)
151-
152-
ge_df = create_ge_dataset(raw_products)
153-
if ge_df is None:
154-
return dg.AssetCheckResult(
155-
passed=False,
156-
severity=dg.AssetCheckSeverity.WARN,
157-
description="Failed to create Great Expectations dataset",
158-
)
159-
103+
ge_df = PandasDataset(raw_products)
160104
valid_categories = ["Electronics", "Clothing", "Home", "Sports", "Books"]
161105
result = ge_df.expect_column_values_to_be_in_set("category", valid_categories)
162106

examples/data-quality-patterns/src/data_quality_patterns/defs/assets/python_checks.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ def check_accuracy_age(
8989
return dg.AssetCheckResult(
9090
passed=False,
9191
severity=dg.AssetCheckSeverity.ERROR,
92-
description=f"Found {len(invalid_records)} records with invalid ages",
92+
description=warnings[0]
93+
if warnings
94+
else f"Found {len(invalid_records)} records with invalid ages",
9395
metadata={
9496
"invalid_count": len(invalid_records),
9597
"invalid_ages": invalid_records["age"].tolist(),
@@ -220,13 +222,15 @@ def check_validity_email(
220222
validity_rate = calculate_validity(raw_customers, "email")
221223
threshold = 0.90
222224

223-
invalid_records, _ = validate_email_format(raw_customers, "email")
225+
invalid_records, errors = validate_email_format(raw_customers, "email")
224226

225227
if validity_rate < threshold:
226228
return dg.AssetCheckResult(
227229
passed=False,
228230
severity=dg.AssetCheckSeverity.ERROR,
229-
description=f"Email validity {validity_rate:.1%} below threshold {threshold:.0%}",
231+
description=errors[0]
232+
if errors
233+
else f"Email validity {validity_rate:.1%} below threshold {threshold:.0%}",
230234
metadata={
231235
"validity_rate": float(validity_rate),
232236
"invalid_count": len(invalid_records),
@@ -281,14 +285,16 @@ def check_uniqueness_customer_id(
281285
) -> dg.AssetCheckResult:
282286
"""Check that customer IDs are unique (uniqueness)."""
283287
uniqueness_rate = calculate_uniqueness(raw_customers, "customer_id")
284-
duplicate_records, _ = check_uniqueness(raw_customers, ["customer_id"])
288+
duplicate_records, errors = check_uniqueness(raw_customers, ["customer_id"])
285289

286290
if not duplicate_records.empty:
287291
duplicate_ids = duplicate_records["customer_id"].unique().tolist()
288292
return dg.AssetCheckResult(
289293
passed=False,
290294
severity=dg.AssetCheckSeverity.ERROR,
291-
description=f"Found {len(duplicate_ids)} duplicate customer IDs",
295+
description=errors[0]
296+
if errors
297+
else f"Found {len(duplicate_ids)} duplicate customer IDs",
292298
metadata={
293299
"uniqueness_rate": float(uniqueness_rate),
294300
"duplicate_ids": duplicate_ids[:10], # First 10
@@ -310,13 +316,13 @@ def check_uniqueness_order_id(
310316
) -> dg.AssetCheckResult:
311317
"""Check that order IDs are unique (uniqueness)."""
312318
uniqueness_rate = calculate_uniqueness(raw_orders, "order_id")
313-
duplicate_records, _ = check_uniqueness(raw_orders, ["order_id"])
319+
duplicate_records, errors = check_uniqueness(raw_orders, ["order_id"])
314320

315321
if not duplicate_records.empty:
316322
return dg.AssetCheckResult(
317323
passed=False,
318324
severity=dg.AssetCheckSeverity.ERROR,
319-
description="Found duplicate order IDs",
325+
description=errors[0] if errors else "Found duplicate order IDs",
320326
metadata={
321327
"uniqueness_rate": float(uniqueness_rate),
322328
"duplicate_count": len(duplicate_records),
@@ -335,7 +341,11 @@ def check_uniqueness_order_id(
335341
# ============================================================================
336342

337343

338-
@dg.asset_check(asset=raw_orders, name="check_integrity_customer_ref", additional_ins={"raw_customers": dg.AssetIn("raw_customers")})
344+
@dg.asset_check(
345+
asset=raw_orders,
346+
name="check_integrity_customer_ref",
347+
additional_ins={"raw_customers": dg.AssetIn("raw_customers")},
348+
)
339349
def check_integrity_customer_ref(
340350
context: dg.AssetCheckExecutionContext,
341351
raw_orders: pd.DataFrame,

examples/data-quality-patterns/src/data_quality_patterns/defs/assets/raw_data.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,7 @@ def raw_customers(
4949
# Store in DuckDB for dbt to access
5050
with duckdb.get_connection() as conn:
5151
conn.execute("CREATE SCHEMA IF NOT EXISTS raw")
52-
conn.execute("DROP TABLE IF EXISTS raw.customers")
53-
conn.execute("CREATE TABLE raw.customers AS SELECT * FROM df")
52+
conn.execute("CREATE OR REPLACE TABLE raw.customers AS SELECT * FROM df")
5453

5554
context.add_output_metadata(
5655
{
@@ -67,7 +66,6 @@ def raw_customers(
6766
@dg.asset(
6867
group_name="raw_data",
6968
compute_kind="python",
70-
deps=[raw_customers],
7169
freshness_policy=daily_freshness_policy,
7270
)
7371
def raw_orders(
@@ -102,8 +100,7 @@ def raw_orders(
102100
# Store in DuckDB for dbt to access
103101
with duckdb.get_connection() as conn:
104102
conn.execute("CREATE SCHEMA IF NOT EXISTS raw")
105-
conn.execute("DROP TABLE IF EXISTS raw.orders")
106-
conn.execute("CREATE TABLE raw.orders AS SELECT * FROM df")
103+
conn.execute("CREATE OR REPLACE TABLE raw.orders AS SELECT * FROM df")
107104

108105
context.add_output_metadata(
109106
{
@@ -137,8 +134,7 @@ def raw_products(
137134
# Store in DuckDB
138135
with duckdb.get_connection() as conn:
139136
conn.execute("CREATE SCHEMA IF NOT EXISTS raw")
140-
conn.execute("DROP TABLE IF EXISTS raw.products")
141-
conn.execute("CREATE TABLE raw.products AS SELECT * FROM df")
137+
conn.execute("CREATE OR REPLACE TABLE raw.products AS SELECT * FROM df")
142138

143139
context.add_output_metadata(
144140
{

examples/data-quality-patterns/src/data_quality_patterns/defs/resources/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,3 @@
1111
"generate_orders",
1212
"generate_products",
1313
]
14-

examples/data-quality-patterns/src/data_quality_patterns/defs/resources/data_generator.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def generate_customers(
113113

114114
# Age - may have accuracy issues
115115
if has_accuracy_issue:
116-
age = random.choice([-5, 0, 150, 200, 999]) # Invalid ages
116+
age = random.choice([-5, -1, 150, 200, 999]) # Invalid ages (outside 0-120 range)
117117
else:
118118
age = random.randint(18, 80)
119119

@@ -264,4 +264,3 @@ def generate_products(
264264
data.append(record)
265265

266266
return pd.DataFrame(data)
267-
Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,16 @@
11
"""Great Expectations integration utilities."""
22

3-
from typing import Any, Optional
4-
53
import pandas as pd
4+
from great_expectations.dataset import PandasDataset
65

7-
try:
8-
from great_expectations.dataset import PandasDataset
9-
10-
GE_AVAILABLE = True
11-
except ImportError:
12-
GE_AVAILABLE = False
13-
PandasDataset = None
146

15-
16-
def create_ge_dataset(df: pd.DataFrame) -> Optional[Any]:
7+
def create_ge_dataset(df: pd.DataFrame) -> PandasDataset:
178
"""Create a Great Expectations dataset from a pandas DataFrame.
189
1910
Args:
2011
df: pandas DataFrame to convert
2112
2213
Returns:
23-
Great Expectations PandasDataset, or None if GE is not available
14+
Great Expectations PandasDataset
2415
"""
25-
if not GE_AVAILABLE or PandasDataset is None:
26-
return None
27-
2816
return PandasDataset(df)

0 commit comments

Comments
 (0)