Local-first ETL/ELT pipelines that run on DuckDB. CLI, Python API, and MCP server for AI agents.
Project description
Duckle
Local-first ETL/ELT pipelines that run on DuckDB. Python builds the plan, DuckDB moves the rows. Your data never leaves the machine.
pip install duckle
duckle quickstart
quickstart scaffolds sample data and a pipeline, runs it, and shows you the rows. One command from nothing to a result, because the engine ships in the install: a ~20 MB native binary plus the DuckDB CLI. No JVM, no Docker, no server, no account.
No install at all, if you have uv:
uvx duckle@latest quickstart
No data passes through Python
Every method appends a node to a pipeline graph. Nothing executes until .run(), and then the whole graph is handed to the engine, compiled to SQL, and executed inside DuckDB.
That is the difference from a dataframe library: a billion-row job costs what the SQL costs, because it is SQL. There is no interpreter in the data path, and no to_pandas() escape hatch quietly pulling rows into memory.
import duckle
from duckle import col
(duckle.read_csv("orders.csv")
.where(col.amount >= 20)
.derive(total="round(amount * 1.2, 2)", tag="f'{region}-{id}'")
.write_parquet("out.parquet")
.run())
Python expressions, compiled to SQL
amount * 1.2, f'{a}-{b}', x if c else y, name.strip().upper() and email is None are translated to vectorized DuckDB SQL at plan time:
.derive(band="'high' if amount > 50 else 'low'")
# CASE WHEN ("amount" > 50) THEN 'high' ELSE 'low' END
An expression with no exact SQL equivalent is rejected with the construct named, never quietly executed somewhere slower. Comprehensions, lambda, indexing and eval are refused by name.
Prefer editor completion over strings? col builds the same tree with real operators, and fragments are reusable:
eu_only = col.region.isin(["EU", "UK"])
p.where(eu_only & (col.amount >= 20))
See exactly what will run, before it runs:
p.explain() # prints the compiled SQL, one block per stage
359 components, not 10 file formats
Component ids map onto attribute paths, so anything in the catalog is reachable:
duckle.src.salesforce(object="Account", authMode="clientCredentials")
duckle.xf.geo.reproject(geomColumn="geom", targetCrs="EPSG:3857")
duckle.snk.salesforce.bulk(operation="upsert", externalIdField="Ext__c")
104 sources, 66 sinks and 138 transforms: Postgres, MySQL, Oracle, SQL Server, Snowflake, Databricks, Teradata, SAP OData, Salesforce (including Bulk API 2.0), Kafka, WebSocket, S3, SFTP, IMAP, LanceDB and more.
duckle.component_ids(contains="salesforce")
duckle.describe("snk.salesforce.bulk") # settings, and which ones the engine ignores
A CI gate that needs no engine
duckle validate compiles every pipeline without opening a source or writing a sink, so it needs no DuckDB, no credentials and no network. Exit codes are stable and safe to gate on:
| code | meaning |
|---|---|
0 |
clean |
1 |
a real finding: a pipeline failed, or did not compile |
2 |
the runner could not start: bad usage, missing engine |
duckle validate --json # machine-readable, for a build step
duckle --pipeline my.json # run one
Note: validate does not yet catch every missing required property, so a clean validate is not proof that a run will succeed.
Agent-ready, with nothing installed
The same package is an MCP server, so an AI agent gets a governed way to work with data instead of a shell. Point Claude Code, Claude Desktop or Cursor at it:
{ "mcpServers": { "duckle": { "command": "uvx", "args": ["duckle", "mcp"] } } }
Or in Claude Code:
claude mcp add duckle -- uvx duckle mcp
That is the whole setup. No pip install, no PATH, no engine to configure: uv fetches the package and the DuckDB engine into a throwaway environment and the server finds it there. If you did pip install duckle, "command": "duckle", "args": ["mcp"] works the same way.
19 tools, including list_components, get_component_schema, create_pipeline, validate_pipeline, run_pipeline, pipeline_lineage, trust_report and schema_drift.
What that buys over letting an agent write a script: it can discover a real connector rather than guess one, compile-check a pipeline before anything executes, run it, and get column-level lineage back. validate_pipeline opens no source and writes no sink, so an agent can check its work without touching your data. The pipeline it produces is the same JSON your desktop canvas opens, so you can inspect what it built.
Code and canvas are the same file
Pipelines are the same JSON the Duckle desktop studio reads. A pipeline written in Python opens on the canvas, and one drawn on the canvas runs from Python:
p.save("pipelines/orders.json") # opens in the studio
duckle.from_json("pipelines/orders.json").run()
Install notes
duckle depends on duckdb-cli, published by the DuckDB Foundation, so the engine arrives with the install and works offline. To pin your own build instead, set DUCKLE_DUCKDB_BIN.
Wheels ship for Linux, macOS and Windows on x86-64 and arm64. The wheel carries a compiled Rust binary and is Python-version independent (py3-none-<platform>).
Apache-2.0 · GitHub · duckle.org · Issues
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file duckle-0.5.8-py3-none-win_arm64.whl.
File metadata
- Download URL: duckle-0.5.8-py3-none-win_arm64.whl
- Upload date:
- Size: 21.8 MB
- Tags: Python 3, Windows ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4e8d6b1b46cef6dc5d84dbc4b77daa7e87bdf9dd39e4bd197f9469757195625d
|
|
| MD5 |
cbbed48af05c08ac7d64bfc528e2d656
|
|
| BLAKE2b-256 |
8477b6d34d4b339838bb424776b7703554256316c7b9a89cef81c90961582694
|
Provenance
The following attestation bundles were made for duckle-0.5.8-py3-none-win_arm64.whl:
Publisher:
pypi.yml on slothflowlabs/duckle
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
duckle-0.5.8-py3-none-win_arm64.whl -
Subject digest:
4e8d6b1b46cef6dc5d84dbc4b77daa7e87bdf9dd39e4bd197f9469757195625d - Sigstore transparency entry: 2211619471
- Sigstore integration time:
-
Permalink:
slothflowlabs/duckle@ec660f42ebb55dc0cf26d9ffb856023691668e0f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/slothflowlabs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@ec660f42ebb55dc0cf26d9ffb856023691668e0f -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file duckle-0.5.8-py3-none-win_amd64.whl.
File metadata
- Download URL: duckle-0.5.8-py3-none-win_amd64.whl
- Upload date:
- Size: 23.0 MB
- Tags: Python 3, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dfdcc13f93a4fb1e3054cd9febe7521fa5e3db974bf99bee832719ac49a3f78b
|
|
| MD5 |
48167e51b9934a8ce7f2fe5995d5e78d
|
|
| BLAKE2b-256 |
6b006f80617328723f2d0b9e72efca02bf7acc481c00870957a111658bbb0423
|
Provenance
The following attestation bundles were made for duckle-0.5.8-py3-none-win_amd64.whl:
Publisher:
pypi.yml on slothflowlabs/duckle
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
duckle-0.5.8-py3-none-win_amd64.whl -
Subject digest:
dfdcc13f93a4fb1e3054cd9febe7521fa5e3db974bf99bee832719ac49a3f78b - Sigstore transparency entry: 2211619350
- Sigstore integration time:
-
Permalink:
slothflowlabs/duckle@ec660f42ebb55dc0cf26d9ffb856023691668e0f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/slothflowlabs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@ec660f42ebb55dc0cf26d9ffb856023691668e0f -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file duckle-0.5.8-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.
File metadata
- Download URL: duckle-0.5.8-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
- Upload date:
- Size: 23.2 MB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a919d764f867a78eb51db225b2e49a5d4b3ab5b59705cf36e7976b6bd0eb4f4
|
|
| MD5 |
85be6407d37d12c96d4119a61fbe55ff
|
|
| BLAKE2b-256 |
15558cb113a272f20b1a5d6ba7bc69ad1d34bf89a2b916a58473dd00cce5d3ef
|
Provenance
The following attestation bundles were made for duckle-0.5.8-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl:
Publisher:
pypi.yml on slothflowlabs/duckle
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
duckle-0.5.8-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl -
Subject digest:
0a919d764f867a78eb51db225b2e49a5d4b3ab5b59705cf36e7976b6bd0eb4f4 - Sigstore transparency entry: 2211619374
- Sigstore integration time:
-
Permalink:
slothflowlabs/duckle@ec660f42ebb55dc0cf26d9ffb856023691668e0f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/slothflowlabs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@ec660f42ebb55dc0cf26d9ffb856023691668e0f -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file duckle-0.5.8-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl.
File metadata
- Download URL: duckle-0.5.8-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
- Upload date:
- Size: 22.3 MB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3d7f6a3686f294c3fe3c0dc34e051f104aebe01544b257d60f172a6567f8624f
|
|
| MD5 |
b8279444b26d10eb6232c94f180fcef5
|
|
| BLAKE2b-256 |
ab2456de6a8434ff14c51e9a6c2c68ee47fae5f557276f1fe309dfddd94f3e43
|
Provenance
The following attestation bundles were made for duckle-0.5.8-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl:
Publisher:
pypi.yml on slothflowlabs/duckle
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
duckle-0.5.8-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl -
Subject digest:
3d7f6a3686f294c3fe3c0dc34e051f104aebe01544b257d60f172a6567f8624f - Sigstore transparency entry: 2211619504
- Sigstore integration time:
-
Permalink:
slothflowlabs/duckle@ec660f42ebb55dc0cf26d9ffb856023691668e0f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/slothflowlabs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@ec660f42ebb55dc0cf26d9ffb856023691668e0f -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file duckle-0.5.8-py3-none-macosx_11_0_x86_64.whl.
File metadata
- Download URL: duckle-0.5.8-py3-none-macosx_11_0_x86_64.whl
- Upload date:
- Size: 21.3 MB
- Tags: Python 3, macOS 11.0+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4d289c45e2eec12664c5c31a162a0a4ffc821b872a96d57e58013f63ce10e39e
|
|
| MD5 |
5f917a9391d154f70454c2200aa9931d
|
|
| BLAKE2b-256 |
08cf56b7ba2eb21edba0d5292d1c5ef667ddc6418c9da987ba06c0ef29872669
|
Provenance
The following attestation bundles were made for duckle-0.5.8-py3-none-macosx_11_0_x86_64.whl:
Publisher:
pypi.yml on slothflowlabs/duckle
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
duckle-0.5.8-py3-none-macosx_11_0_x86_64.whl -
Subject digest:
4d289c45e2eec12664c5c31a162a0a4ffc821b872a96d57e58013f63ce10e39e - Sigstore transparency entry: 2211619435
- Sigstore integration time:
-
Permalink:
slothflowlabs/duckle@ec660f42ebb55dc0cf26d9ffb856023691668e0f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/slothflowlabs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@ec660f42ebb55dc0cf26d9ffb856023691668e0f -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file duckle-0.5.8-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: duckle-0.5.8-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 20.5 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f440afbc26c3c178d8133b2ff81767fa437b17f1f93f714dc5d4a9c22bd6afb0
|
|
| MD5 |
b715192ab0a65d8d7efbe48303ed3b28
|
|
| BLAKE2b-256 |
966b16b45226b7082b22df80df9c0b264fe9b4401621b41759fea8e66937f9bb
|
Provenance
The following attestation bundles were made for duckle-0.5.8-py3-none-macosx_11_0_arm64.whl:
Publisher:
pypi.yml on slothflowlabs/duckle
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
duckle-0.5.8-py3-none-macosx_11_0_arm64.whl -
Subject digest:
f440afbc26c3c178d8133b2ff81767fa437b17f1f93f714dc5d4a9c22bd6afb0 - Sigstore transparency entry: 2211619399
- Sigstore integration time:
-
Permalink:
slothflowlabs/duckle@ec660f42ebb55dc0cf26d9ffb856023691668e0f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/slothflowlabs
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@ec660f42ebb55dc0cf26d9ffb856023691668e0f -
Trigger Event:
workflow_dispatch
-
Statement type: