Skip to main content

URI-based, engine-agnostic ETL (Python port of dataframe-io, built on Ibis)

Project description

DataFrame IO (Python)

A Python port of the Scala dataframe-io library. DataFrame IO allows you to

  • read data from various sources
  • transform it using various transformers
  • write data to various sinks

by specifying source, transform and sink URIs.

Unlike the original, which is built on Apache Spark, this port is built on Ibis — a portable dataframe API. Ibis is the dataframe abstraction: the same pipeline runs on any Ibis backend (duckdb, polars, pyspark, postgres, …) by changing a single --engine flag, with no change to the pipeline itself. The default backend is duckdb.

The URI schema is

protocol://host/path?queryParam1=value1&queryParam2=value2

The protocol decides which source or sink is actually used. Currently, the following options for sources and sinks are available:

For transformations, the following options are available:

Delta, Excel, Hive, Kafka, Solr, Avro and streaming sources from the Scala version are not yet ported.

Installation

uv venv --python 3.12
uv pip install -e '.[dev]'          # add ,polars or ,pyspark for those engines

Running

CLI

The package installs a dfio console script that wires --source, --transform, and --sink URIs into a pipeline. Each option may be repeated.

dfio --source 'data+text:///path/to/in.csv' \
     --transform 'data+out+sql:///SELECT%20*%20FROM%20data%20WHERE%20n%3E1' \
     --sink 'out+parquet:///path/to/out.parquet'

Select a different engine — the rest of the pipeline is unchanged:

dfio --engine polars --source ... --sink ...

dfio --help lists the available schemes.

Python API

from dfio import ETL, Source, Transformation, Sink

ETL(
    sources=[Source.parse("data+text:///path/to/in.csv")],
    transforms=[Transformation.parse("data+out+sql:///SELECT * FROM data WHERE n > 1")],
    sinks=[Sink.parse("out+parquet:///path/to/out.parquet")],
    backend="duckdb",
).run()

Source and Sink URI schemas

The URI schema for sources and sinks generally looks like this

dfToReadInto+sourceType://some-host/some-path?additional=parameters
sourceType://some-host/some-path?additional=parameters

dfToPersist+sinkType://some-host/some-path?additional=parameters
sinkType://some-host/some-path?additional=parameters

The possible options for sourceType and sinkType are listed below. The dfToReadInto is used to save the result of reading the source. If not specified, it defaults to "source". The dfToPersist is the name of the DataFrame that should be persisted to the sink. If not specified, it defaults to "sink".

Both dfToReadInto and dfToPersist are optional. Because they live in the URI scheme, they must be valid scheme characters: use hyphens, not underscores (hyphens are normalized to underscores internally so names are valid SQL identifiers, e.g. my-data becomes the table my_data).

Console

console://anything

The source returns an empty DataFrame. The sink prints an excerpt of the DataFrame to the console.

Values

values:///?header=foo:int,bar:string&values=1,a;2,b

The source returns a DataFrame with column names and types specified in header and values specified in values (rows separated by ;, cells by ,). Supported types are int, long, double; anything else is string. The sink prints an excerpt of the DataFrame to the console.

Text

text:///path/to/some.csv

Reads/writes CSV or TSV files, with the delimiter determined by file extension (.csv,, .tsv → tab). ?header= defaults to true. CSV IO is routed through Apache Arrow so behaviour is identical across every engine.

Parquet

parquet:///path/to/file.parquet

Reads/writes Parquet at the given path.

Transformation URI Schemas

The URI schema for transformations generally looks like this

sourceName+sinkName+transformationType://some-host/some-path?additional=parameters

The sourceName is the previously named intermediate DataFrame used as input. By default it is "source". The sinkName registers the result under a name; by default "sink". Both can be specified or omitted:

transformationType://                       # both default to "source"/"sink"
sourceName+transformationType://             # only sourceName given
sourceName+sinkName+transformationType://    # both given

Identity

sourceName+sinkName+identity:///

Renames a DataFrame from sourceName to sinkName (passthrough).

SQL

sql:///SELECT%20foo%20AS%20bar%20FROM%20sourceName

Applies inline SQL to its input. The SQL must be URL encoded (a space is %20) and follow the triple slash sql:///. The query runs against the engine's catalog, so it may reference any previously registered named DataFrame by name. This is only convenient for short queries.

