Session¶
The session API provides a small async unit-of-work wrapper around an Ormdantic database instance.
Use async with db.session() for commit-on-success and rollback-on-error flows. The session tracks pending inserts, dirty loaded models, deleted models, relationship additions, and an identity map for repeated loads.
Session.savepoint(name=None) opens a nested savepoint. If the block raises, Ormdantic rolls back to the database savepoint and restores the session's pending, dirty, deleted, identity, and snapshot state.
If flush() fails, the session restores the pre-flush unit-of-work state and requires rollback() before more work is accepted.
ormdantic.session.Session
¶
Session(database, *, transaction_options=None)
Minimal async unit-of-work session for Ormdantic models.
Create a session bound to an Ormdantic database instance.
Source code in ormdantic/session.py
def __init__(
self, database: Any, *, transaction_options: Any | None = None
) -> None:
"""Create a session bound to an `Ormdantic` database instance."""
self._database = database
self._transaction_options = transaction_options
self._new: list[BaseModel] = []
self._dirty: list[BaseModel] = []
self._deleted: list[BaseModel] = []
self._identity_map: dict[tuple[type[BaseModel], Any], BaseModel] = {}
self._snapshots: dict[tuple[type[BaseModel], Any], dict[str, Any]] = {}
self._failed_flush_error: Exception | None = None
self._savepoint_sequence = 0
self._closed = False
add
¶
add(model)
Stage a new model for insertion on flush.
Source code in ormdantic/session.py
def add(self, model: BaseModel) -> None:
"""Stage a new model for insertion on flush."""
self._ensure_usable()
self._cascade_add(model, set())
mark_dirty
¶
mark_dirty(model)
Stage an existing model for update on flush.
Source code in ormdantic/session.py
def mark_dirty(self, model: BaseModel) -> None:
"""Stage an existing model for update on flush."""
self._ensure_usable()
if model not in self._new and model not in self._dirty:
self._dirty.append(model)
delete
¶
delete(model)
Stage an existing model for deletion on flush.
Source code in ormdantic/session.py
def delete(self, model: BaseModel) -> None:
"""Stage an existing model for deletion on flush."""
self._ensure_usable()
self._cascade_delete(model, set())
merge
¶
merge(model)
Merge a detached model into the identity map and stage it as dirty.
Source code in ormdantic/session.py
def merge(self, model: BaseModel) -> BaseModel:
"""Merge a detached model into the identity map and stage it as dirty."""
self._ensure_usable()
table = self._database._table_map.model_to_data[type(model)]
key = (type(model), getattr(model, table.pk))
if cached := self._identity_map.get(key):
for field, value in model.__dict__.items():
setattr(cached, field, value)
self.mark_dirty(cached)
return cached
if staged := self._staged_model_for_key(key):
for field, value in model.__dict__.items():
setattr(staged, field, value)
if staged not in self._new:
self.mark_dirty(staged)
return staged
self._remember(model)
self.mark_dirty(model)
return model
expire
¶
expire(model)
Remove a model from the identity map.
Source code in ormdantic/session.py
def expire(self, model: BaseModel) -> None:
"""Remove a model from the identity map."""
self._ensure_usable()
key = self._identity_key(model)
self._identity_map.pop(key, None)
self._snapshots.pop(key, None)
flush
async
¶
flush()
Write staged inserts and updates without ending the transaction.
Source code in ormdantic/session.py
async def flush(self) -> None:
"""Write staged inserts and updates without ending the transaction."""
self._ensure_usable()
state = self._capture_state()
try:
await self._database._events.dispatch("before_flush", session=self)
self._detect_relationship_changes()
for model in self._detect_dirty_models():
self.mark_dirty(model)
inserted = self._dependency_ordered(list(self._new))
for batch in self._model_batches(inserted):
stored_models = await self._database[type(batch[0])].insert_many(batch)
for stored in stored_models:
self._remember(stored)
self._new.clear()
updated = list(self._dirty)
for batch in self._model_batches(updated):
for model in batch:
stored = await self._database[type(model)].update(model)
self._remember(stored)
self._dirty.clear()
deleted = list(reversed(self._dependency_ordered(list(self._deleted))))
for batch in self._model_batches(deleted):
for model in batch:
key = self._identity_key(model)
pk = key[1]
await self._database[type(model)].delete(pk)
self._identity_map.pop(key, None)
self._snapshots.pop(key, None)
self._deleted.clear()
except Exception as exc:
self._restore_state(state)
self._failed_flush_error = exc
raise
await self._database._events.dispatch("after_flush", session=self)
commit
async
¶
commit()
Flush changes and commit the active transaction.
Source code in ormdantic/session.py
async def commit(self) -> None:
"""Flush changes and commit the active transaction."""
if self._closed:
return
await self.flush()
await self._database._commit()
self._closed = True
rollback
async
¶
rollback()
Discard staged changes and roll back the active transaction.
Source code in ormdantic/session.py
async def rollback(self) -> None:
"""Discard staged changes and roll back the active transaction."""
if self._closed:
return
try:
await self._database._rollback()
finally:
self._new.clear()
self._dirty.clear()
self._deleted.clear()
self._identity_map.clear()
self._snapshots.clear()
self._failed_flush_error = None
self._closed = True
savepoint
¶
savepoint(name=None)
Open a nested session savepoint that restores unit-of-work state.
Source code in ormdantic/session.py
def savepoint(self, name: str | None = None) -> Any:
"""Open a nested session savepoint that restores unit-of-work state."""
self._ensure_usable()
if name is None:
self._savepoint_sequence += 1
name = f"session_sp_{self._savepoint_sequence}"
return _SessionSavepoint(self, name)
refresh
async
¶
refresh(model, *, depth=0)
Reload a model by primary key and remember the refreshed instance.
Source code in ormdantic/session.py
async def refresh(self, model: BaseModel, *, depth: int = 0) -> BaseModel | None:
"""Reload a model by primary key and remember the refreshed instance."""
self._ensure_usable()
table = self._database._table_map.model_to_data[type(model)]
refreshed = await self._database[type(model)].find_one(
getattr(model, table.pk), depth=depth
)
if refreshed is not None:
self._remember(refreshed)
return refreshed
get_cached
¶
get_cached(model_type, pk)
Return a model from the identity map if it has been remembered.
Source code in ormdantic/session.py
def get_cached(self, model_type: type[BaseModel], pk: Any) -> BaseModel | None:
"""Return a model from the identity map if it has been remembered."""
return self._identity_map.get((model_type, pk))
get
async
¶
get(model_type, pk, *, depth=0)
Return a cached model or load it by primary key.
Source code in ormdantic/session.py
async def get(
self, model_type: type[BaseModel], pk: Any, *, depth: int = 0
) -> BaseModel | None:
"""Return a cached model or load it by primary key."""
self._ensure_usable()
if cached := self.get_cached(model_type, pk):
return cached
loaded = await self._database[model_type].find_one(pk, depth=depth)
if loaded is not None:
self._remember(loaded)
return loaded