Open Data Framework¶
Lightweight dependency-injection framework for data applications.
Looking for the CLI, UI, MCP server, or chat?
Those live in the sibling odf
package, which depends on this one. This site covers the core
framework only — no UI, no CLI, no MCP server.
Why¶
Most data projects end up solving the same structural problems repeatedly: how to wire
dependencies, manage component lifecycles, and keep configuration separate from code.
This framework provides that plumbing as a small set of reusable abstractions —
Entity, Repository, Component, Service, Task, Pipeline — so it doesn't need
to be reinvented by hand in every project.
Want a ready-made project layout instead?
Scaffolding an actual project from a template is the
odf package's job — this is what
that scaffold is built on.
Getting Started¶
Install:
A minimal project is a config file plus an app/ package:
.
├── config.toml
├── main.py
└── app/
├── __init__.py
├── entities.py
├── repositories.py
└── storages.py
app/entities.py — the data unit:
from dataclasses import dataclass
from opendataframework import Entity
@Entity
@dataclass
class User:
id: int | None
name: str
app/storages.py — a Component wired from config, doing the actual I/O:
import sqlite3
from opendataframework import Component, Config
@Component
class SQLite:
def __init__(self, config: Config) -> None:
self.conn = sqlite3.connect(config.get("sqlite").get("path"))
self.conn.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
app/repositories.py — the Repository, depending on the storage:
from opendataframework import Repository
from app.entities import User
from app.storages import SQLite
@Repository(User)
class Users:
def __init__(self, sqlite: SQLite) -> None:
self.db = sqlite
def all(self) -> list[User]:
rows = self.db.conn.execute("SELECT id, name FROM users").fetchall()
return [User(id=r[0], name=r[1]) for r in rows]
def save(self, user: User) -> None:
self.db.conn.execute("INSERT INTO users (id, name) VALUES (?, ?)", (user.id, user.name))
self.db.conn.commit()
Config file (config.toml):
Main file (main.py):
from opendataframework import Project
from app.entities import User
from app.repositories import Users
project = Project.from_config("config.toml")
project.start()
users = project.context.get(Users)
users.save(User(id=None, name="Ada"))
print(users.all())
project.start() blocks until every component has completed its initialisation stage —
there is no hidden latency on first access. Adding a new component means declaring its
dependencies in the constructor, not writing wiring code — a config section is only
needed if the component reads from Config itself.
Full lifecycle and usage patterns
See Project.
More worked, runnable projects
Each isolating exactly one core abstraction — see Examples.
Core Concepts¶
Each concept below has a dedicated doc with the full interface, decorator usage, and worked examples.
| Abstraction | Role |
|---|---|
| Entity | Structured data unit (dataclass / ORM model) |
| Repository | Data access; manages Entities |
| View | Declares which representation and field(s) best fit a Repository's data |
| Component | DI-managed object with no execution contract |
| Service | Long-running Component (worker, backing service) |
| Task | Finite work unit; called explicitly |
| Pipeline | Ordered composition of Tasks/Pipelines |
| Layer | Decorator that assigns a Component to a subsystem |
Supporting the above are Context, Config, Project, Namespace, and Logger — the machinery that registers, resolves, and organises everything.
| Machinery | Role |
|---|---|
| Context | Registers, resolves, and lifecycles every component, in dependency order |
| Config | Wraps loaded TOML into the nested object components read via DI |
| Project | Composition root; owns the Context and starts/stops the application |
| Namespace | Base class behind every decorator (@Component, @Service, …); not an end-user concept |
| Logger | Per-component logging handle, injected already bound to its own class name |