floe-python wraps the Floe Rust engine via PyO3 and
maturin, so the same contract-first ingestion gate can
run from Python scripts, Jupyter notebooks, tests, or orchestrators. Use it when
you want Floe inside an existing Python workflow without deploying a service or
starting a separate platform instance.
A single ABI3 wheel covers Python 3.10–3.13.
pip install floe-pythonFour functions cover the main embedded and notebook use cases:
| Function | Returns | When to use |
|---|---|---|
floe.validate(config_path) |
None |
Pre-flight config check before running |
floe.run(config_path) |
RunOutcome |
Execute the full ingestion pipeline |
floe.load_config(config_path) |
RootConfig |
Inspect entities and settings without running |
floe.extract_config_env_vars(config_path) |
dict[str, str] |
List {{VAR}} placeholders in the config |
A quick example for each:
import floe
# Check the config is valid before kicking off a scheduled run
floe.validate("orders.yml")
# Dry run — see what would be processed without moving any data
outcome = floe.run("orders.yml", dry_run=True)
for preview in outcome.dry_run_previews:
print(preview["name"], "→", preview["scanned_files"], "files")
# Inspect what entities are defined (useful in notebooks or CI)
config = floe.load_config("orders.yml")
print(config.entity_names) # ["orders", "returns"]
print(config.entities[0].source_format) # "csv"
# See which env vars need to be set before the pipeline can run
placeholders = floe.extract_config_env_vars("orders.yml")
# e.g. {"report_root": "", "incoming_root": ""}floe.run() returns a RunOutcome. The most useful properties:
outcome = floe.run("orders.yml")
# Top-level summary
summary = outcome.summary
print(summary["run"]["status"]) # "success" | "success_with_warnings" | "failed"
print(summary["results"]["rows_total"]) # total rows scanned
print(summary["results"]["accepted_total"]) # rows written to accepted sink
# Per-entity status lives in summary["entities"], not entity_reports
for entity in summary["entities"]:
print(entity["name"], entity["status"]) # "success" | "rejected" | "failed" | …
# Per-entity detail reports (schema, file-level results, write metadata)
for report in outcome.entity_reports:
name = report["entity"]["name"]
acc = report["results"]["accepted_total"]
rej = report["results"]["rejected_total"]
print(f"{name}: {acc} accepted, {rej} rejected")
# Quick dict for pandas or downstream processing
data = outcome.to_dict()In a Jupyter notebook, RunOutcome renders as a color-coded HTML table automatically — no extra code required:
# In a notebook cell, just end with the variable name:
outcome = floe.run("orders.yml")
outcome # renders inline HTML table with per-entity status rowsThe _repr_html_() method is called automatically by Jupyter. Status colors: green = Success, orange = SuccessWithWarnings, red = Failure.
floe-python supports a single global observer callback that receives structured events as they happen. You register it before run(), then clear it after.
import floe
def on_event(event: dict) -> None:
kind = event["event"]
if kind == "entity_finished":
print(f" {event['name']}: {event['accepted']} accepted, {event['rejected']} rejected")
elif kind == "run_finished":
print(f"Run {event['run_id']}: {event['status']}")
floe.set_observer(on_event)
outcome = floe.run("orders.yml")
floe.clear_observer()The callback is swappable between calls — calling set_observer again replaces the current one. If your callback raises an exception it is silently ignored so the run is never aborted by observer code.
| Event | Key fields |
|---|---|
run_started |
run_id, config, report_base, ts_ms |
entity_started |
run_id, name, ts_ms |
file_started |
run_id, entity, input, ts_ms |
file_finished |
run_id, entity, input, status, rows, accepted, rejected, elapsed_ms |
schema_evolution_applied |
run_id, entity, mode, added_columns |
entity_finished |
run_id, name, status, files, rows, accepted, rejected, warnings, errors |
run_finished |
run_id, status, exit_code, files, rows, accepted, rejected, summary_uri |
log |
run_id, log_level, code, message, entity, input |
All events have an event key (string discriminator) and ts_ms (Unix timestamp in milliseconds). The full TypedDict definitions are in python/floe/_floe.pyi.
Profiles let you override config variables, storage credentials, or lineage settings without editing the YAML. There are two ways to provide them:
# Option 1: load a profile YAML file
floe.run("orders.yml", profile_path="prod.yml")
# Option 2: pass overrides inline (useful in notebooks and CI)
floe.run("orders.yml", profile_vars={
"report_root": "/data/reports",
"incoming_root": "s3://my-bucket/incoming",
})
# validate() also accepts profile overrides
floe.validate("orders.yml", profile_vars={"report_root": "/tmp"})
# Note: load_config() is profile-unaware — it takes only config_pathprofile_vars and profile_path can be combined — inline vars take precedence over the file. See docs/profiles.md for the full profile format.
When a pipeline uses incremental_mode: file, floe tracks which files have already been ingested. Two helper functions let you inspect and reset that state:
# See what floe has already processed for an entity
state = floe.inspect_entity_state("orders.yml", "orders")
print(state["incremental_mode"]) # "file"
print(state["state"]) # dict with ingested file list, or None
# Clear state to force a full re-ingest on the next run
deleted = floe.reset_entity_state("orders.yml", "orders")
print(deleted) # True if state existed and was removedfloe-python raises typed exceptions so you can handle config problems and run failures separately:
try:
outcome = floe.run("orders.yml")
except floe.FloeConfigError as e:
# YAML is invalid or a required field is missing
print(f"Config error: {e}")
except floe.FloeStorageError as e:
# Could not reach S3/GCS/ADLS or read a local path
print(f"Storage error: {e}")
except floe.FloeRunError as e:
# Pipeline ran but encountered a fatal data error
print(f"Run failed: {e}")
except floe.FloeError as e:
# Catch-all for any other floe error
print(f"Floe error: {e}")Exception hierarchy:
FloeError
├── FloeConfigError — config parse or validation failure
├── FloeRunError — pipeline execution failure
├── FloeStorageError — cloud storage or local I/O failure
└── FloeIoError — lower-level I/O error
floe.run() and floe.validate() release the Python GIL while Rust is working, so they are safe to call from threads managed by asyncio, concurrent.futures, or a Dagster worker. You can run multiple pipelines in parallel across threads.
The observer callback is called from the Rust thread and must reacquire the GIL. Keep callbacks fast and non-blocking — avoid heavy pandas operations or network calls inside the callback. Use a queue if you need to hand off work:
import queue, threading
q: queue.Queue = queue.Queue()
floe.set_observer(q.put_nowait) # fast: just enqueues
outcome = floe.run("orders.yml")
floe.clear_observer()
while not q.empty():
print(q.get_nowait()) # process events after the runIf you want to build the native extension yourself (e.g., to test a local floe-core change):
# Install maturin
pip install maturin
# Clone the repo and build in development mode
git clone https://github.com/malon64/floe
cd floe/crates/floe-python
maturin develop # builds and installs into your current virtualenvFor a release-quality wheel:
maturin build --release # produces a .whl in target/wheels/The extension is compiled as floe._floe (a C extension module) and re-exported through floe/__init__.py. The ABI3 flag (abi3-py310) means the same .so file works on Python 3.10, 3.11, 3.12, and 3.13.