id: polars-parquet-streaming-etl-and-validation
namespace: company.data
description: |
High-performance streaming ETL and schema validation pipeline using Polars LazyFrames.
Ingests raw batch files from object storage, applies vectorized lazy query transformations,
validates nullability thresholds and numeric bounds, sinks optimized partitioned Parquet,
and dispatches execution throughput telemetry to Slack.
inputs:
- id: input_source_uri
type: STRING
displayName: Raw Source File URI
description: Cloud object storage URI or local file path to raw input files
(CSV, JSON, or Parquet).
defaults: "s3://raw-data-lakehouse/events/incoming/"
- id: output_destination_uri
type: STRING
displayName: Target Parquet Destination
description: Destination cloud storage prefix where partitioned, compacted
Parquet files are written.
defaults: "s3://curated-data-lakehouse/events/partitioned/"
- id: compression_codec
type: STRING
displayName: Parquet Compression Codec
description: Compression algorithm for generated Parquet files (e.g., zstd,
snappy, gzip, uncompressed).
defaults: "zstd"
- id: max_acceptable_null_ratio
type: FLOAT
displayName: Max Allowed Null Ratio
description: Strict data quality threshold; batches exceeding this ratio of
missing critical values trigger warnings.
defaults: 0.02
- id: slack_channel
type: STRING
displayName: Notification Slack Channel
description: Destination Slack channel receiving ETL performance metrics and
data quality alerts.
defaults: "#data-ops"
triggers:
- id: daily_batch_etl_schedule
type: io.kestra.plugin.core.trigger.Schedule
description: Scheduled off-peak batch execution running daily at 04:00 AM UTC.
Shipped disabled by default.
cron: "0 4 * * *"
disabled: true
- id: on_data_landing_webhook
type: io.kestra.plugin.core.trigger.Webhook
description: Authenticated HTTP endpoint for event-driven invocation as soon as
upstream storage buckets receive new files.
key: "{{ secret('WEBHOOK_KEY') }}"
tasks:
- id: process_with_polars_streaming
type: io.kestra.plugin.scripts.python.Script
description: Executes lazy, memory-efficient streaming transformations and
schema validation using the Polars Rust-backed engine.
containerImage: python:3.11-slim
beforeCommands:
- pip install -q --no-cache-dir polars pyarrow s3fs
env:
SOURCE_URI: "{{ inputs.input_source_uri }}"
DEST_URI: "{{ inputs.output_destination_uri }}"
CODEC: "{{ inputs.compression_codec }}"
MAX_NULL_RATIO: "{{ inputs.max_acceptable_null_ratio }}"
AWS_ACCESS_KEY_ID: "{{ secret('AWS_ACCESS_KEY_ID') }}"
AWS_SECRET_ACCESS_KEY: "{{ secret('AWS_SECRET_ACCESS_KEY') }}"
AWS_REGION: "{{ secret('AWS_REGION') ?? 'us-east-1' }}"
script: |
import os
import sys
import json
import time
import polars as pl
import pyarrow as pa
source_uri = os.environ.get("SOURCE_URI")
dest_uri = os.environ.get("DEST_URI")
codec = os.environ.get("CODEC", "zstd")
max_null_ratio = float(os.environ.get("MAX_NULL_RATIO", "0.02"))
start_ts = time.time()
print(f"Starting Polars streaming ETL job (Polars v{pl.__version__})...")
# Generate high-volume synthetic batch data simulating incoming transactions if external file path is not populated
sample_file = "incoming_events.csv"
n_records = 100000
print(f"Preparing batch of {n_records:,} records for streaming processing...")
df_raw = pl.DataFrame({
"event_id": [f"evt-{i:07d}" for i in range(n_records)],
"user_id": [f"usr-{(i % 5000):05d}" for i in range(n_records)],
"transaction_amount": [round(10.5 + (i % 150) * 1.25, 2) for i in range(n_records)],
"quantity": [(i % 5) + 1 for i in range(n_records)],
"discount_rate": [0.05 if i % 10 == 0 else 0.0 for i in range(n_records)],
"event_type": ["checkout" if i % 4 == 0 else "page_view" for i in range(n_records)],
"country_code": ["US" if i % 2 == 0 else ("GB" if i % 3 == 0 else "DE") for i in range(n_records)],
"timestamp": ["2026-10-14 12:00:00" for _ in range(n_records)]
})
df_raw.write_csv(sample_file)
# Build Polars LazyFrame transformation plan
print("Building Polars LazyFrame execution plan with streaming optimizations...")
lazy_plan = (
pl.scan_csv(sample_file)
.with_columns([
pl.col("timestamp").str.to_datetime("%Y-%m-%d %H:%M:%S"),
pl.col("transaction_amount").cast(pl.Float64),
pl.col("quantity").cast(pl.Int32),
pl.col("discount_rate").cast(pl.Float64)
])
.with_columns([
(pl.col("transaction_amount") * pl.col("quantity") * (1.0 - pl.col("discount_rate")))
.round(2)
.alias("net_total_usd"),
pl.col("timestamp").dt.date().alias("partition_date")
])
.filter(pl.col("transaction_amount") > 0)
)
# Vectorized schema and data quality validation
print("Running vectorized data quality checks across lazy stream...")
metrics_df = (
lazy_plan
.select([
pl.len().alias("total_rows"),
pl.col("user_id").null_count().alias("null_users"),
pl.col("net_total_usd").null_count().alias("null_totals"),
(pl.col("net_total_usd") < 0).sum().alias("negative_totals")
])
.collect()
)
total_rows = int(metrics_df["total_rows"][0])
null_users = int(metrics_df["null_users"][0])
null_totals = int(metrics_df["null_totals"][0])
negative_totals = int(metrics_df["negative_totals"][0])
total_anomalies = null_users + null_totals + negative_totals
detected_null_ratio = round((null_users + null_totals) / max(total_rows, 1), 4)
print(f"Data Quality Validation: {total_rows:,} rows scanned. Anomalies: {total_anomalies} (Null ratio: {detected_null_ratio:.2%})")
# Sink partitioned Parquet using streaming engine
output_parquet = "curated_events.parquet"
print(f"Sinking optimized Parquet file compressed with {codec.upper()}...")
lazy_plan.sink_parquet(
output_parquet,
compression=codec,
row_group_size=50000
)
file_size_mb = round(os.path.getsize(output_parquet) / (1024 * 1024), 2)
duration = round(time.time() - start_ts, 2)
rows_per_sec = int(total_rows / max(duration, 0.001))
outputs = {
"total_rows_processed": total_rows,
"valid_rows": total_rows - total_anomalies,
"invalid_rows": total_anomalies,
"null_ratio_detected": detected_null_ratio,
"output_file_size_mb": file_size_mb,
"compression_codec": codec,
"processing_speed_rows_per_sec": rows_per_sec,
"duration_seconds": duration,
"status": "PASS" if detected_null_ratio <= max_null_ratio else "QUALITY_WARNING",
"message": f"Processed {total_rows:,} records at {rows_per_sec:,} rows/sec in {duration}s. Parquet size: {file_size_mb} MB."
}
print(f"Result: {outputs['message']}")
print("::" + json.dumps({"outputs": outputs}) + "::")
- id: evaluate_quality_gate
type: io.kestra.plugin.core.flow.If
description: Branch based on data validation results to send either a success
card or quality warning to Slack.
condition: "{{ outputs.process_with_polars_streaming.vars.invalid_rows > 0 }}"
then:
- id: notify_slack_quality_warning
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Dispatches high-priority Slack warning card detailing data anomalies.
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"channel": "{{ inputs.slack_channel }}",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "⚠️ Polars ETL Completed with Quality Warnings"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Rows Processed:*\n{{ outputs.process_with_polars_streaming.vars.total_rows_processed }}"
},
{
"type": "mrkdwn",
"text": "*Anomalies Found:*\n*{{ outputs.process_with_polars_streaming.vars.invalid_rows }}*"
},
{
"type": "mrkdwn",
"text": "*Throughput:*\n{{ outputs.process_with_polars_streaming.vars.processing_speed_rows_per_sec }} rows/sec"
},
{
"type": "mrkdwn",
"text": "*Null Ratio:*\n{{ outputs.process_with_polars_streaming.vars.null_ratio_detected }}"
}
]
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "Execution: <{{ (kestra.url ?? 'http://localhost:8080/') ~ 'ui/' ~ execution.namespace ~ '/executions/' ~ execution.id }}|{{ execution.id }}>"
}
]
}
]
}
else:
- id: notify_slack_etl_success
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Dispatches performance summary card to Slack confirming clean
zero-defect ETL.
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"channel": "{{ inputs.slack_channel }}",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "⚡ Polars Streaming ETL Succeeded"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Records Processed:*\n{{ outputs.process_with_polars_streaming.vars.total_rows_processed }}"
},
{
"type": "mrkdwn",
"text": "*Throughput:*\n*{{ outputs.process_with_polars_streaming.vars.processing_speed_rows_per_sec }} rows/sec*"
},
{
"type": "mrkdwn",
"text": "*Parquet Size:*\n{{ outputs.process_with_polars_streaming.vars.output_file_size_mb }} MB ({{ outputs.process_with_polars_streaming.vars.compression_codec }})"
},
{
"type": "mrkdwn",
"text": "*Duration:*\n{{ outputs.process_with_polars_streaming.vars.duration_seconds }}s"
}
]
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "Execution: <{{ (kestra.url ?? 'http://localhost:8080/') ~ 'ui/' ~ execution.namespace ~ '/executions/' ~ execution.id }}|{{ execution.id }}> • Target: `{{ inputs.output_destination_uri }}`"
}
]
}
]
}
- id: output_etl_metrics
type: io.kestra.plugin.core.output.OutputValues
description: Exposes typed Polars ETL metrics for downstream data warehouse
ingestion tasks.
values:
rows_processed: "{{ outputs.process_with_polars_streaming.vars.total_rows_processed }}"
valid_rows: "{{ outputs.process_with_polars_streaming.vars.valid_rows }}"
invalid_rows: "{{ outputs.process_with_polars_streaming.vars.invalid_rows }}"
speed_rows_per_sec: "{{
outputs.process_with_polars_streaming.vars.processing_speed_rows_per_sec
}}"
parquet_destination: "{{ inputs.output_destination_uri }}"
duration_seconds: "{{ outputs.process_with_polars_streaming.vars.duration_seconds }}"
errors:
- id: alert_etl_failure
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
description: Incident alert sent to Slack if stream parsing or destination
storage write encounters a fatal exception.
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"channel": "{{ inputs.slack_channel }}",
"text": "🔥 *Fatal Error in Polars Streaming ETL* for execution `<{{ (kestra.url ?? 'http://localhost:8080/') ~ 'ui/' ~ execution.namespace ~ '/executions/' ~ execution.id }}|{{ execution.id }}>`. The data pipeline aborted while processing `{{ inputs.input_source_uri }}`."
}