Skip to main content

Setmore ETL pipelines for Databricks Unity Catalog

Project description

๐Ÿš€ Setmore ETL for Databricks

Python 3.10+ License: MIT Databricks

A production-grade Python library for building reliable, incremental ETL pipelines from Setmore REST API to Databricks Unity Catalog. Built for Databricks Serverless environments with exactly-once semantics, automatic watermarking, and comprehensive audit logging.

โœจ Features

  • ๐Ÿ”„ Incremental Processing - Smart watermark management with configurable lookback windows for late-arriving data
  • ๐ŸŽฏ Exactly-Once Semantics - Deterministic extraction IDs prevent duplicate processing
  • ๐Ÿ” Thread-Safe OAuth - Automatic token refresh with 5-minute buffer for Databricks Serverless
  • ๐Ÿ“Š Unity Catalog Native - First-class support for Databricks catalogs, schemas, and Delta tables
  • ๐Ÿ” Comprehensive Audit Trail - Track every extraction with detailed metrics and error logging
  • ๐Ÿ’พ Delta Lake Optimized - Efficient upsert patterns for appointments, staff, and services
  • ๐Ÿ—๏ธ Type-Safe Schemas - Explicit PySpark schemas eliminate runtime type inference issues
  • ๐Ÿงช Production Ready - Immutable configurations, pure functions, and zero side effects on import

๐Ÿ“‹ Prerequisites

  • Databricks Workspace with Unity Catalog enabled
  • Python 3.10 or higher
  • Setmore account with API access
  • Setmore OAuth refresh token (obtain here)

๐Ÿ“ฆ Installation

From PyPI (Coming Soon)

pip install setmore-etl

From Source

git clone https://github.com/p4w3l/setmore-etl.git
cd setmore-etl
pip install -e .

For Development

pip install -e ".[dev]"

๐Ÿš€ Quick Start

1. Configure Your Pipeline

from setmore_etl import SetmoreConfig

config = SetmoreConfig(
    catalog="main",
    schema="setmore_prod",
    landing_volume="raw_data",
    lookback_hours=2,  # Handle late-arriving appointments
)

2. Extract Appointments

from pyspark.sql import SparkSession
from setmore_etl import (
    SetmoreAPIClient,
    WatermarkManager,
    AuditLogger,
    flatten_appointment,
    APPOINTMENT_SCHEMA
)

# Initialize components
spark = SparkSession.builder.getOrCreate()
client = SetmoreAPIClient(refresh_token="your_token", config=config)
watermark = WatermarkManager(spark, config)
audit = AuditLogger(spark, config)

# Calculate extraction window
window_start, window_end, extraction_id = watermark.calculate_extraction_window(
    pipeline_name="appointments"
)

# Check idempotency
if watermark.check_extraction_exists(extraction_id):
    print(f"Extraction {extraction_id} already completed")
    exit(0)

# Fetch and transform data
appointments = []
cursor = None

while True:
    response = client.fetch_appointments_page(
        start_date=window_start.strftime("%d-%m-%Y"),
        end_date=window_end.strftime("%d-%m-%Y"),
        cursor=cursor
    )
    
    if response.get("response"):
        batch = response["data"].get("appointments", [])
        appointments.extend([
            flatten_appointment(appt, "api", extraction_id)
            for appt in batch
        ])
        cursor = response["data"].get("cursor")
        if not cursor:
            break
    else:
        break

# Write to Delta table
df = spark.createDataFrame(appointments, schema=APPOINTMENT_SCHEMA)
df.write.format("delta").mode("append").saveAsTable(
    f"{config.catalog}.{config.schema}.appointments"
)

# Record watermark
watermark.set_watermark(
    pipeline_name="appointments",
    extraction_id=extraction_id,
    window_start=window_start,
    window_end=window_end,
    records=len(appointments)
)

# Audit logging
audit.log(
    pipeline_name="appointments",
    extraction_id=extraction_id,
    window_start=window_start,
    window_end=window_end,
    appointments=len(appointments),
    status="success"
)

3. Extract Staff and Services

# Staff members
staff_records = client.fetch_all_staff()
staff_df = spark.createDataFrame([
    process_staff_record(s, "api", extraction_id)
    for s in staff_records
], schema=STAFF_SCHEMA)

staff_df.write.format("delta").mode("overwrite").option(
    "overwriteSchema", "true"
).saveAsTable(f"{config.catalog}.{config.schema}.staff")

# Services
service_records = client.fetch_all_services()
services_df = spark.createDataFrame([
    process_service_record(s, "api", extraction_id)
    for s in service_records
], schema=SERVICE_SCHEMA)

services_df.write.format("delta").mode("overwrite").option(
    "overwriteSchema", "true"
).saveAsTable(f"{config.catalog}.{config.schema}.services")

๐Ÿ—๏ธ Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     Setmore REST API                        โ”‚
โ”‚            https://developer.setmore.com/api/v1             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                         โ”‚
                         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                  SetmoreAPIClient                           โ”‚
โ”‚  โ€ข Thread-safe OAuth token management                       โ”‚
โ”‚  โ€ข Automatic pagination                                     โ”‚
โ”‚  โ€ข Request retry logic                                      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                         โ”‚
                         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                  Transformation Layer                       โ”‚
โ”‚  โ€ข flatten_appointment()    โ€ข parse_iso_timestamp()         โ”‚
โ”‚  โ€ข process_staff_record()   โ€ข generate_extraction_id()      โ”‚
โ”‚  โ€ข process_service_record()                                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                         โ”‚
                         โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚              Databricks Unity Catalog                       โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚  appointments  โ”‚  โ”‚    staff     โ”‚  โ”‚   services     โ”‚  โ”‚
โ”‚  โ”‚   (Delta)      โ”‚  โ”‚   (Delta)    โ”‚  โ”‚   (Delta)      โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚  โ”‚  watermarks    โ”‚  โ”‚     audit_log                    โ”‚  โ”‚
โ”‚  โ”‚   (Delta)      โ”‚  โ”‚     (Delta)                      โ”‚  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ“Š Data Models

Appointments

Captures booking details with customer information and temporal partitioning.

Column Type Description
key STRING Unique appointment identifier
start_time TIMESTAMP Appointment start (UTC)
end_time TIMESTAMP Appointment end (UTC)
staff_key STRING Foreign key to staff
service_key STRING Foreign key to service
customer_key STRING Customer identifier
cost DOUBLE Appointment cost
customer_first_name STRING Customer first name
customer_email STRING Customer email
date_key STRING Partition key (YYYY-MM-DD)
extraction_id STRING Extraction batch identifier

Staff

Team member directory with contact information.

Column Type Description
key STRING Unique staff identifier
first_name STRING Staff first name
last_name STRING Staff last name
email_id STRING Staff email
work_phone STRING Contact number
extraction_id STRING Extraction batch identifier

Services

Service catalog with pricing and duration.

Column Type Description
key STRING Unique service identifier
service_name STRING Service display name
staff_keys ARRAY Available staff members
duration INT Service duration (minutes)
cost DOUBLE Service price
extraction_id STRING Extraction batch identifier

๐Ÿ”ง Configuration Reference

SetmoreConfig

@dataclass(frozen=True)
class SetmoreConfig:
    catalog: str              # Unity Catalog name
    schema: str               # Schema name (e.g., "setmore_prod")
    landing_volume: str       # Volume for raw data storage
    base_url: str            # API base URL (default: Setmore v1)
    lookback_hours: int      # Late-arrival window (default: 2)
    watermark_table: str     # Watermark table name
    audit_table: str         # Audit log table name

Environment Variables

For Databricks notebooks, use secrets:

refresh_token = dbutils.secrets.get(scope="setmore", key="refresh_token")

๐ŸŽฏ Use Cases

Healthcare & Wellness

  • Patient appointment analytics
  • Provider utilization tracking
  • Revenue forecasting
  • No-show pattern analysis

Beauty & Personal Care

  • Stylist performance metrics
  • Service popularity trends
  • Customer retention analysis
  • Revenue per service tracking

