Skip to main content

Open Data Framework

PyPI Python License: MIT Docs

Lightweight dependency-injection framework for data applications.

This package provides structural abstractions — Entity, Repository, Component, Service, Task, Pipeline, Layer, Namespace, Context, Project, Config, Logger, View — with zero third-party dependencies. No UI, no CLI, no MCP server.

Need the CLI, UI, or MCP server? That's the odf package, which depends on this one.

Full documentation: opendataframework.github.io/opendataframework


Why

Most data projects solve 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 needs three things: an entity and repository, a config file, and a main file that starts the Project.

Application code (app.py):

import sqlite3
from dataclasses import dataclass

from opendataframework import Component, Config, Entity, Repository


@Entity
@dataclass
class User:
    id: int
    name: str


@Component
class SQLite:
    def __init__(self, config: Config):
        self.conn = sqlite3.connect(config.sqlite.path)
        self.conn.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")


@Repository(User)
class Users:
    def __init__(self, sqlite: SQLite):
        self.db = sqlite.conn

    def all(self) -> list[User]:
        rows = self.db.execute("SELECT id, name FROM users").fetchall()
        return [User(id=row[0], name=row[1]) for row in rows]

    def save(self, user: User) -> None:
        self.db.execute("INSERT OR REPLACE INTO users VALUES (?, ?)", (user.id, user.name))
        self.db.commit()

Config file (config.toml):

[sqlite]
path = "app.db"

Main file (main.py):

from opendataframework import Project

from app import User, Users

project = Project.from_config("config.toml")
project.start()

users = project.context.get(Users)
users.save(User(id=1, 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.


Core Concepts

Entity

A structured unit of data the system works with.

@Entity
@dataclass
class User:
    id: int
    name: str

Repository

Manages Entities; abstracts persistence away from business logic.

@Repository(User)
class Users:
    def get(self, user_id: int) -> User: ...
    def save(self, user: User) -> None: ...

View

An optional, single declaration a Repository attaches to itself: which representation (table, map, image, video, ...) and field(s) best fit its data. Metadata only — this package renders nothing itself. See View.

@Repository(Store)
class Stores:
    def all(self) -> list[Store]: ...

    def data_view(self) -> LocationView:
        return LocationView(fields=("lat", "lon"))

Component

A Context-managed object with no required execution contract — wired in and given its dependencies through the constructor.

@Component
class Classifier:
    def fit(self, data): ...
    def predict(self, data): ...

Service

A long-running executable, wired through DI the same way as a Component but with a mandatory setup → run → stop lifecycle — an API server, worker process, or scheduler. run() blocks by nature; the framework backgrounds it automatically.

@Service
class Postgres:
    def setup(self): ...
    def run(self): ...
    def stop(self): ...

Task

A finite executable, wired through DI the same way as a Component — performs one bounded unit of work, once.

@Task
class MetricsFetcher:
    def execute(self): ...

Pipeline

Coordinates multiple Tasks (and optionally other Pipelines) into an ordered workflow.

@Pipeline
class DailyAnalytics:
    def execute(self): ...

Layer

An organisational grouping — which subsystem a component belongs to (Api, Storage, Analytics, Messaging, Monitoring, Security, or custom). Independent of execution type.

@Storage
@Service
class Postgres: ...

Namespace

The base class underlying every framework decorator — Entity, Component, Service, Task, Pipeline, Repository, and Layer are all Namespace subclasses, each with its own independent name → class mapping. A framework extension point, not something end users subclass directly. See Namespace.

Config

Typed, dot-accessible view over a configuration dict — snake_case and kebab-case are interchangeable, and nested sections chain naturally. Resolved automatically into the container by Project.from_config.

@Storage
@Service
class Postgres:
    def __init__(self, config: Config):
        self.host = config.postgres.host

Logger

Per-component logging handle, injectable via constructor DI like any other dependency — writes land in that component's own log file with no name argument needed.

@Task
class ExportUsers:
    def __init__(self, users: Users, logger: Logger):
        self.users = users
        self.logger = logger

    def execute(self) -> None:
        self.logger.info("export starting")

Context

Registers, resolves, and lifecycles every component in dependency order. No circular dependencies — restructure with a shared third collaborator instead.

component = project.context.get(UsersApi)  # typed, by class

Project

The composition root — owns Context and configuration, and is the single entry point for starting and stopping the application.

project = Project.from_config("config.toml")
project.start()

Multiple independent Project instances are supported in the same process; there is no global mutable state.


Design Guidelines

  • Prefer composition — decorators (@Api @Service), not inheritance from framework base classes.
  • Keep responsibilities focused — one primary reason to change per class.
  • Avoid global mutable state — framework state belongs to objects.
  • Avoid hidden magic — dependencies must be visible in constructor signatures.

Mental Model

Data is represented by Entities.
Repositories manage data.

Components provide capabilities.
Services run for extended periods.
Tasks execute bounded work.
Pipelines coordinate tasks (and other pipelines).

Layers organize components.
Context manages components and repositories.
Project owns everything.

Examples

See examples/ — small, focused projects each isolating one core abstraction.

For the CLI, UI, MCP server, and chat surface built on top of this package, see odf.


Development

Requires Python >=3.14, managed with Poetry.

poetry install       # install dependencies, including dev
poetry run pytest    # run the test suite

See CONTRIBUTING.md for how to propose changes.


License

MIT

Metadata

Release files for opendataframework 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for opendataframework 0.2.0
File Size Uploaded Provenance Actions
opendataframework-0.2.0.tar.gz 32.2 kB Without provenance Details for opendataframework-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for opendataframework 0.2.0
File Interpreter ABI Platform Actions
opendataframework-0.2.0-py3-none-any.whl Python 3 none any Details for opendataframework-0.2.0-py3-none-any.whl

Total release size: 69.6 kB

Release files / opendataframework-0.2.0.tar.gz

Download URL opendataframework-0.2.0.tar.gz
Size 32.2 kB
Tags Source
SHA-256 checksum
How to use checksums
061090284fd11fb3fe35be1a6e3126ea520b2ebc74ac72301f32af303084662d
BLAKE2b-256 checksum
How to use checksums
360ef3d84ce08154c6d2d26e146f6f85f6f3fc3e69de3c8d394b54e20e3914a5
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.4.1 CPython/3.13.1 Darwin/24.3.0

Release files / opendataframework-0.2.0-py3-none-any.whl

Download URL opendataframework-0.2.0-py3-none-any.whl
Size 37.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
354c4caa3c6f11e480c87185662f08f6b7f05d77a766ce2cb66e806c68d45f9d
BLAKE2b-256 checksum
How to use checksums
c5888eda9d052b6c10263427f7f2c3e6d5d462d3b3aac27cf3329ef1099fc5d4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/2.4.1 CPython/3.13.1 Darwin/24.3.0

Security for opendataframework 0.2.0

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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page