DuckDB-backed OCEL 2.0 storage with typed queries and in-memory loading
Project description
oceldb
DuckDB-backed dataframe layer for OCEL 2.0.
oceldb exposes object-centric event logs through a typed, lazy dataframe API
backed by DuckDB. Ibis is used internally, but queries are built through
oceldb's typed expression wrappers. Logs can be stored as typed Parquet files
or loaded into ephemeral in-memory storage. Nothing is materialised into
Python objects until you explicitly call .execute().
Installation
pip install oceldb
# or
uv add oceldb
Requires Python 3.12+.
Quick start
from oceldb import OCEL, col, desc
from oceldb.io import convert_ocel
# Convert an OCEL 2.0 JSON, XML, or SQLite file to the oceldb Parquet layout
convert_ocel("running-example.jsonocel", "running-example", overwrite=True)
ocel = OCEL.read("running-example")
# Event counts by type
event_counts = (
ocel.events()
.group_by("ocel_type")
.aggregate(n=col("ocel_id").count())
.order_by(desc("n"))
.execute()
)
# Latest state of each order
latest_states = ocel.object_states("order").latest().execute()
# Filter objects using a predicate
from oceldb.predicates import participated_in
paid_orders = ocel.objects("order").filter(participated_in(ocel, "Pay Order"))
print(paid_orders.count().execute())
For applications that do not need persistent oceldb output, load the source directly into temporary DuckDB storage:
with OCEL.load("running-example.jsonocel") as ocel:
latest_states = ocel.object_states("order").latest().execute()
Core concepts
OCEL 2.0 extends flat event logs with multiple interrelated object types. An order-fulfilment process, for example, involves orders, items, packages, and payments — each with their own lifecycle. oceldb stores this structure natively.
The OCEL class
OCEL.read(path) opens a persisted Parquet log directory. OCEL.load(source)
imports JSON, XML, SQLite, or a registered custom source into in-memory
DuckDB tables without writing an oceldb directory. Each returns an OCEL
instance backed by lazy oceldb.Table expressions.
ocel = OCEL.read("my-log")
# or:
ocel = OCEL.load("source.jsonocel")
ocel.events() # all events: ocel_id, ocel_type, ocel_time, …attrs
ocel.events("Place Order") # filtered to one type
ocel.events("A", "B") # filtered to multiple types
ocel.objects() # all objects: ocel_id, ocel_type
ocel.objects("order") # filtered to one type
ocel.object_changes("order") # sparse history rows for object type
ocel.object_states("order") # fill-forward state view (see below)
ocel.flatten("order") # case-centric event log with orders as cases
ocel.event_object # E2O bridge: event_id, event_type, object_id, object_type, qualifier
ocel.object_object # O2O bridge: source_id, source_type, target_id, target_type, qualifier
ocel.manifest # parsed manifest.json (counts, types, time range)
Object states
Attributes in OCEL 2.0 are stored as change rows. object_states reconstructs
point-in-time snapshots via fill-forward:
from datetime import datetime
states = ocel.object_states("order")
states.history() # all change rows in chronological order
states.latest() # most recent state per object
states.as_of(datetime(2024, 6, 1)) # state at a specific timestamp
Flattening
Flatten an OCEL to a traditional case-centric event log by choosing one object type as the case notion:
flat_orders = ocel.flatten("order").execute()
The flattened table follows the PM4Py/XES naming convention:
case:concept:name: object id of the selected object typeconcept:name: event activity, fromocel_typetime:timestamp: event timestamp, fromocel_timeocel_event_id: original OCEL event id- event attributes are preserved as additional columns
Typed expressions
All accessors return oceldb.Table expressions. Use the wrappers exported by
oceldb for expression construction; do not compose queries with Ibis
objects directly. The wrappers keep the supported expression surface typed
despite Ibis' dynamic typing internals.
from oceldb import col, desc, row_number
# Latest object states with a specific attribute value
heavy = ocel.object_states("Container").latest().filter(
col("Weight") > 500
)
# Event timeline per flattened case with lag/lead
flat = ocel.flatten("order")
timeline = flat.mutate(
seq=row_number().over(
group_by="case:concept:name", order_by=["time:timestamp", "ocel_event_id"]
),
previous=col("concept:name").lag().over(
group_by="case:concept:name", order_by=["time:timestamp", "ocel_event_id"]
),
next=col("concept:name").lead().over(
group_by="case:concept:name", order_by=["time:timestamp", "ocel_event_id"]
),
).order_by("case:concept:name", "time:timestamp").execute()
# Aggregation and ordering
counts = (
ocel.events()
.group_by("ocel_type")
.aggregate(n=col("ocel_id").count())
.order_by(desc("n"))
.execute()
)
The typed surface includes Table.filter, select, mutate, drop,
rename, distinct, limit, order_by, group_by, join, execute,
and to_pyarrow; column comparisons and aggregations; and the col, asc,
desc, row_number, and union helpers.
Predicates
oceldb.predicates provides free-function predicates for OCEL-specific filter
expressions. All return deferred boolean expressions for use with
oceldb.Table.filter().
Object predicates
from oceldb.predicates import (
participated_in, # object participated in ≥1 event of a given type
event_count, # count of events of a given type per object
cooccurrence_count, # count of co-occurring objects of a given type
o2o_count, # count of O2O-linked objects of a given type
o2o_reachable, # reachability via O2O relations
)
# Orders that were paid
paid = ocel.objects("order").filter(participated_in(ocel, "Pay Order"))
# Orders with more than 3 items co-occurring
busy = ocel.objects("order").filter(cooccurrence_count(ocel, "item") >= 3)
# Objects with exactly two linked children
ocel.objects("order").filter(o2o_count(ocel, "item") == 2)
# Objects linked (directly or transitively) to a Transport Document
ocel.objects("Container").filter(o2o_reachable(ocel, "Transport Document"))
Event predicates
from oceldb.predicates import (
involves, # event involves ≥1 object of a given type
has_matching_predecessor, # batch-synchronisation check
)
# Events that involve at least one item
ocel.events().filter(involves(ocel, "item"))
# Sync events that have a matching preceding Group event
matched = ocel.events("Sync").filter(
has_matching_predecessor(ocel, "Group", "member")
)
Count predicates
event_count, cooccurrence_count, o2o_count return a CountPredicate
object that supports threshold comparisons:
event_count(ocel, "Pay Order") >= 1 # participated at least once
event_count(ocel, "Pay Order") == 1 # participated exactly once
event_count(ocel, "Pay Order") == 0 # never participated
Inspection
oceldb.inspect provides structural summaries read directly from the manifest
(no SQL):
from oceldb.inspect import overview, event_types, object_types
print(overview(ocel))
# Events: 35,413 (14 types)
# Objects: 13,910 (7 types)
# E2O relations: 74,334
# O2O relations: 15,953
# Time range: 2023-05-22 11:54:42 → 2024-08-22 12:18:41
for et in event_types(ocel): # sorted by count descending
print(et.name, et.count, et.attributes)
for ot in object_types(ocel): # sorted by count descending
print(ot.name, ot.object_count, ot.attributes)
Import
Convert an OCEL 2.0 exchange format to the oceldb Parquet layout:
from oceldb.io import convert_ocel
convert_ocel("source.jsonocel", "target/", overwrite=True) # JSON
convert_ocel("source.xml", "target/", overwrite=True) # XML
convert_ocel("source.sqlite", "target/", overwrite=True) # SQLite
The format is inferred from common OCEL file extensions (.json, .jsonocel,
.xml, .xmlocel, .db, .sqlite, .sqlite3). Pass format="json" /
"xml" / "sqlite" when the filename does not expose the format.
Custom sources can be registered through the shared converter registry:
from oceldb import OCEL
from oceldb.io import ConverterSpec, register
register(ConverterSpec(format="myformat", source_factory=MySource))
convert_ocel(source, target, format="myformat")
# or:
ocel = OCEL.load(source, format="myformat")
Storage layout
my-log/
manifest.json # schema, provenance, totals
events/
ocel_type=<url-encoded-type>/
data.parquet # ocel_id, ocel_time, …attrs; sorted by ocel_time
objects/
ocel_type=<url-encoded-type>/
data.parquet # ocel_id
object_changes/
ocel_type=<url-encoded-type>/
data.parquet # ocel_id, ocel_time, ocel_changed_field, …attrs
event_object.parquet # E2O bridge with denormalised type columns
object_object.parquet # O2O bridge with denormalised type columns (if present)
Type names with spaces or special characters are URL-encoded in directory names
(e.g. ocel_type=Place%20Order). DuckDB uses Hive partition pruning to skip
files when filtering by type.
Type checking
Ibis' expression types are dynamic and produce pervasive errors in strict
pyright/basedpyright projects. For that reason, Ibis is an implementation
detail: public query methods return oceldb.Table, and supported query
operations are exposed through the wrappers in oceldb.expr.
from oceldb import OCEL, col, desc
with OCEL.load("source.jsonocel") as ocel:
result = (
ocel.events("Place Order")
.filter(col("amount") > 100)
.order_by(desc("ocel_time"))
.execute()
)
Keep expressions within this typed API. Importing Ibis expressions directly or operating on the underlying raw backend bypasses the wrapper layer and brings back the typing problems it is intended to isolate.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file oceldb-0.6.0.tar.gz.
File metadata
- Download URL: oceldb-0.6.0.tar.gz
- Upload date:
- Size: 31.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4fdd239c9cb658fca312d2cd5b5fda4b4ee558b63a0f1154e3722c0253d4db8c
|
|
| MD5 |
1aa0c30116736266a317b700ac76dca7
|
|
| BLAKE2b-256 |
9035f43b3c07210add6ff6e4f6443502648319edfccd3652047464acd11a832e
|
Provenance
The following attestation bundles were made for oceldb-0.6.0.tar.gz:
Publisher:
publish.yml on sebmne/oceldb
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
oceldb-0.6.0.tar.gz -
Subject digest:
4fdd239c9cb658fca312d2cd5b5fda4b4ee558b63a0f1154e3722c0253d4db8c - Sigstore transparency entry: 1693506204
- Sigstore integration time:
-
Permalink:
sebmne/oceldb@0d3a51ed430da7eb8ac953596ff49a6874f23f71 -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/sebmne
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0d3a51ed430da7eb8ac953596ff49a6874f23f71 -
Trigger Event:
release
-
Statement type:
File details
Details for the file oceldb-0.6.0-py3-none-any.whl.
File metadata
- Download URL: oceldb-0.6.0-py3-none-any.whl
- Upload date:
- Size: 38.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5daac74c671a1770d36c7b6a5a7bd7d8d44c8df5894fe180b406e71b9133739f
|
|
| MD5 |
1b22bb033983c366bfbc685444602741
|
|
| BLAKE2b-256 |
1500ba145adbcebca9dcec5ac77487cc4f8913e971150b3c5716234080a9f53f
|
Provenance
The following attestation bundles were made for oceldb-0.6.0-py3-none-any.whl:
Publisher:
publish.yml on sebmne/oceldb
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
oceldb-0.6.0-py3-none-any.whl -
Subject digest:
5daac74c671a1770d36c7b6a5a7bd7d8d44c8df5894fe180b406e71b9133739f - Sigstore transparency entry: 1693506332
- Sigstore integration time:
-
Permalink:
sebmne/oceldb@0d3a51ed430da7eb8ac953596ff49a6874f23f71 -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/sebmne
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0d3a51ed430da7eb8ac953596ff49a6874f23f71 -
Trigger Event:
release
-
Statement type: