Add temporal validity and lifecycle tracking to categories
This commit is contained in:
+112
-5
@@ -1,4 +1,6 @@
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -17,20 +19,125 @@ def upsert_category(
|
||||
) -> CategoryDb | None:
|
||||
if category is None:
|
||||
return None
|
||||
parameters = {
|
||||
"name": category.name,
|
||||
"parent": upsert_category(session, category.parent),
|
||||
parent = upsert_category(session, category.parent, do_commit=do_commit)
|
||||
parent_id = parent.id if parent else None
|
||||
matching_rows = session.query(CategoryDb).filter_by(
|
||||
name=category.name, parent_id=parent_id
|
||||
)
|
||||
active = matching_rows.filter(CategoryDb.valid_to.is_(None)).one_or_none()
|
||||
if active is not None:
|
||||
return active
|
||||
|
||||
parameters = {"name": category.name, "parent": parent}
|
||||
parameters_existing = {**parameters, "valid_to": None}
|
||||
parameters_new: dict[str, Any] = {
|
||||
**parameters,
|
||||
"valid_from": None,
|
||||
"valid_to": None,
|
||||
"last_action": "created",
|
||||
}
|
||||
historical = matching_rows.filter(CategoryDb.valid_to.is_not(None)).first()
|
||||
if historical is not None:
|
||||
parameters_new["valid_from"] = _utc_datetime(None)
|
||||
parameters_new["last_action"] = "changed"
|
||||
|
||||
return _upsert(
|
||||
session,
|
||||
CategoryDb,
|
||||
parameters,
|
||||
parameters,
|
||||
parameters_existing,
|
||||
parameters_new,
|
||||
do_update=False,
|
||||
do_commit=do_commit,
|
||||
)
|
||||
|
||||
|
||||
def _utc_datetime(value: datetime | None) -> datetime:
|
||||
if value is None:
|
||||
return datetime.now(UTC)
|
||||
if value.tzinfo is None or value.utcoffset() is None:
|
||||
return value.replace(tzinfo=UTC)
|
||||
return value.astimezone(UTC)
|
||||
|
||||
|
||||
def _ensure_after_valid_from(category: CategoryDb, effective_at: datetime) -> None:
|
||||
if category.valid_from is None:
|
||||
return
|
||||
valid_from = _utc_datetime(category.valid_from)
|
||||
if effective_at <= valid_from:
|
||||
raise ValueError("The action time must be later than valid_from")
|
||||
|
||||
|
||||
def change_category(
|
||||
session: Session,
|
||||
category_id: int,
|
||||
replacement: Category,
|
||||
effective_at: datetime | None = None,
|
||||
do_commit: bool = True,
|
||||
) -> CategoryDb:
|
||||
"""Close an active category version and start its replacement version."""
|
||||
current = (
|
||||
session.query(CategoryDb).filter_by(id=category_id, valid_to=None).one_or_none()
|
||||
)
|
||||
if current is None:
|
||||
raise ValueError(f"No active category found with id {category_id}")
|
||||
|
||||
action_time = _utc_datetime(effective_at)
|
||||
_ensure_after_valid_from(current, action_time)
|
||||
parent = upsert_category(session, replacement.parent, do_commit=False)
|
||||
parent_id = parent.id if parent else None
|
||||
|
||||
if current.name == replacement.name and current.parent_id == parent_id:
|
||||
return current
|
||||
|
||||
replacement_active = (
|
||||
session.query(CategoryDb)
|
||||
.filter_by(name=replacement.name, parent_id=parent_id, valid_to=None)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
current.valid_to = action_time
|
||||
current.last_action = "changed"
|
||||
session.flush()
|
||||
|
||||
if replacement_active is None:
|
||||
replacement_active = CategoryDb(
|
||||
name=replacement.name,
|
||||
parent=parent,
|
||||
valid_from=action_time,
|
||||
valid_to=None,
|
||||
last_action="changed",
|
||||
)
|
||||
session.add(replacement_active)
|
||||
session.flush()
|
||||
|
||||
if do_commit:
|
||||
session.commit()
|
||||
return replacement_active
|
||||
|
||||
|
||||
def retire_category(
|
||||
session: Session,
|
||||
category_id: int,
|
||||
effective_at: datetime | None = None,
|
||||
do_commit: bool = True,
|
||||
) -> CategoryDb:
|
||||
"""Close an active category version without creating a replacement."""
|
||||
category = (
|
||||
session.query(CategoryDb).filter_by(id=category_id, valid_to=None).one_or_none()
|
||||
)
|
||||
if category is None:
|
||||
raise ValueError(f"No active category found with id {category_id}")
|
||||
|
||||
action_time = _utc_datetime(effective_at)
|
||||
_ensure_after_valid_from(category, action_time)
|
||||
category.valid_to = action_time
|
||||
category.last_action = "retired"
|
||||
session.flush()
|
||||
if do_commit:
|
||||
session.commit()
|
||||
return category
|
||||
|
||||
|
||||
def upsert_b4w(
|
||||
session: Session, data: B4wExport, do_update=False, do_commit=True
|
||||
) -> B4wDb | None:
|
||||
|
||||
Reference in New Issue
Block a user