Object Graph Mapping¶
oold-python's core feature is IRI-transparent references: a field annotated with range can hold either a Python object or an IRI string. The library resolves IRIs on first access via the registered backend.
Direct object assignment¶
The simplest case - pass objects directly:
from oold.model import LinkedBaseModel
from pydantic import ConfigDict
from typing import List, Optional
class Tag(LinkedBaseModel):
model_config = ConfigDict(json_schema_extra={"$id": "https://example.com/Tag"})
id: str
name: str
class Article(LinkedBaseModel):
model_config = ConfigDict(json_schema_extra={"$id": "https://example.com/Article"})
id: str
title: str
primary_tag: Optional[Tag] = None
tags: List[Tag] = []
python_tag = Tag(id="ex:tag-python", name="python")
async_tag = Tag(id="ex:tag-async", name="async")
article = Article(
id="ex:article-1",
title="Async Python Tips",
primary_tag=python_tag,
tags=[python_tag, async_tag],
)
print(article.primary_tag.name) # python
print(article.tags[1].name) # async
IRI string assignment and lazy resolution¶
Assign IRI strings instead of objects. The backend resolves them on first access.
from oold.backend.document_store import SimpleDictDocumentStore
from oold.backend.interface import SetResolverParam, StoreParam, set_resolver
store = SimpleDictDocumentStore()
set_resolver(SetResolverParam(iri="ex", resolver=store))
# Pre-populate the backend
store.store(StoreParam(nodes={
"ex:tag-python": python_tag,
"ex:tag-async": async_tag,
}))
# Assign IRIs - objects are NOT loaded yet
article2 = Article(
id="ex:article-2",
title="Another Post",
primary_tag="ex:tag-python", # IRI string
tags=["ex:tag-python", "ex:tag-async"],
)
# First access triggers backend resolution
print(article2.primary_tag.name) # python - loaded from store on demand
print(article2.tags[0].name) # python
!!! tip Lazy resolution keeps startup fast: only the entities you actually access are fetched from the backend.
Resolving by IRI directly¶
Use the class-level [] operator as a shorthand:
tag = Tag["ex:tag-python"] # equivalent to store.resolve(...)
print(tag.name) # python
Mixing objects and IRIs¶
You can mix concrete objects and IRIs in a list:
article3 = Article(
id="ex:article-3",
title="Mixed References",
tags=[python_tag, "ex:tag-async"], # one object, one IRI
)
cast() - converting between model classes¶
cast() converts an instance from one model class to another while preserving __iris__ references.
from oold.model import LinkedBaseModel
class ArticleV1(LinkedBaseModel):
id: str
title: str
body: str = ""
class ArticleV2(LinkedBaseModel):
id: str
title: str
content: str = "" # renamed field
v1 = ArticleV1(id="ex:a1", title="Hello", body="Some text")
# Cast to V2; fields not on V2 are dropped, None fields use V2 defaults
v2 = v1.cast(ArticleV2, remove_extra=True, none_to_default=True)
print(v2.id) # ex:a1
print(v2.title) # Hello
print(v2.content) # "" - default, since 'body' doesn't exist on V2
cast() parameters:
| Parameter | Effect |
|---|---|
remove_extra=True |
Drop fields not defined on the target class |
none_to_default=True |
Replace None / empty-list values with the target's defaults |
silent=True |
Suppress warnings about dropped fields (default) |
You can also construct a target instance directly from another model:
v2 = ArticleV2(v1, content="migrated")
Default IRI values¶
A field can carry a default IRI that is resolved automatically on instantiation:
# Schema definition
{
"b_default": {"type": "string", "range": "Tag.json", "default": "ex:tag-python"}
}
When you instantiate the model without supplying b_default, the IRI "ex:tag-python" is used and resolved on first access.