Professional Services

  • Consultant booking analytics
  • Client meeting analysis
  • Service demand forecasting
  • Staff capacity planning

Education & Tutoring

  • Student session tracking
  • Tutor availability optimization
  • Subject popularity analysis
  • Enrollment trend reporting

๐Ÿ” Advanced Usage

Custom Extraction Windows

from datetime import datetime, timezone, timedelta

# Last 30 days
window_start = datetime.now(timezone.utc) - timedelta(days=30)
window_end = datetime.now(timezone.utc)
extraction_id = generate_extraction_id(window_start, window_end)

Error Handling

try:
    appointments = client.fetch_appointments_page(start_date, end_date)
except requests.HTTPError as e:
    audit.log(
        pipeline_name="appointments",
        extraction_id=extraction_id,
        window_start=window_start,
        window_end=window_end,
        status="failed",
        error=str(e)
    )
    raise

Delta Merge Pattern (Upserts)

from delta.tables import DeltaTable

target_table = f"{config.catalog}.{config.schema}.appointments"

if table_exists(spark, config.catalog, config.schema, "appointments"):
    delta_table = DeltaTable.forName(spark, target_table)
    
    delta_table.alias("target").merge(
        df.alias("source"),
        "target.key = source.key"
    ).whenMatchedUpdateAll().whenNotMatchedInsertAll().execute()
else:
    df.write.format("delta").saveAsTable(target_table)

๐Ÿงช Testing

# Run tests
pytest tests/

# Run with coverage
pytest --cov=setmore_etl tests/

# Type checking
mypy src/setmore_etl

# Linting
ruff check src/
black --check src/

๐Ÿ“š API Reference

Full API documentation available at https://setmore.docs.apiary.io/

Key Endpoints Used

  • GET /o/oauth2/token - OAuth token refresh
  • GET /bookingapi/appointments - Fetch appointments with pagination
  • GET /bookingapi/staffs - Fetch staff members
  • GET /bookingapi/services - Fetch service catalog

๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

Development Setup

# Clone repository
git clone https://github.com/p4w3l/setmore-etl.git
cd setmore-etl

# Create virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install development dependencies
pip install -e ".[dev]"

# Run tests
pytest

Code Style

  • Follow PEP 8 guidelines
  • Use type hints for all function signatures
  • Write docstrings for public APIs
  • Maintain test coverage above 80%

๐Ÿ“ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ™ Acknowledgments

  • Built for Databricks Unity Catalog
  • Integrates with Setmore booking platform
  • Inspired by modern data engineering best practices

๐Ÿ“ž Support

๐Ÿ—บ๏ธ Roadmap

  • Add streaming support for real-time ingestion
  • Implement data quality checks and validations
  • Add support for Setmore webhooks
  • Create dbt models for analytics
  • Add Prefect/Airflow orchestration examples
  • Develop data observability integrations (Monte Carlo, Great Expectations)

Built with โค๏ธ for the data engineering community

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

setmore_etl-0.2.0.tar.gz (50.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

setmore_etl-0.2.0-py3-none-any.whl (15.1 kB view details)

Uploaded Python 3

File details

Details for the file setmore_etl-0.2.0.tar.gz.

File metadata

  • Download URL: setmore_etl-0.2.0.tar.gz
  • Upload date:
  • Size: 50.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.7.21

File hashes

Hashes for setmore_etl-0.2.0.tar.gz
Algorithm Hash digest
SHA256 d756b0dfe9e1b0fdd538b1c16d9513801dfe627c388921fda8e4dffa6023e66f
MD5 7650ddc4b1ab5500f7569cd0b3181662
BLAKE2b-256 aabb5dae5928eb066201c94acc06de0df134cf19ad93b50ac468232604edf19b

See more details on using hashes here.

File details

Details for the file setmore_etl-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for setmore_etl-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 950595f37042be20e4658a6b49a5b94ab7fa3d9486bede52be765db72906fba7
MD5 aeeeb40f29d4ab81368a040e76370f16
BLAKE2b-256 d8460dc6db7fea444d732fdf31d22860b4542a06e1398edd4329330413b4ed37

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page