
Ingesting Data
- 49 installs
- 426 repo stars
- Updated December 11, 2025
- ancoleman/ai-design-components
ingesting-data is a Claude Code skill that provides data ingestion patterns for loading data from cloud storage, files, APIs, and streaming sources into databases.
About
This skill provides patterns for loading data into systems from external sources such as cloud storage, files, APIs, and streaming feeds. It covers batch ingestion, streaming ingestion, API polling, and change data capture, with example code in Python, TypeScript, Rust, and Go. Developers use it when importing CSV, JSON, or Parquet files, pulling from S3 or GCS, consuming API feeds, or building ETL and ELT pipelines.
- Data ingestion patterns for loading data into databases
- Covers cloud storage (S3/GCS/Azure), files (CSV/JSON/Parquet), APIs, and streaming
- Includes batch, streaming, API-polling, and CDC patterns with Python, TypeScript, Rust, and Go examples
Ingesting Data by the numbers
- 49 all-time installs (skills.sh)
- Ranked #936 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
ingesting-data capabilities & compatibility
- Capabilities
- batch ingestion · streaming ingestion · api polling · change data capture
- Works with
- aws · gcp · azure · kafka · postgres · snowflake
- Use cases
- data analysis · database · devops
- Pricing
- Free
What ingesting-data says it does
Data ingestion patterns for loading data from cloud storage, APIs, files, and streaming sources into databases.
Polars for file processing (faster than pandas)
npx skills add https://github.com/ancoleman/ai-design-components --skill ingesting-dataAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 49 |
|---|---|
| repo stars | ★ 426 |
| Last updated | December 11, 2025 |
| Repository | ancoleman/ai-design-components ↗ |
What it does
Building ETL/ELT pipelines to load data from files, S3/GCS, APIs, or streams into databases.
Who is it for?
Building ETL/ELT pipelines from files, cloud storage, APIs, or streams into databases.
Skip if: Apps with no external data sources or bulk-loading needs.
When should I use this skill?
You are importing CSV/JSON/Parquet files, pulling from S3/GCS buckets, consuming API feeds, or building ETL pipelines.
What you get
Reliable ingestion pipelines that load and validate data from files, storage, APIs, or streams.
- Batch ingestion pipeline
- Streaming ingestion pipeline
- API polling pipeline
By the numbers
- 4 ingestion patterns (batch, streaming, API polling, CDC)
- chunked reading recommended for files over 100MB
Files
Data Ingestion Patterns
This skill provides patterns for getting data INTO systems from external sources.
When to Use This Skill
- Importing CSV, JSON, Parquet, or Excel files
- Loading data from S3, GCS, or Azure Blob storage
- Consuming REST/GraphQL API feeds
- Building ETL/ELT pipelines
- Database migration and CDC (Change Data Capture)
- Streaming data ingestion from Kafka/Kinesis
Ingestion Pattern Decision Tree
What is your data source?
├── Cloud Storage (S3, GCS, Azure) → See cloud-storage.md
├── Files (CSV, JSON, Parquet) → See file-formats.md
├── REST/GraphQL APIs → See api-feeds.md
├── Streaming (Kafka, Kinesis) → See streaming-sources.md
├── Legacy Database → See database-migration.md
└── Need full ETL framework → See etl-tools.mdQuick Start by Language
Python (Recommended for ETL)
dlt (data load tool) - Modern Python ETL:
import dlt
# Define a source
@dlt.source
def github_source(repo: str):
@dlt.resource(write_disposition="merge", primary_key="id")
def issues():
response = requests.get(f"https://api.github.com/repos/{repo}/issues")
yield response.json()
return issues
# Load to destination
pipeline = dlt.pipeline(
pipeline_name="github_issues",
destination="postgres", # or duckdb, bigquery, snowflake
dataset_name="github_data"
)
load_info = pipeline.run(github_source("owner/repo"))
print(load_info)Polars for file processing (faster than pandas):
import polars as pl
# Read CSV with schema inference
df = pl.read_csv("data.csv")
# Read Parquet (columnar, efficient)
df = pl.read_parquet("s3://bucket/data.parquet")
# Read JSON lines
df = pl.read_ndjson("events.jsonl")
# Write to database
df.write_database(
table_name="events",
connection="postgresql://user:pass@localhost/db",
if_table_exists="append"
)TypeScript/Node.js
S3 ingestion:
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { parse } from "csv-parse/sync";
const s3 = new S3Client({ region: "us-east-1" });
async function ingestFromS3(bucket: string, key: string) {
const response = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
const body = await response.Body?.transformToString();
// Parse CSV
const records = parse(body, { columns: true, skip_empty_lines: true });
// Insert to database
await db.insert(eventsTable).values(records);
}API feed polling:
import { Hono } from "hono";
// Webhook receiver for real-time ingestion
const app = new Hono();
app.post("/webhooks/stripe", async (c) => {
const event = await c.req.json();
// Validate webhook signature
const signature = c.req.header("stripe-signature");
// ... validation logic
// Ingest event
await db.insert(stripeEventsTable).values({
eventId: event.id,
type: event.type,
data: event.data,
receivedAt: new Date()
});
return c.json({ received: true });
});Rust
High-performance file ingestion:
use polars::prelude::*;
use aws_sdk_s3::Client;
async fn ingest_parquet(client: &Client, bucket: &str, key: &str) -> Result<DataFrame> {
// Download from S3
let resp = client.get_object()
.bucket(bucket)
.key(key)
.send()
.await?;
let bytes = resp.body.collect().await?.into_bytes();
// Parse with Polars
let df = ParquetReader::new(Cursor::new(bytes))
.finish()?;
Ok(df)
}Go
Concurrent file processing:
package main
import (
"context"
"encoding/csv"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
func ingestCSV(ctx context.Context, client *s3.Client, bucket, key string) error {
resp, err := client.GetObject(ctx, &s3.GetObjectInput{
Bucket: &bucket,
Key: &key,
})
if err != nil {
return err
}
defer resp.Body.Close()
reader := csv.NewReader(resp.Body)
records, err := reader.ReadAll()
if err != nil {
return err
}
// Batch insert to database
return batchInsert(ctx, records)
}Ingestion Patterns
1. Batch Ingestion (Files/Storage)
For periodic bulk loads:
Source → Extract → Transform → Load → Validate
↓ ↓ ↓ ↓ ↓
S3 Download Clean/Map Insert Count checkKey considerations:
- Use chunked reading for large files (>100MB)
- Implement idempotency with checksums
- Track file processing state
- Handle partial failures
2. Streaming Ingestion (Real-time)
For continuous data flow:
Source → Buffer → Process → Load → Ack
↓ ↓ ↓ ↓ ↓
Kafka In-memory Transform DB Commit offsetKey considerations:
- At-least-once vs exactly-once semantics
- Backpressure handling
- Dead letter queues for failures
- Checkpoint management
3. API Polling (Feeds)
For external API data:
Schedule → Fetch → Dedupe → Load → Update cursor
↓ ↓ ↓ ↓ ↓
Cron API call By ID Insert Last timestampKey considerations:
- Rate limiting and backoff
- Incremental loading (cursors, timestamps)
- API pagination handling
- Retry with exponential backoff
4. Change Data Capture (CDC)
For database replication:
Source DB → Capture changes → Transform → Target DB
↓ ↓ ↓ ↓
Postgres Debezium/WAL Map schema Insert/UpdateKey considerations:
- Initial snapshot + streaming changes
- Schema evolution handling
- Ordering guarantees
- Conflict resolution
Library Recommendations
| Use Case | Python | TypeScript | Rust | Go |
|---|---|---|---|---|
| ETL Framework | dlt, Meltano, Dagster | - | - | - |
| Cloud Storage | boto3, gcsfs, adlfs | @aws-sdk/, @google-cloud/ | aws-sdk-s3, object_store | aws-sdk-go-v2 |
| File Processing | polars, pandas, pyarrow | papaparse, xlsx, parquetjs | polars-rs, arrow-rs | encoding/csv, parquet-go |
| Streaming | confluent-kafka, aiokafka | kafkajs | rdkafka-rs | franz-go, sarama |
| CDC | Debezium, pg_logical | - | - | - |
Reference Documentation
references/cloud-storage.md- S3, GCS, Azure Blob patternsreferences/file-formats.md- CSV, JSON, Parquet, Excel handlingreferences/api-feeds.md- REST polling, webhooks, GraphQL subscriptionsreferences/streaming-sources.md- Kafka, Kinesis, Pub/Subreferences/database-migration.md- Schema migration, CDC patternsreferences/etl-tools.md- dlt, Meltano, Airbyte, Fivetran
Scripts
scripts/validate_csv_schema.py- Validate CSV against expected schemascripts/test_s3_connection.py- Test S3 bucket connectivityscripts/generate_dlt_pipeline.py- Generate dlt pipeline scaffold
Chaining with Database Skills
After ingestion, chain to appropriate database skill:
| Destination | Chain to Skill |
|---|---|
| PostgreSQL, MySQL | databases-relational |
| MongoDB, DynamoDB | databases-document |
| Qdrant, Pinecone | databases-vector (after embedding) |
| ClickHouse, TimescaleDB | databases-timeseries |
| Neo4j | databases-graph |
For vector databases, chain through ai-data-engineering for embedding:
ingesting-data → ai-data-engineering → databases-vectorskill: "ingesting-data"
version: "1.0"
domain: "backend"
base_outputs:
# Core ETL pipeline structure
- path: "src/ingestion/"
must_contain:
- "Pipeline configuration or orchestration"
- "Data source connectors"
- "Error handling and logging"
# Configuration files
- path: "config/ingestion.yaml"
must_contain:
- "Source configurations"
- "Destination settings"
- "Scheduling or trigger rules"
# Testing infrastructure
- path: "tests/ingestion/"
must_contain:
- "Schema validation tests"
- "Connection tests"
- "Data quality checks"
conditional_outputs:
maturity:
starter:
# Simple file-based ingestion
- path: "src/ingestion/loaders/"
must_contain:
- "CSV/JSON file loader"
- "Basic validation"
- "Single-threaded processing"
- path: "src/ingestion/schema.py"
must_contain:
- "Expected data schema definition"
- "Type validation"
- path: "requirements.txt"
must_contain:
- "polars or pandas"
- "Database client library"
intermediate:
# Cloud storage + API ingestion
- path: "src/ingestion/connectors/"
must_contain:
- "S3/GCS storage connector"
- "API client with pagination"
- "Retry logic and backoff"
- path: "src/ingestion/pipelines/"
must_contain:
- "ETL pipeline orchestration"
- "Incremental loading logic"
- "State management (cursors/checkpoints)"
- path: "src/ingestion/transforms/"
must_contain:
- "Data cleaning functions"
- "Schema mapping"
- "Deduplication logic"
- path: "docker-compose.yml"
must_contain:
- "Local development setup"
- "Test data sources"
advanced:
# Production-grade streaming + CDC
- path: "src/ingestion/streaming/"
must_contain:
- "Kafka/Kinesis consumer"
- "Backpressure handling"
- "Dead letter queue"
- "Exactly-once semantics"
- path: "src/ingestion/cdc/"
must_contain:
- "Change Data Capture setup"
- "Schema evolution handling"
- "Conflict resolution"
- path: "src/ingestion/monitoring/"
must_contain:
- "Ingestion metrics (records/sec, lag)"
- "Data quality metrics"
- "Alerting rules"
- path: "infrastructure/orchestration/"
must_contain:
- "Airflow/Dagster DAGs"
- "Dependency management"
- "Failure recovery"
- path: "infrastructure/terraform/"
must_contain:
- "Cloud storage buckets"
- "IAM roles and policies"
- "Streaming infrastructure (Kafka/Kinesis)"
database:
postgres:
- path: "src/ingestion/destinations/postgres.py"
must_contain:
- "PostgreSQL connection pool"
- "COPY or batch insert"
- "Transaction management"
mysql:
- path: "src/ingestion/destinations/mysql.py"
must_contain:
- "MySQL connection pool"
- "Batch insert with ON DUPLICATE KEY"
mongodb:
- path: "src/ingestion/destinations/mongodb.py"
must_contain:
- "MongoDB bulk operations"
- "Upsert logic"
clickhouse:
- path: "src/ingestion/destinations/clickhouse.py"
must_contain:
- "ClickHouse async inserts"
- "Optimized batch size"
bigquery:
- path: "src/ingestion/destinations/bigquery.py"
must_contain:
- "BigQuery streaming insert or load job"
- "Schema auto-detect or explicit schema"
snowflake:
- path: "src/ingestion/destinations/snowflake.py"
must_contain:
- "Snowflake COPY INTO from stage"
- "Stage file management"
scaffolding:
- template: "python-etl-pipeline"
generates:
- "src/ingestion/pipeline.py"
- "src/ingestion/connectors/s3.py"
- "src/ingestion/connectors/api.py"
- "config/sources.yaml"
- "requirements.txt"
conditions:
- language: "python"
- template: "dlt-pipeline"
generates:
- "pipelines/github_pipeline.py"
- ".dlt/config.toml"
- ".dlt/secrets.toml"
- "requirements.txt"
conditions:
- language: "python"
- framework: "dlt"
- template: "typescript-ingestion"
generates:
- "src/ingestion/s3-loader.ts"
- "src/ingestion/webhook-receiver.ts"
- "src/types/events.ts"
- "package.json"
conditions:
- language: "typescript"
- template: "airflow-dag"
generates:
- "dags/data_ingestion_dag.py"
- "dags/config/connections.yaml"
- "docker-compose.yml"
conditions:
- orchestrator: "airflow"
- template: "streaming-consumer"
generates:
- "src/consumers/kafka_consumer.py"
- "src/processors/event_processor.py"
- "config/kafka.yaml"
- "docker-compose.yml"
conditions:
- source_type: "streaming"
- language: "python"
metadata:
primary_blueprints:
- "data-pipeline"
contributes_to:
- "Data ingestion layer"
- "ETL/ELT pipelines"
- "Batch and streaming data loading"
- "API and file-based data collection"
- "Database migration and CDC"
common_patterns:
- "Batch file ingestion from S3/GCS"
- "REST API polling with cursor-based pagination"
- "Webhook receivers for real-time events"
- "Kafka/Kinesis streaming consumers"
- "Change Data Capture from source databases"
- "CSV/JSON/Parquet parsing and validation"
chaining:
before:
- "api-first (for webhook endpoints)"
- "authenticating-users (for API authentication)"
after:
- "databases-relational (for SQL destinations)"
- "databases-document (for NoSQL destinations)"
- "databases-vector (via ai-data-engineering for embeddings)"
- "databases-timeseries (for time-series data)"
- "transforming-data (for post-ingestion transformations)"
key_libraries:
python:
- "dlt (data load tool)"
- "polars (fast dataframes)"
- "boto3 (AWS S3)"
- "google-cloud-storage (GCS)"
- "confluent-kafka (Kafka)"
- "debezium (CDC)"
typescript:
- "@aws-sdk/client-s3"
- "papaparse (CSV)"
- "kafkajs"
- "hono (webhooks)"
rust:
- "polars-rs"
- "aws-sdk-s3"
- "rdkafka"
go:
- "aws-sdk-go-v2"
- "encoding/csv"
- "franz-go (Kafka)"
validation_requirements:
- "Schema validation before insert"
- "Idempotency checks (checksums, deduplication)"
- "Data quality metrics tracking"
- "Connection and permission testing"
- "Error handling and retry logic"
performance_considerations:
- "Chunked reading for large files (>100MB)"
- "Parallel processing where possible"
- "Batch inserts (1000-10000 records)"
- "Connection pooling for databases"
- "Backpressure handling for streaming"
security_checklist:
- "IAM roles for cloud storage access"
- "API key rotation and secrets management"
- "Network policies for database access"
- "Data encryption in transit and at rest"
- "Audit logging of ingestion operations"
API Feed Ingestion
Table of Contents
- REST API Polling
- Python with httpx (async)
- TypeScript with fetch
- Webhook Receivers
- Python (FastAPI)
- TypeScript (Hono)
- GraphQL Subscriptions
- Python (gql)
- Rate Limiting & Backoff
- Best Practices
REST API Polling
Python with httpx (async)
import httpx
import asyncio
from datetime import datetime, timedelta
async def poll_api(
url: str,
interval_seconds: int = 60,
cursor_field: str = "updated_at"
):
"""Poll API with incremental loading."""
async with httpx.AsyncClient() as client:
cursor = load_cursor() # Load from DB/file
while True:
params = {cursor_field + "_gt": cursor} if cursor else {}
response = await client.get(url, params=params)
response.raise_for_status()
data = response.json()
if data["items"]:
await process_items(data["items"])
cursor = max(item[cursor_field] for item in data["items"])
save_cursor(cursor)
await asyncio.sleep(interval_seconds)
# With pagination
async def fetch_all_pages(url: str, page_size: int = 100):
"""Fetch all pages from paginated API."""
async with httpx.AsyncClient() as client:
page = 1
while True:
response = await client.get(url, params={
"page": page,
"per_page": page_size
})
data = response.json()
yield data["items"]
if len(data["items"]) < page_size:
break
page += 1TypeScript with fetch
async function* fetchWithPagination(baseUrl: string, pageSize = 100) {
let cursor: string | undefined;
while (true) {
const url = new URL(baseUrl);
url.searchParams.set("limit", String(pageSize));
if (cursor) url.searchParams.set("cursor", cursor);
const response = await fetch(url);
const data = await response.json();
yield data.items;
cursor = data.next_cursor;
if (!cursor) break;
}
}
// Usage
for await (const batch of fetchWithPagination("https://api.example.com/items")) {
await processBatch(batch);
}Webhook Receivers
Python (FastAPI)
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
app = FastAPI()
@app.post("/webhooks/stripe")
async def stripe_webhook(request: Request):
payload = await request.body()
sig_header = request.headers.get("stripe-signature")
# Verify signature
if not verify_stripe_signature(payload, sig_header):
raise HTTPException(status_code=400, detail="Invalid signature")
event = await request.json()
# Idempotency check
if await is_processed(event["id"]):
return {"status": "already_processed"}
# Process and store
await store_event(event)
await mark_processed(event["id"])
return {"status": "ok"}
def verify_stripe_signature(payload: bytes, sig_header: str) -> bool:
secret = os.environ["STRIPE_WEBHOOK_SECRET"]
timestamp, signature = parse_stripe_header(sig_header)
signed_payload = f"{timestamp}.{payload.decode()}"
expected = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)TypeScript (Hono)
import { Hono } from "hono";
import { createHmac, timingSafeEqual } from "crypto";
const app = new Hono();
app.post("/webhooks/github", async (c) => {
const payload = await c.req.text();
const signature = c.req.header("x-hub-signature-256");
// Verify
const expected = `sha256=${createHmac("sha256", process.env.GITHUB_SECRET!)
.update(payload)
.digest("hex")}`;
if (!timingSafeEqual(Buffer.from(signature!), Buffer.from(expected))) {
return c.json({ error: "Invalid signature" }, 401);
}
const event = JSON.parse(payload);
await db.insert(githubEvents).values({
eventId: c.req.header("x-github-delivery"),
eventType: c.req.header("x-github-event"),
payload: event,
receivedAt: new Date()
});
return c.json({ received: true });
});GraphQL Subscriptions
Python (gql)
from gql import Client, gql
from gql.transport.websockets import WebsocketsTransport
transport = WebsocketsTransport(
url="wss://api.example.com/graphql",
headers={"Authorization": f"Bearer {token}"}
)
async with Client(transport=transport) as session:
subscription = gql("""
subscription {
orderCreated {
id
customer { name }
items { product quantity }
total
}
}
""")
async for result in session.subscribe(subscription):
await process_order(result["orderCreated"])Rate Limiting & Backoff
import asyncio
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=1, min=1, max=60)
)
async def fetch_with_retry(url: str):
async with httpx.AsyncClient() as client:
response = await client.get(url)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 60))
await asyncio.sleep(retry_after)
raise Exception("Rate limited")
response.raise_for_status()
return response.json()Best Practices
1. Always use idempotency keys - Prevent duplicate processing 2. Verify webhook signatures - Security is critical 3. Implement cursor-based pagination - More reliable than offset 4. Store raw payloads - Debug and replay capability 5. Use exponential backoff - Be a good API citizen
Cloud Storage Ingestion Patterns
Table of Contents
- S3 (AWS)
- Python with boto3
- TypeScript with AWS SDK v3
- GCS (Google Cloud)
- Python with gcsfs
- Azure Blob Storage
- Python with adlfs
- Best Practices
S3 (AWS)
Python with boto3
import boto3
import polars as pl
from io import BytesIO
s3 = boto3.client('s3')
def list_and_ingest(bucket: str, prefix: str):
"""List objects and ingest all matching files."""
paginator = s3.get_paginator('list_objects_v2')
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
for obj in page.get('Contents', []):
key = obj['Key']
if key.endswith('.parquet'):
ingest_parquet(bucket, key)
def ingest_parquet(bucket: str, key: str) -> pl.DataFrame:
"""Ingest Parquet file from S3."""
response = s3.get_object(Bucket=bucket, Key=key)
return pl.read_parquet(BytesIO(response['Body'].read()))
# Stream large files
def stream_csv(bucket: str, key: str, chunk_size: int = 10000):
"""Stream CSV in chunks for memory efficiency."""
response = s3.get_object(Bucket=bucket, Key=key)
reader = pl.read_csv_batched(response['Body'], batch_size=chunk_size)
while True:
batch = reader.next_batches(1)
if not batch:
break
yield batch[0]TypeScript with AWS SDK v3
import { S3Client, GetObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3";
import { Readable } from "stream";
const s3 = new S3Client({ region: process.env.AWS_REGION });
async function* listObjects(bucket: string, prefix: string) {
let continuationToken: string | undefined;
do {
const response = await s3.send(new ListObjectsV2Command({
Bucket: bucket,
Prefix: prefix,
ContinuationToken: continuationToken
}));
for (const obj of response.Contents ?? []) {
yield obj;
}
continuationToken = response.NextContinuationToken;
} while (continuationToken);
}
async function downloadAsStream(bucket: string, key: string): Promise<Readable> {
const response = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
return response.Body as Readable;
}GCS (Google Cloud)
Python with gcsfs
import gcsfs
import polars as pl
fs = gcsfs.GCSFileSystem(project='my-project')
# Direct read with Polars
df = pl.read_parquet('gs://bucket/path/data.parquet')
# List and process
files = fs.glob('gs://bucket/data/*.csv')
for file in files:
with fs.open(file) as f:
df = pl.read_csv(f)
process(df)Azure Blob Storage
Python with adlfs
import adlfs
import polars as pl
fs = adlfs.AzureBlobFileSystem(
account_name='storageaccount',
account_key='...' # or use managed identity
)
# Read directly
df = pl.read_parquet('abfs://container/path/data.parquet')Best Practices
1. Use appropriate file formats:
- Parquet for analytics (columnar, compressed)
- JSON Lines for streaming/logs
- CSV only for interchange with external systems
2. Implement resumable downloads:
def download_with_resume(bucket, key, local_path):
existing_size = os.path.getsize(local_path) if os.path.exists(local_path) else 0
response = s3.get_object(
Bucket=bucket,
Key=key,
Range=f'bytes={existing_size}-'
)
with open(local_path, 'ab') as f:
f.write(response['Body'].read())3. Track processed files:
def mark_processed(key: str):
db.execute(
"INSERT INTO processed_files (key, processed_at) VALUES (?, ?)",
(key, datetime.utcnow())
)4. Handle rate limits:
- Use exponential backoff
- Implement request throttling
- Consider S3 Inventory for large bucket listings
Database Migration & CDC
Table of Contents
- Schema Migration Tools
- Python (Alembic)
- TypeScript (Drizzle)
- Bulk Data Migration
- Python - Table to Table
- With Transformation
- Change Data Capture (CDC)
- PostgreSQL Logical Replication
- Debezium (Docker)
- Initial Load + CDC Pattern
- Data Validation
Schema Migration Tools
Python (Alembic)
# alembic/versions/001_initial.py
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table(
'users',
sa.Column('id', sa.Integer, primary_key=True),
sa.Column('email', sa.String(255), nullable=False, unique=True),
sa.Column('created_at', sa.DateTime, server_default=sa.func.now())
)
op.create_index('idx_users_email', 'users', ['email'])
def downgrade():
op.drop_table('users')TypeScript (Drizzle)
// drizzle/0001_initial.ts
import { pgTable, serial, varchar, timestamp } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: serial("id").primaryKey(),
email: varchar("email", { length: 255 }).notNull().unique(),
createdAt: timestamp("created_at").defaultNow()
});
// Run migration
import { migrate } from "drizzle-orm/node-postgres/migrator";
await migrate(db, { migrationsFolder: "./drizzle" });Bulk Data Migration
Python - Table to Table
import polars as pl
from sqlalchemy import create_engine
source = create_engine("postgresql://source_db")
target = create_engine("postgresql://target_db")
def migrate_table(table_name: str, batch_size: int = 10000):
"""Migrate table in batches."""
offset = 0
while True:
# Read batch from source
query = f"SELECT * FROM {table_name} ORDER BY id LIMIT {batch_size} OFFSET {offset}"
df = pl.read_database(query, source)
if len(df) == 0:
break
# Write to target
df.write_database(
table_name=table_name,
connection=target,
if_table_exists="append"
)
offset += batch_size
print(f"Migrated {offset} rows...")With Transformation
def migrate_with_transform(source_table: str, target_table: str):
"""Migrate with schema transformation."""
df = pl.read_database(f"SELECT * FROM {source_table}", source)
# Transform
df = df.rename({"old_column": "new_column"})
df = df.with_columns([
pl.col("amount").cast(pl.Decimal(10, 2)),
pl.col("created_at").str.to_datetime()
])
df = df.drop("deprecated_field")
# Write
df.write_database(target_table, target, if_table_exists="replace")Change Data Capture (CDC)
PostgreSQL Logical Replication
import psycopg2
from psycopg2.extras import LogicalReplicationConnection
def consume_changes(slot_name: str = "my_slot"):
conn = psycopg2.connect(
connection_factory=LogicalReplicationConnection,
dsn="postgresql://localhost/mydb"
)
cursor = conn.cursor()
# Create replication slot
cursor.create_replication_slot(slot_name, output_plugin='pgoutput')
# Start replication
cursor.start_replication(slot_name=slot_name)
def consume(msg):
# Parse logical replication message
change = parse_pgoutput(msg.payload)
process_change(change)
msg.cursor.send_feedback(flush_lsn=msg.data_start)
cursor.consume_stream(consume)Debezium (Docker)
# docker-compose.yml
version: '3'
services:
zookeeper:
image: confluentinc/cp-zookeeper:7.5.0
environment:
ZOOKEEPER_CLIENT_PORT: 2181
kafka:
image: confluentinc/cp-kafka:7.5.0
depends_on: [zookeeper]
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
debezium:
image: debezium/connect:2.4
depends_on: [kafka]
environment:
BOOTSTRAP_SERVERS: kafka:9092
GROUP_ID: 1
CONFIG_STORAGE_TOPIC: debezium_config
OFFSET_STORAGE_TOPIC: debezium_offsets
STATUS_STORAGE_TOPIC: debezium_status# Register Debezium connector
import requests
connector_config = {
"name": "postgres-connector",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"database.hostname": "postgres",
"database.port": "5432",
"database.user": "debezium",
"database.password": "secret",
"database.dbname": "mydb",
"topic.prefix": "mydb",
"table.include.list": "public.users,public.orders",
"plugin.name": "pgoutput"
}
}
requests.post(
"http://localhost:8083/connectors",
json=connector_config
)Initial Load + CDC Pattern
async def initial_load_with_cdc(table: str):
"""
1. Start CDC capture
2. Take snapshot
3. Apply CDC changes after snapshot
"""
# Mark CDC start position
cdc_start = await get_current_lsn()
# Full table snapshot
snapshot_df = pl.read_database(f"SELECT * FROM {table}", source)
await write_to_target(snapshot_df, table)
# Apply buffered CDC changes
changes = await get_changes_since(cdc_start)
for change in changes:
await apply_change(change)
# Continue with live CDC
await consume_cdc_stream()Data Validation
def validate_migration(source_table: str, target_table: str) -> dict:
"""Validate migration completeness."""
source_count = pl.read_database(
f"SELECT COUNT(*) as cnt FROM {source_table}", source
)["cnt"][0]
target_count = pl.read_database(
f"SELECT COUNT(*) as cnt FROM {target_table}", target
)["cnt"][0]
# Sample comparison
source_sample = pl.read_database(
f"SELECT * FROM {source_table} ORDER BY RANDOM() LIMIT 100", source
)
target_sample = pl.read_database(
f"SELECT * FROM {target_table} WHERE id IN ({','.join(map(str, source_sample['id']))})", target
)
return {
"source_count": source_count,
"target_count": target_count,
"count_match": source_count == target_count,
"sample_match": source_sample.equals(target_sample)
}ETL Tools Reference
Table of Contents
- dlt (data load tool) - Recommended
- Installation
- Basic Pipeline
- Incremental Loading
- Transformations with dbt
- Meltano (ELT Platform)
- Installation
- Add Extractors/Loaders
- Configuration
- Run Pipeline
- Dagster (Orchestration + ELT)
- Assets-based Pipeline
- Airbyte (Low-Code ELT)
- Docker Setup
- API Configuration
- Tool Selection Guide
- Comparison Matrix
dlt (data load tool) - Recommended
Modern Python-first ETL with automatic schema evolution.
Installation
pip install dlt[postgres] # or dlt[duckdb], dlt[bigquery], etc.Basic Pipeline
import dlt
# Define source with resources
@dlt.source
def api_source(api_key: str):
@dlt.resource(write_disposition="merge", primary_key="id")
def users():
response = requests.get(
"https://api.example.com/users",
headers={"Authorization": f"Bearer {api_key}"}
)
yield response.json()
@dlt.resource(write_disposition="append")
def events():
response = requests.get("https://api.example.com/events")
yield response.json()
return users, events
# Create pipeline
pipeline = dlt.pipeline(
pipeline_name="api_pipeline",
destination="postgres",
dataset_name="raw_data"
)
# Run
load_info = pipeline.run(api_source(os.environ["API_KEY"]))
print(load_info)Incremental Loading
@dlt.source
def incremental_source():
@dlt.resource(primary_key="id")
def orders(
updated_at=dlt.sources.incremental("updated_at", initial_value="2024-01-01")
):
# Only fetch records after last updated_at
response = requests.get(
f"https://api.example.com/orders?updated_after={updated_at.last_value}"
)
yield response.json()
return ordersTransformations with dbt
# Run dbt models after loading
pipeline.run(source)
pipeline.run(
dbt_package="./dbt_project",
select=["staging", "marts"]
)Meltano (ELT Platform)
Singer-based with 500+ connectors.
Installation
pip install meltano
meltano init my_project
cd my_projectAdd Extractors/Loaders
# Add extractors (sources)
meltano add extractor tap-github
meltano add extractor tap-salesforce
# Add loaders (destinations)
meltano add loader target-postgres
meltano add loader target-snowflakeConfiguration
# meltano.yml
plugins:
extractors:
- name: tap-github
config:
repository: owner/repo
start_date: '2024-01-01'
loaders:
- name: target-postgres
config:
host: localhost
port: 5432
database: warehouseRun Pipeline
meltano run tap-github target-postgresDagster (Orchestration + ELT)
Assets-based Pipeline
from dagster import asset, Definitions
from dagster_duckdb import DuckDBResource
@asset
def raw_orders(duckdb: DuckDBResource):
"""Load orders from S3."""
with duckdb.get_connection() as conn:
conn.execute("""
CREATE TABLE raw_orders AS
SELECT * FROM read_parquet('s3://bucket/orders/*.parquet')
""")
@asset(deps=[raw_orders])
def cleaned_orders(duckdb: DuckDBResource):
"""Clean and transform orders."""
with duckdb.get_connection() as conn:
conn.execute("""
CREATE TABLE cleaned_orders AS
SELECT
id,
customer_id,
CAST(amount AS DECIMAL(10,2)) as amount,
status
FROM raw_orders
WHERE status IS NOT NULL
""")
defs = Definitions(
assets=[raw_orders, cleaned_orders],
resources={"duckdb": DuckDBResource(database="warehouse.db")}
)Airbyte (Low-Code ELT)
Docker Setup
git clone https://github.com/airbytehq/airbyte.git
cd airbyte
./run-ab-platform.shAPI Configuration
import requests
# Create source
source = requests.post(
"http://localhost:8000/api/v1/sources/create",
json={
"name": "My PostgreSQL",
"sourceDefinitionId": "decd338e-5647-4c0b-adf4-da0e75f5a750",
"workspaceId": "...",
"connectionConfiguration": {
"host": "localhost",
"port": 5432,
"database": "source_db"
}
}
)Tool Selection Guide
| Use Case | Tool | Why |
|---|---|---|
| Python-first, flexible | dlt | Pythonic, auto schema, fast |
| Many connectors needed | Meltano/Airbyte | 500+ pre-built |
| Complex orchestration | Dagster | Assets, observability |
| Enterprise/managed | Fivetran | SaaS, guaranteed |
| Real-time streaming | Custom + Kafka | Low latency |
Comparison Matrix
| Feature | dlt | Meltano | Dagster | Airbyte |
|---|---|---|---|---|
| Setup | pip install | CLI | pip install | Docker |
| Connectors | 50+ | 500+ | 50+ | 300+ |
| Custom sources | Python | Singer tap | Python | Java/Python |
| Schema evolution | Automatic | Manual | Manual | Automatic |
| Orchestration | Basic | Airflow | Built-in | Basic |
| Incremental | Yes | Yes | Yes | Yes |
| Transformations | dbt | dbt | Built-in | dbt |
| Hosting | Self | Self | Cloud/Self | Cloud/Self |
File Format Handling
Table of Contents
- CSV Processing
- Python (Polars - Recommended)
- TypeScript (papaparse)
- JSON Processing
- JSON Lines (Recommended for streaming)
- Nested JSON
- Parquet (Analytics Recommended)
- Python
- Rust (arrow-rs)
- Excel Processing
- Python (openpyxl + polars)
- Schema Validation
- Python with Pandera
- Format Selection Guide
CSV Processing
Python (Polars - Recommended)
import polars as pl
# Basic read with type inference
df = pl.read_csv("data.csv")
# With explicit schema
df = pl.read_csv(
"data.csv",
schema={
"id": pl.Int64,
"name": pl.Utf8,
"amount": pl.Float64,
"created_at": pl.Datetime
},
null_values=["", "NULL", "N/A"],
skip_rows=1 # Skip header if needed
)
# Chunked reading for large files
reader = pl.read_csv_batched("large.csv", batch_size=100_000)
while True:
batch = reader.next_batches(1)
if not batch:
break
process_batch(batch[0])TypeScript (papaparse)
import Papa from "papaparse";
import fs from "fs";
// Streaming parse
const file = fs.createReadStream("data.csv");
Papa.parse(file, {
header: true,
dynamicTyping: true,
step: (row) => {
// Process each row
processRow(row.data);
},
complete: () => {
console.log("Parsing complete");
}
});JSON Processing
JSON Lines (Recommended for streaming)
import polars as pl
import json
# Polars native NDJSON
df = pl.read_ndjson("events.jsonl")
# Streaming JSON Lines
def stream_jsonl(path: str):
with open(path) as f:
for line in f:
yield json.loads(line)
# Write JSON Lines
df.write_ndjson("output.jsonl")Nested JSON
# Flatten nested structures
df = pl.read_json("nested.json")
df = df.unnest("metadata").unnest("user")Parquet (Analytics Recommended)
Python
import polars as pl
# Read with column selection (efficient!)
df = pl.read_parquet(
"data.parquet",
columns=["id", "amount", "created_at"]
)
# Read from S3 directly
df = pl.read_parquet("s3://bucket/data.parquet")
# Write with compression
df.write_parquet("output.parquet", compression="zstd")
# Partitioned writes
df.write_parquet(
"output/",
partition_by=["year", "month"]
)Rust (arrow-rs)
use arrow::parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
use std::fs::File;
fn read_parquet(path: &str) -> Result<Vec<RecordBatch>> {
let file = File::open(path)?;
let builder = ParquetRecordBatchReaderBuilder::try_new(file)?;
let reader = builder.build()?;
reader.collect()
}Excel Processing
Python (openpyxl + polars)
import polars as pl
# Read specific sheet
df = pl.read_excel(
"data.xlsx",
sheet_name="Sheet1",
read_options={"header_row": 0}
)
# Read all sheets
sheets = pl.read_excel("data.xlsx", sheet_name=None)
for name, df in sheets.items():
print(f"Sheet: {name}, Rows: {len(df)}")Schema Validation
Python with Pandera
import pandera as pa
import polars as pl
schema = pa.DataFrameSchema({
"id": pa.Column(int, nullable=False, unique=True),
"email": pa.Column(str, pa.Check.str_matches(r'^[\w\.-]+@[\w\.-]+\.\w+$')),
"amount": pa.Column(float, pa.Check.ge(0)),
"status": pa.Column(str, pa.Check.isin(["pending", "completed", "failed"]))
})
# Validate
df = pl.read_csv("data.csv")
validated = schema.validate(df.to_pandas())Format Selection Guide
| Use Case | Format | Why |
|---|---|---|
| Analytics/BI | Parquet | Columnar, compressed, fast |
| Streaming/Logs | JSON Lines | Appendable, streamable |
| Data Exchange | CSV | Universal compatibility |
| Human Editing | Excel/CSV | Familiar tools |
| Configuration | JSON/YAML | Structured, readable |
Streaming Data Ingestion
Table of Contents
- Apache Kafka
- Python (confluent-kafka)
- TypeScript (kafkajs)
- Rust (rdkafka)
- AWS Kinesis
- Python (boto3)
- Google Pub/Sub
- Python
- Exactly-Once Semantics
- Pattern: Idempotent Processing
- Pattern: Outbox for Reliability
- Dead Letter Queues
- Backpressure Handling
Apache Kafka
Python (confluent-kafka)
from confluent_kafka import Consumer, KafkaError
import json
def create_consumer(group_id: str, topics: list[str]) -> Consumer:
conf = {
'bootstrap.servers': 'localhost:9092',
'group.id': group_id,
'auto.offset.reset': 'earliest',
'enable.auto.commit': False # Manual commit for exactly-once
}
consumer = Consumer(conf)
consumer.subscribe(topics)
return consumer
def consume_messages(consumer: Consumer, batch_size: int = 100):
"""Consume messages with manual commit."""
messages = []
while True:
msg = consumer.poll(timeout=1.0)
if msg is None:
continue
if msg.error():
if msg.error().code() == KafkaError._PARTITION_EOF:
continue
raise Exception(msg.error())
messages.append(json.loads(msg.value().decode()))
if len(messages) >= batch_size:
yield messages
consumer.commit()
messages = []
# Usage
consumer = create_consumer("my-group", ["events"])
for batch in consume_messages(consumer):
process_batch(batch)TypeScript (kafkajs)
import { Kafka, Consumer, EachBatchPayload } from "kafkajs";
const kafka = new Kafka({
clientId: "my-app",
brokers: ["localhost:9092"]
});
const consumer = kafka.consumer({ groupId: "my-group" });
async function startConsumer() {
await consumer.connect();
await consumer.subscribe({ topic: "events", fromBeginning: true });
await consumer.run({
eachBatch: async ({ batch, resolveOffset, heartbeat }: EachBatchPayload) => {
for (const message of batch.messages) {
const event = JSON.parse(message.value!.toString());
await processEvent(event);
resolveOffset(message.offset);
await heartbeat();
}
}
});
}Rust (rdkafka)
use rdkafka::consumer::{Consumer, StreamConsumer};
use rdkafka::Message;
async fn consume_events(consumer: StreamConsumer) -> Result<()> {
loop {
match consumer.recv().await {
Ok(msg) => {
if let Some(payload) = msg.payload() {
let event: Event = serde_json::from_slice(payload)?;
process_event(event).await?;
}
consumer.commit_message(&msg, CommitMode::Async)?;
}
Err(e) => eprintln!("Kafka error: {}", e),
}
}
}AWS Kinesis
Python (boto3)
import boto3
import json
kinesis = boto3.client('kinesis')
def consume_kinesis(stream_name: str, shard_id: str):
# Get shard iterator
response = kinesis.get_shard_iterator(
StreamName=stream_name,
ShardId=shard_id,
ShardIteratorType='LATEST'
)
shard_iterator = response['ShardIterator']
while True:
response = kinesis.get_records(
ShardIterator=shard_iterator,
Limit=100
)
for record in response['Records']:
data = json.loads(record['Data'])
yield data
shard_iterator = response['NextShardIterator']Google Pub/Sub
Python
from google.cloud import pubsub_v1
import json
subscriber = pubsub_v1.SubscriberClient()
subscription_path = subscriber.subscription_path("project", "subscription")
def callback(message):
data = json.loads(message.data.decode())
process_message(data)
message.ack()
streaming_pull = subscriber.subscribe(subscription_path, callback=callback)
# Run forever
streaming_pull.result()Exactly-Once Semantics
Pattern: Idempotent Processing
async def process_with_idempotency(event: dict):
event_id = event["id"]
# Check if already processed
existing = await db.execute(
"SELECT 1 FROM processed_events WHERE event_id = ?",
(event_id,)
)
if existing:
return # Skip duplicate
# Process in transaction
async with db.transaction():
await process_event(event)
await db.execute(
"INSERT INTO processed_events (event_id, processed_at) VALUES (?, ?)",
(event_id, datetime.utcnow())
)Pattern: Outbox for Reliability
async def process_with_outbox(event: dict):
async with db.transaction():
# 1. Write to outbox
await db.execute(
"INSERT INTO outbox (event_id, payload, status) VALUES (?, ?, 'pending')",
(event["id"], json.dumps(event))
)
# 2. Process event
result = await process_event(event)
# 3. Mark complete
await db.execute(
"UPDATE outbox SET status = 'complete' WHERE event_id = ?",
(event["id"],)
)
return resultDead Letter Queues
async def consume_with_dlq(consumer, dlq_producer):
for msg in consumer:
try:
await process_message(msg)
consumer.commit()
except Exception as e:
# Send to DLQ after max retries
if msg.retry_count >= 3:
await dlq_producer.send(
topic="events-dlq",
value={
"original": msg.value,
"error": str(e),
"failed_at": datetime.utcnow().isoformat()
}
)
consumer.commit()
else:
# Re-queue for retry
raiseBackpressure Handling
import asyncio
from asyncio import Semaphore
class BackpressureConsumer:
def __init__(self, max_concurrent: int = 100):
self.semaphore = Semaphore(max_concurrent)
async def process_with_backpressure(self, message):
async with self.semaphore:
await process_message(message)
async def consume(self, consumer):
tasks = []
async for msg in consumer:
task = asyncio.create_task(self.process_with_backpressure(msg))
tasks.append(task)
# Periodically clean completed tasks
if len(tasks) > 1000:
tasks = [t for t in tasks if not t.done()]#!/usr/bin/env python3
"""
dlt Pipeline Generator
Generates a dlt (data load tool) pipeline scaffold for common ingestion patterns.
Creates boilerplate code for API, file, or database sources.
Usage:
python generate_dlt_pipeline.py --source api --name github_issues --output ./pipelines
python generate_dlt_pipeline.py --source s3 --name raw_events --output ./pipelines
python generate_dlt_pipeline.py --source database --name legacy_users --output ./pipelines
"""
import argparse
import sys
from pathlib import Path
from textwrap import dedent
def generate_api_pipeline(name: str) -> str:
"""Generate API source pipeline."""
return dedent(f'''
"""
dlt Pipeline: {name}
Ingests data from REST API with incremental loading support.
Usage:
python {name}_pipeline.py
"""
import dlt
import requests
from typing import Iterator
@dlt.source
def {name}_source(api_base_url: str, api_key: str = dlt.secrets.value):
"""
Source for {name} API data.
Args:
api_base_url: Base URL of the API
api_key: API key for authentication
"""
@dlt.resource(
write_disposition="merge",
primary_key="id"
)
def items(
updated_at=dlt.sources.incremental("updated_at", initial_value="2024-01-01T00:00:00Z")
) -> Iterator[dict]:
"""Fetch items with incremental loading."""
headers = {{"Authorization": f"Bearer {{api_key}}"}}
page = 1
while True:
response = requests.get(
f"{{api_base_url}}/items",
headers=headers,
params={{
"updated_after": updated_at.last_value,
"page": page,
"per_page": 100
}}
)
response.raise_for_status()
data = response.json()
if not data["items"]:
break
yield from data["items"]
page += 1
return items
def main():
# Create pipeline
pipeline = dlt.pipeline(
pipeline_name="{name}",
destination="duckdb", # Change to postgres, bigquery, etc.
dataset_name="{name}_data"
)
# Run pipeline
load_info = pipeline.run(
{name}_source(api_base_url="https://api.example.com")
)
print(load_info)
if __name__ == "__main__":
main()
''').strip()
def generate_s3_pipeline(name: str) -> str:
"""Generate S3 source pipeline."""
return dedent(f'''
"""
dlt Pipeline: {name}
Ingests Parquet/CSV files from S3 bucket.
Usage:
python {name}_pipeline.py
"""
import dlt
import boto3
import polars as pl
from io import BytesIO
from typing import Iterator
@dlt.source
def {name}_source(bucket: str, prefix: str):
"""
Source for {name} S3 data.
Args:
bucket: S3 bucket name
prefix: Object prefix to filter
"""
s3 = boto3.client("s3")
@dlt.resource(write_disposition="append")
def files() -> Iterator[dict]:
"""Ingest files from S3."""
paginator = s3.get_paginator("list_objects_v2")
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
for obj in page.get("Contents", []):
key = obj["Key"]
# Skip non-data files
if not (key.endswith(".parquet") or key.endswith(".csv")):
continue
# Download and parse
response = s3.get_object(Bucket=bucket, Key=key)
body = response["Body"].read()
if key.endswith(".parquet"):
df = pl.read_parquet(BytesIO(body))
else:
df = pl.read_csv(BytesIO(body))
# Yield records
for record in df.to_dicts():
record["_source_file"] = key
yield record
return files
def main():
# Create pipeline
pipeline = dlt.pipeline(
pipeline_name="{name}",
destination="duckdb",
dataset_name="{name}_data"
)
# Run pipeline
load_info = pipeline.run(
{name}_source(
bucket="my-data-bucket",
prefix="raw/events/"
)
)
print(load_info)
if __name__ == "__main__":
main()
''').strip()
def generate_database_pipeline(name: str) -> str:
"""Generate database source pipeline."""
return dedent(f'''
"""
dlt Pipeline: {name}
Migrates data from source database with incremental loading.
Usage:
python {name}_pipeline.py
"""
import dlt
from dlt.sources.sql_database import sql_database
def main():
# Source database connection
source_db = sql_database(
credentials=dlt.secrets["sources.{name}.credentials"],
schema="public",
table_names=["users", "orders", "products"],
incremental=dlt.sources.incremental("updated_at")
)
# Create pipeline
pipeline = dlt.pipeline(
pipeline_name="{name}",
destination="postgres",
dataset_name="{name}_data"
)
# Run pipeline
load_info = pipeline.run(source_db)
print(load_info)
if __name__ == "__main__":
main()
''').strip()
TEMPLATES = {
"api": generate_api_pipeline,
"s3": generate_s3_pipeline,
"database": generate_database_pipeline,
}
def main():
parser = argparse.ArgumentParser(
description="Generate dlt pipeline scaffold"
)
parser.add_argument(
"--source",
choices=["api", "s3", "database"],
required=True,
help="Source type"
)
parser.add_argument(
"--name",
required=True,
help="Pipeline name (snake_case)"
)
parser.add_argument(
"--output",
type=Path,
default=Path("."),
help="Output directory"
)
args = parser.parse_args()
# Generate code
generator = TEMPLATES[args.source]
code = generator(args.name)
# Write file
output_file = args.output / f"{args.name}_pipeline.py"
args.output.mkdir(parents=True, exist_ok=True)
output_file.write_text(code)
print(f"Generated: {output_file}")
print("")
print("Next steps:")
print(f" 1. Edit {output_file} to configure your source")
print(" 2. Add credentials to .dlt/secrets.toml")
print(f" 3. Run: python {output_file}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
S3 Connection Testing Tool
Tests S3 bucket connectivity, permissions, and file listing.
Useful for validating AWS credentials before ingestion.
Usage:
python test_s3_connection.py --bucket my-bucket
python test_s3_connection.py --bucket my-bucket --prefix data/2024/
python test_s3_connection.py --bucket my-bucket --profile production
"""
import argparse
import sys
from datetime import datetime
try:
import boto3
from botocore.exceptions import ClientError, NoCredentialsError
except ImportError:
print("Error: boto3 library not installed")
print("Install with: pip install boto3")
sys.exit(1)
def test_connection(bucket: str, prefix: str = "", profile: str = None) -> bool:
"""Test S3 bucket connection and permissions."""
# Create session
if profile:
session = boto3.Session(profile_name=profile)
print(f"Using AWS profile: {profile}")
else:
session = boto3.Session()
print("Using default AWS credentials")
s3 = session.client("s3")
print(f"Bucket: {bucket}")
print(f"Prefix: {prefix or '(root)'}")
print("")
# Test 1: Check bucket exists and is accessible
print("1. Testing bucket access...")
try:
s3.head_bucket(Bucket=bucket)
print(" Bucket exists and is accessible")
except ClientError as e:
error_code = e.response["Error"]["Code"]
if error_code == "404":
print(f" Bucket not found: {bucket}")
elif error_code == "403":
print(f" Access denied to bucket: {bucket}")
else:
print(f" Error: {e}")
return False
except NoCredentialsError:
print(" No AWS credentials found")
print(" Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY")
return False
# Test 2: List objects
print("2. Testing list objects permission...")
try:
response = s3.list_objects_v2(
Bucket=bucket,
Prefix=prefix,
MaxKeys=10
)
count = response.get("KeyCount", 0)
print(f" Found {count} objects (showing up to 10)")
if "Contents" in response:
for obj in response["Contents"][:5]:
size = obj["Size"]
key = obj["Key"]
print(f" - {key} ({format_size(size)})")
if count > 5:
print(f" ... and {count - 5} more")
except ClientError as e:
print(f" List objects failed: {e}")
return False
# Test 3: Check read permission on first object
print("3. Testing read permission...")
if "Contents" in response and response["Contents"]:
test_key = response["Contents"][0]["Key"]
try:
s3.head_object(Bucket=bucket, Key=test_key)
print(f" Can read: {test_key}")
except ClientError as e:
print(f" Read permission denied: {e}")
return False
else:
print(" No objects to test (bucket may be empty)")
# Test 4: Check region
print("4. Checking bucket region...")
try:
location = s3.get_bucket_location(Bucket=bucket)
region = location.get("LocationConstraint") or "us-east-1"
print(f" Bucket region: {region}")
except ClientError:
print(" Could not determine region")
print("")
print("Connection test PASSED")
return True
def format_size(size: int) -> str:
"""Format byte size to human readable."""
for unit in ["B", "KB", "MB", "GB"]:
if size < 1024:
return f"{size:.1f} {unit}"
size /= 1024
return f"{size:.1f} TB"
def main():
parser = argparse.ArgumentParser(
description="Test S3 bucket connectivity"
)
parser.add_argument(
"--bucket",
required=True,
help="S3 bucket name"
)
parser.add_argument(
"--prefix",
default="",
help="Object prefix to filter (optional)"
)
parser.add_argument(
"--profile",
help="AWS profile name (optional)"
)
args = parser.parse_args()
success = test_connection(
bucket=args.bucket,
prefix=args.prefix,
profile=args.profile
)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
CSV Schema Validation Tool
Validates CSV files against expected schema before ingestion.
Checks column names, data types, and value constraints.
Usage:
python validate_csv_schema.py --file data.csv --schema schema.json
python validate_csv_schema.py --file data.csv --columns id:int,name:str,amount:float
"""
import argparse
import json
import sys
from pathlib import Path
from typing import Dict, List, Any
try:
import polars as pl
except ImportError:
print("Error: polars library not installed")
print("Install with: pip install polars")
sys.exit(1)
def parse_column_spec(spec: str) -> Dict[str, str]:
"""Parse column:type specification."""
columns = {}
for item in spec.split(","):
name, dtype = item.split(":")
columns[name.strip()] = dtype.strip()
return columns
def validate_columns(df: pl.DataFrame, expected: Dict[str, str]) -> List[str]:
"""Validate column names and types."""
errors = []
# Check for missing columns
for col in expected:
if col not in df.columns:
errors.append(f"Missing column: {col}")
# Check for extra columns
for col in df.columns:
if col not in expected:
errors.append(f"Unexpected column: {col}")
# Check types
type_map = {
"int": [pl.Int8, pl.Int16, pl.Int32, pl.Int64],
"float": [pl.Float32, pl.Float64],
"str": [pl.Utf8, pl.String],
"bool": [pl.Boolean],
"date": [pl.Date],
"datetime": [pl.Datetime],
}
for col, expected_type in expected.items():
if col in df.columns:
actual_type = df[col].dtype
valid_types = type_map.get(expected_type, [])
if valid_types and actual_type not in valid_types:
errors.append(
f"Column '{col}': expected {expected_type}, got {actual_type}"
)
return errors
def validate_nulls(df: pl.DataFrame, required: List[str]) -> List[str]:
"""Check for null values in required columns."""
errors = []
for col in required:
if col in df.columns:
null_count = df[col].null_count()
if null_count > 0:
errors.append(f"Column '{col}' has {null_count} null values")
return errors
def validate_unique(df: pl.DataFrame, unique: List[str]) -> List[str]:
"""Check for unique constraints."""
errors = []
for col in unique:
if col in df.columns:
total = len(df)
unique_count = df[col].n_unique()
if unique_count < total:
errors.append(
f"Column '{col}' has {total - unique_count} duplicate values"
)
return errors
def main():
parser = argparse.ArgumentParser(
description="Validate CSV file against schema"
)
parser.add_argument(
"--file",
type=Path,
required=True,
help="CSV file to validate"
)
parser.add_argument(
"--schema",
type=Path,
help="JSON schema file"
)
parser.add_argument(
"--columns",
help="Column spec (e.g., id:int,name:str,amount:float)"
)
parser.add_argument(
"--required",
help="Comma-separated list of required (non-null) columns"
)
parser.add_argument(
"--unique",
help="Comma-separated list of unique columns"
)
parser.add_argument(
"--sample",
type=int,
default=1000,
help="Number of rows to sample for validation (default: 1000)"
)
args = parser.parse_args()
# Load CSV
try:
df = pl.read_csv(args.file, n_rows=args.sample)
print(f"Loaded {len(df)} rows from {args.file}")
print(f"Columns: {df.columns}")
print("")
except Exception as e:
print(f"Error reading CSV: {e}")
sys.exit(1)
all_errors = []
# Load schema
if args.schema:
with open(args.schema) as f:
schema = json.load(f)
expected_columns = schema.get("columns", {})
required = schema.get("required", [])
unique = schema.get("unique", [])
elif args.columns:
expected_columns = parse_column_spec(args.columns)
required = args.required.split(",") if args.required else []
unique = args.unique.split(",") if args.unique else []
else:
print("Error: Either --schema or --columns must be provided")
sys.exit(1)
# Validate columns
print("Validating columns...")
all_errors.extend(validate_columns(df, expected_columns))
# Validate nulls
if required:
print("Checking required columns...")
all_errors.extend(validate_nulls(df, required))
# Validate unique
if unique:
print("Checking unique constraints...")
all_errors.extend(validate_unique(df, unique))
# Print results
print("")
if all_errors:
print("Validation FAILED:")
for error in all_errors:
print(f" - {error}")
sys.exit(1)
else:
print("Validation PASSED")
print("")
print("Schema summary:")
for col in df.columns:
print(f" {col}: {df[col].dtype}")
sys.exit(0)
if __name__ == "__main__":
main()
Related skills
FAQ
Which Python tools does it recommend for ETL?
It recommends dlt (data load tool) for modern Python ETL and Polars for fast file processing, which it notes is faster than pandas.
What ingestion patterns does it cover?
It covers batch ingestion from files and storage, streaming ingestion, API polling with cursors, and change data capture (CDC) for database replication.