🪷 Lotos
Self-hosted data ingestion framework — extract, transform, and load data from anywhere.
Lotos is a lightweight, YAML-driven ETL framework built with Python. Define pipelines as configuration, not code.
Features
- YAML-based pipelines — declarative, version-controlled, easy to read
- Pluggable architecture — connectors, transforms, and sinks are auto-discovered via a registry
- Source connectors — SQL databases, REST APIs, local/remote files (CSV, JSON, Parquet)
- Transform chain — select, rename, cast, filter, deduplicate, flatten, SQL transforms, computed columns, custom expressions, and schema validation
- Destination sinks — local files, Azure Blob Storage, Azure Data Lake Storage Gen2
- Full & incremental loading — watermark-based extraction with SQLite-persisted state
- DAG orchestration — dependency-aware parallel execution of pipeline graphs with retries and backoff
- Run history — every execution is tracked (status, row counts, duration, errors)
- State management — inspect, reset, and clear watermarks and history via CLI
- Secret resolution — reference environment variables or
.envvalues with${SECRET:KEY}syntax - Structured logging — powered by
structlogwith JSON or console output - Monitoring — Prometheus metrics endpoint + pre-built Grafana dashboard
- Data quality checks — row count, null %, freshness, uniqueness, value range, regex match
- CLI interface — run, validate, inspect, orchestrate, monitor, and scaffold pipelines from the terminal
Architecture
Installation
pip install -e .
With optional dependencies:
pip install -e ".[azure]" # Azure Blob / ADLS Gen2
pip install -e ".[postgres]" # PostgreSQL
pip install -e ".[mysql]" # MySQL
pip install -e ".[monitoring]" # Prometheus metrics + Grafana
pip install -e ".[all]" # Everything
pip install -e ".[dev]" # Dev tools (pytest, ruff)
Requires Python 3.11+
Quick Start
1. Generate a pipeline template
lotos init my_pipeline --source sql
2. Edit the YAML
pipeline:
name: my_pipeline
description: "Extract customers and clean data"
version: "1.0"
source:
connector: sql
load_mode: incremental # full (default) or incremental
config:
connection_string: "mysql+pymysql://user:pass@localhost:3306/mydb"
query: "SELECT * FROM customers"
transforms:
- type: select_columns
config:
columns: [id, name, email, created_at]
- type: deduplicate
config:
subset: [id]
keep: last
sink:
connector: file
config:
path: "output/customers.parquet"
format: parquet
write_mode: overwrite
watermark:
field: updated_at
initial_value: "2020-01-01"
3. Run it
lotos run pipelines/my_pipeline.yaml --log-level INFO
Load Modes
Lotos supports two extraction strategies, configured per-pipeline via source.load_mode:
Full Load (full)
Extracts all rows from the source on every run. The watermark is ignored for filtering but still tracked for informational purposes.
source:
connector: sql
load_mode: full
config:
query: "SELECT * FROM orders"
Incremental Load (incremental)
Extracts only new/changed rows since the last run, using a watermark column. The watermark value is persisted in a local SQLite state store (.lotos/state.db) and automatically updated after each successful run.
source:
connector: sql
load_mode: incremental
config:
connection_string: "${SECRET:DB_CONN}"
query: "SELECT * FROM orders WHERE updated_at > :watermark"
watermark:
field: updated_at
initial_value: "2024-01-01"
Watermark resolution order:
- Explicit CLI override (
--watermark) - Full mode → always
None(no filter) - Incremental → persisted value from SQLite state store
- Incremental, no persisted value →
watermark.initial_valuefrom YAML
Pipeline Orchestration
Orchestrate multiple pipelines as a directed acyclic graph (DAG). Independent pipelines run in parallel; dependent pipelines wait for their upstream dependencies.
Declaring dependencies
pipeline:
name: load_orders
depends_on:
- load_customers
- load_products
Running the DAG
lotos orchestrate pipelines/ --workers 4 --log-level INFO
Lotos will:
- Discover all
.yaml/.ymlfiles in the directory - Build a dependency graph from
depends_ondeclarations - Topologically sort into execution layers (Kahn's algorithm)
- Execute each layer in parallel with a thread pool
- Skip downstream pipelines if an upstream dependency fails (marked
cancelled) - Retry failed pipelines with exponential backoff (
2^attemptseconds, capped at 120s) - Track every run in the state store
Example DAG
pipelines/
├── 01_customers.yaml # depends_on: []
├── 02_products.yaml # depends_on: []
├── 03_orders.yaml # depends_on: [customers, products]
└── 04_analytics.yaml # depends_on: [orders]
Execution layers:
Layer 1: customers, products (parallel)
Layer 2: orders (waits for layer 1)
Layer 3: analytics (waits for layer 2)
Retry & schedule config
Configure retries and timeouts per pipeline:
schedule:
max_retries: 3 # 1 initial + 3 retries = 4 attempts
retry_backoff: exponential # 2s, 4s, 8s, …, capped at 120s
timeout_seconds: 3600 # per-pipeline timeout warning
State Management
All pipeline state is stored in .lotos/state.db (SQLite with WAL mode).
Watermarks
lotos state show # List all watermarks
lotos state reset my_pipeline # Clear watermark (forces full reload)
Run History
lotos history # Show all runs (last 20)
lotos history my_pipeline # Filter by pipeline
lotos history --status failed -n 50 # Filter by status, show 50
lotos state clear-history # Delete all history
lotos state clear-history my_pipeline # Delete history for one pipeline
CLI Commands
| Command | Description |
|---|---|
lotos run <file> |
Run a single pipeline |
lotos orchestrate <dir> |
Run all pipelines as a DAG |
lotos validate <file> |
Validate YAML without running |
lotos inspect <file> |
Show pipeline details |
lotos init <name> |
Generate a pipeline template |
lotos monitor |
Start Prometheus metrics server |
lotos quality <file> |
Run quality checks only (no sink) |
lotos list connectors |
List available source connectors |
lotos list transforms |
List available transforms |
lotos list sinks |
List available destination sinks |
lotos list all |
List everything |
lotos history [pipeline] |
Show run history |
lotos state show |
Show all stored watermarks |
lotos state reset <name> |
Clear watermark for a pipeline |
lotos state clear-history |
Delete run history |
Run options
--output, -o Save result to file (csv/json/parquet)
--format, -f Output format (default: parquet)
--dry-run Extract + transform only, skip sink
--log-level, -l DEBUG, INFO, WARNING, ERROR
--log-format console or json
Orchestrate options
--workers, -w Max parallel pipelines (default: 4)
--dry-run Extract + transform only
--log-level, -l Log level
--log-format Log format
Monitor options
--port, -p Prometheus HTTP port (default: 9090)
--addr Bind address (default: 0.0.0.0)
--backfill Backfill metrics from state store history
Monitoring & Data Quality
Prometheus metrics
Install with pip install -e ".[monitoring]", then:
lotos monitor --port 9090 --backfill # start metrics server
Exposed metrics at http://localhost:9090/metrics:
| Metric | Type | Labels |
|---|---|---|
lotos_pipeline_runs_total |
Counter | pipeline, status |
lotos_pipeline_duration_seconds |
Histogram | pipeline |
lotos_pipeline_rows_extracted_total |
Counter | pipeline |
lotos_pipeline_rows_loaded_total |
Counter | pipeline |
lotos_pipeline_last_success_timestamp |
Gauge | pipeline |
lotos_pipeline_errors_total |
Counter | pipeline |
lotos_quality_checks_total |
Counter | pipeline, check, status |
Grafana dashboard
Import the pre-built dashboard from lotos/monitoring/grafana_dashboard.json into Grafana.
It includes 14 panels covering pipeline runs, duration percentiles, data volume, quality checks, and error trends.
Data quality checks
Add a quality_checks section to any pipeline YAML:
quality_checks:
- type: row_count
min: 100
action: block # block | warn | alert
- type: null_percentage
column: email
max_percentage: 5.0
action: warn
- type: freshness
column: updated_at
max_age_hours: 24
action: alert
- type: uniqueness
columns: [id]
action: block
- type: value_range
column: age
min: 0
max: 150
action: warn
- type: regex_match
column: email
pattern: "^[\\w.+-]+@[\\w-]+\\.[a-zA-Z]{2,}$"
action: alert
Run checks without loading:
lotos quality pipelines/my_pipeline.yaml
Connectors
| Name | Type | Description |
|---|---|---|
sql |
Source | Any SQLAlchemy-compatible database |
rest_api |
Source | REST APIs with pagination & auth |
file |
Source | CSV, JSON, Parquet (local or Azure Blob) |
file |
Sink | Write to local files or Azure Blob |
adls_gen2 |
Sink | Azure Data Lake Storage Gen2 / Blob Storage |
ADLS Gen2 Sink
The adls_gen2 sink supports both Azure storage modes via the hierarchical_namespace option:
sink:
connector: adls_gen2
config:
account_name: "mystorageaccount"
container_name: "mycontainer"
directory_path: "data/output"
file_name: "results"
format: parquet
hierarchical_namespace: false # false = Blob API, true = Data Lake (DFS) API
account_key: "${SECRET:ADLS_KEY}"
write_mode: overwrite
Authentication methods (in priority order):
account_key— storage account keyconnection_string— full connection stringsas_token— shared access signatureDefaultAzureCredential— automatic (managed identity, env vars, CLI login)
Transforms
| Name | Description |
|---|---|
select_columns |
Keep only specified columns |
rename_columns |
Rename columns via a mapping |
cast_types |
Cast column data types |
filter_rows |
Filter rows by conditions |
deduplicate |
Remove duplicate rows |
flatten |
Flatten nested structs/lists |
sql |
Run SQL queries (Polars SQL — no DB needed) |
computed |
Add computed columns with expressions |
custom |
Apply a custom Python expression |
validate_schema |
Validate column types and constraints |
Project Structure
lotos/
├── cli.py # CLI (Typer)
├── config/ # Settings, logging, secrets
├── connectors/ # Source connectors (SQL, REST, file)
├── core/
│ ├── models.py # Pydantic models (YAML schema)
│ ├── pipeline.py # Pipeline execution engine
│ ├── registry.py # Plugin auto-discovery
│ ├── state.py # SQLite state store
│ └── exceptions.py # Custom exceptions
├── orchestration/
│ └── base.py # DAG orchestrator
├── sinks/ # Destination sinks (file, ADLS Gen2)
├── transforms/ # Transform plugins
└── monitoring/ # Monitoring & quality
├── base.py # Monitor interfaces (Log / Prometheus)
├── metrics.py # Prometheus metrics collector & server
├── quality.py # Data quality checker (6 check types)
└── grafana_dashboard.json # Pre-built Grafana dashboard
pipelines/ # Pipeline YAML definitions
tests/ # Test suite
Development
pip install -e ".[dev]"
pytest
ruff check .
License
Artefact
Metadata
Release files for lotos 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| lotos-1.0.0.tar.gz | 598.3 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| lotos-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 662.4 kB
Release files / lotos-1.0.0.tar.gz
| Download URL | lotos-1.0.0.tar.gz |
|---|---|
| Size | 598.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7548d810f9782f7082b1605d735d171f683abb3274275a7f91ecd52ab973bcb2
|
|
BLAKE2b-256 checksum How to use checksums |
bde4a2ffd1c6ac61d0f0061081d7cc03d3fdd69baa81e10c40bec313478199aa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|
Release files / lotos-1.0.0-py3-none-any.whl
| Download URL | lotos-1.0.0-py3-none-any.whl |
|---|---|
| Size | 64.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
cad27ed0f73c9b2db287db3bfd99d5e30acbc36900b17e098b17e0e409c4ac2b
|
|
BLAKE2b-256 checksum How to use checksums |
40438ed701c4e5887a07047ad6f3ffe5010c5b01e762be7443f51834eedec23e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.15
|
Security for lotos 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.