The SQL is parsed in a fixed dialect (duckdb by default) and transpiled by Ibis to whichever engine runs it, so the same query means the same thing on every backend rather than being reinterpreted per engine. Override the input dialect with ?dialect= (e.g. sql:///<encoded>?dialect=postgres).

SQL-File

sql-file:///path/to/query.sql

Applies SQL read from a file to its input. The referenced tables must have been registered in a previous step.

Flatten

sourceName+sinkName+flatten:///

Recursively unpacks nested struct columns into parent_child columns.

Flatten and explode

sourceName+sinkName+flatten-explode:///

Recursively unpacks struct columns and explodes array columns into rows.

Declarative graphs

A graph file (JSON or YAML) declares data: sources and a pipeline: of nodes referencing each other by id. A top-level engine: key (default duckdb) pins the backend so SQL nodes and plugin nodes share one. Run it from Python:

import dfio

graph = dfio.Graph.load("pipeline.yaml")
graph.bind("vmt", "data/2026-06-23.csv")        # rebind a source path (feature I)
engine = dfio.Engine.from_config(graph.engine)
dfio.run_graph(graph, engine, out_dir="out", only=["coverage"], runtime=cfg)

or from the CLI: dfio --graph pipeline.yaml --bind vmt=NEW.csv --only a,b --out-dir out.

  • Inline params keep their native YAML type and are coerced to strings, so infer: false (a bool) works without quoting.
  • cache: true on a node persists its output to out_dir/<id>.parquet; --only id[,id...] runs just those nodes, loading any other input from that cache (failing fast if absent).
  • A text source/read_csv takes delimiter: (alias sep:) to override the extension default. dfio.read_csv(path, infer=False, empty="string") does the string-faithful read outside a graph, returning a polars.DataFrame.

Extending with plugins

External packages register new schemes via entry points, the Python analogue of the Scala ServiceLoader:

[project.entry-points."dfio.sources"]
my-scheme = "my_package:MyUriParser"

[project.entry-points."dfio.transforms"]
my-transform = "my_package:MyTransformerParser"

# Domain nodes: multi-input, Polars-native, side-effecting steps that fit
# neither a single-input transform nor a source/sink. Each resolves to a
# `(dfio.NodeContext) -> None` callable selected by a pipeline node's `type`.
[project.entry-points."dfio.nodes"]
cluster = "my_package.nodes:cluster_node"

A dfio.nodes plugin receives a NodeContext carrying its id, an ordered inputs dict of polars.DataFrames (in inputs: order), string params, the run's out_dir, and an opaque runtime object passed to run_graph. It reads ctx.input (single-input sugar), may take several inputs, and either calls ctx.emit(df) to register a result for downstream nodes or writes its own files as a pure sink.

Tests

Tests are property-based, using Hypothesis:

.venv/bin/pytest | tee test-output.txt

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

df_etl_cli-0.2.1.tar.gz (114.9 kB view details)

Uploaded Source

Built Distribution

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

df_etl_cli-0.2.1-py3-none-any.whl (31.8 kB view details)

Uploaded Python 3

File details

Details for the file df_etl_cli-0.2.1.tar.gz.

File metadata

  • Download URL: df_etl_cli-0.2.1.tar.gz
  • Upload date:
  • Size: 114.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for df_etl_cli-0.2.1.tar.gz
Algorithm Hash digest
SHA256 47f6e00ecc8c90fd35b235d68359cb0e294ad53a0ca2d3c49f1ee4a3643a7fc7
MD5 78644c5cfa23280c02b3770df4867b90
BLAKE2b-256 3862217fa1bf219ee2b4020fb764f26fe309db37316edd1a79cfdde463cc5254

See more details on using hashes here.

Provenance

The following attestation bundles were made for df_etl_cli-0.2.1.tar.gz:

Publisher: publish.yml on nightscape/df-etl-cli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file df_etl_cli-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: df_etl_cli-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 31.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for df_etl_cli-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 c60425c1e20c065e78e60379cd28dee33a278f4814fcd5b56523561aca800f57
MD5 d32ef2f9efe960b8e0a27e9da6735e72
BLAKE2b-256 71ad0ee9395e1a1e78c80c1fc49f58008948748dddb59254d38cb36b194c9522

See more details on using hashes here.

Provenance

The following attestation bundles were made for df_etl_cli-0.2.1-py3-none-any.whl:

Publisher: publish.yml on nightscape/df-etl-cli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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