Fyrnheim
Activities-first data transformation framework.
Built on Pydantic + Ibis. Define typed sources, detect business events from state changes, resolve identities across systems, and project entity models -- all in Python.
Install
pip install fyrnheim[duckdb]
Quick Start
1. Create a project:
fyr init myproject && cd myproject
2. Define your pipeline in entities/customers.py:
from fyrnheim import (
StateSource, ActivityDefinition, RowAppeared, FieldChanged,
IdentityGraph, IdentitySource, EntityModel, StateField,
)
# Source -- a slowly-changing state table
crm = StateSource(name="crm_contacts", project="p", dataset="raw", table="contacts", id_field="id")
# Activities -- named business events from state changes
signup = ActivityDefinition(name="signup", source="crm_contacts", trigger=RowAppeared())
became_paying = ActivityDefinition(
name="became_paying", source="crm_contacts",
trigger=FieldChanged(field="plan", to_values=["pro", "enterprise"]),
)
# Identity -- resolve across sources
identity = IdentityGraph(
name="customer_identity", canonical_id="customer_id",
sources=[IdentitySource(source="crm_contacts", id_field="id", match_key_field="email")],
)
# Entity -- derived current state
customers = EntityModel(
name="customers", identity_graph="customer_identity",
state_fields=[
StateField(name="email", source="crm_contacts", field="email", strategy="latest"),
StateField(name="plan", source="crm_contacts", field="plan", strategy="latest"),
],
)
3. Run tests:
pytest tests/
Core Concepts
Sources
StateSource -- a slowly-changing table (CRM contacts, subscription records). The diff engine automatically detects row appearances, disappearances, and field changes between snapshots.
StateSource(name="crm_contacts", project="p", dataset="d", table="contacts", id_field="contact_id")
EventSource -- an append-only event stream (page views, transactions).
EventSource(
name="billing_events", project="p", dataset="d", table="transactions",
entity_id_field="customer_id", timestamp_field="created_at", event_type_field="event_type",
)
Activity Definitions
Named business events detected from raw data changes. Each activity ties to a source and a trigger:
| Trigger | Detects |
|---|---|
RowAppeared() |
New row in a state source |
RowDisappeared() |
Row removed from a state source |
FieldChanged(field, to_values) |
Field value changed (optionally to specific values) |
EventOccurred(event_types) |
Specific event types in an event source |
signup = ActivityDefinition(name="signup", source="crm_contacts", trigger=RowAppeared())
became_paying = ActivityDefinition(
name="became_paying", source="crm_contacts",
trigger=FieldChanged(field="plan", to_values=["pro", "enterprise"]),
)
Identity Graph
Cross-source identity resolution. Link records from different systems by a shared match key:
IdentityGraph(
name="customer_identity",
canonical_id="customer_id",
sources=[
IdentitySource(source="crm_contacts", id_field="contact_id", match_key_field="email_hash"),
IdentitySource(source="billing_events", id_field="customer_id", match_key_field="email_hash"),
],
)
Entity Model
Derived current-state projection from resolved identities. Each field picks a source, a column, and a merge strategy (latest, first):
EntityModel(
name="customers",
identity_graph="customer_identity",
state_fields=[
StateField(name="email", source="crm_contacts", field="email", strategy="latest"),
StateField(name="first_seen", source="crm_contacts", field="created_at", strategy="first"),
],
computed_fields=[ComputedColumn(name="is_paying", expression="plan != 'free'")],
)
Analytics Model
Time-grain metric aggregation over the activity stream:
StreamAnalyticsModel(
name="daily_metrics",
identity_graph="customer_identity",
date_grain="daily",
metrics=[
StreamMetric(name="new_signups", expression="count()", event_filter="signup", metric_type="count"),
StreamMetric(name="total_customers", expression="count()", metric_type="snapshot"),
],
)
CLI
fyr init [project_name] # Scaffold a new project
fyr run # Run the pipeline
fyr run --max-parallel-io 8 # Override worker count for I/O fan-out
fyr bench # Run the pipeline and print per-phase timings
fyr bench --json # Same, but emit PipelineTimings as JSON on stdout
fyr --version # Show version
fyr --help # Show available commands
fyr bench reports wall-clock time per phase, per source, per identity graph,
and per analytics entity / metrics model (split into projection vs. write),
making it easy to spot where a pipeline spends its time.
Source loads and entity/metrics writes fan out across a bounded thread pool
(default 4 workers). Tune with the max_parallel_io key in fyrnheim.yaml
or the --max-parallel-io CLI flag on fyr run / fyr bench. Set to 1
for strictly serial behavior.
Why Fyrnheim?
| dbt | Fyrnheim | |
|---|---|---|
| Language | SQL + Jinja | Python |
| Type safety | Runtime errors | Pydantic validation at definition time |
| Local dev | Requires warehouse connection | DuckDB on local parquet files |
| Backend portability | Dialect-specific SQL | Ibis compiles to 15+ backends |
| Testing | Custom schema tests | pytest |
| Identity resolution | Manual SQL joins | Built-in identity graph |
Status
- Alpha -- API may change before 1.0
- DuckDB backend -- fully supported
- BigQuery backend -- supported
- ClickHouse output -- supported as output sink
- Postgres backend -- supported
- Python 3.11+ required
License
MIT
Release files for fyrnheim 0.15.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fyrnheim-0.15.1.tar.gz | 107.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fyrnheim-0.15.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 244.3 kB
Release files / fyrnheim-0.15.1.tar.gz
| Download URL | fyrnheim-0.15.1.tar.gz |
|---|---|
| Size | 107.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
be359e4faafb6be5b7c1d6b221c688dfe378d9e13b000bf9f1d1fcd2e9690a2c
|
|
BLAKE2b-256 checksum How to use checksums |
f16118ae0c668b96e4cb2f6c0de5c594dca3102721f5e81091b6026e2f44b69b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 23, 2026.
Transparency logRelease files / fyrnheim-0.15.1-py3-none-any.whl
| Download URL | fyrnheim-0.15.1-py3-none-any.whl |
|---|---|
| Size | 136.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f10c886ea2961bcf76766333b423dffefedb0f2ec5d325029a440ff375e75a37
|
|
BLAKE2b-256 checksum How to use checksums |
7c4c2265fbb53ebc5c190cbc0995dae0484ba7bd9813884c53b9e45f7b4e6b61
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 23, 2026.
Transparency log