Skip to content

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:

pip install opendataframework

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):

[sqlite]
path = "app.db"

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