first
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""Repository base exceptions + shared helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Mapping
|
||||
from uuid import UUID
|
||||
|
||||
from motor.motor_asyncio import AsyncIOMotorCollection
|
||||
from pydantic import BaseModel
|
||||
|
||||
from asf_domain.base import generate_id
|
||||
|
||||
|
||||
class ASFRepositoryError(Exception):
|
||||
"""Base for all repository-layer errors."""
|
||||
|
||||
|
||||
class NotFoundError(ASFRepositoryError):
|
||||
def __init__(self, kind: str, entity_id: UUID | str) -> None:
|
||||
super().__init__(f"{kind} not found: {entity_id}")
|
||||
self.kind = kind
|
||||
self.entity_id = entity_id
|
||||
|
||||
|
||||
class DuplicateError(ASFRepositoryError):
|
||||
def __init__(self, kind: str, key: str) -> None:
|
||||
super().__init__(f"{kind} already exists: {key}")
|
||||
self.kind = kind
|
||||
self.key = key
|
||||
|
||||
|
||||
class TenantMismatchError(ASFRepositoryError):
|
||||
"""Raised when an operation targets an entity owned by another tenant."""
|
||||
|
||||
def __init__(self, kind: str, entity_id: UUID | str) -> None:
|
||||
super().__init__(f"Tenant isolation violation: {kind} {entity_id}")
|
||||
|
||||
|
||||
def _now_utc() -> datetime:
|
||||
return datetime.now(tz=timezone.utc)
|
||||
|
||||
|
||||
def _to_doc(m: BaseModel) -> dict[str, Any]:
|
||||
return m.model_dump(mode="python", by_alias=False)
|
||||
|
||||
|
||||
def _from_doc(cls, doc: Mapping[str, Any]):
|
||||
"""Instantiate a Pydantic model from a Mongo document.
|
||||
|
||||
``_id`` → ``id`` rename handled explicitly. Motor already returns UUID
|
||||
objects for UUID fields when uuidRepresentation=standard is set.
|
||||
|
||||
Unknown document fields are filtered out (matches ``extra = forbid``
|
||||
used by most ASF domain models while still tolerating new audit-only
|
||||
Mongo columns like ``updated_at`` / ``metadata``).
|
||||
"""
|
||||
if doc is None:
|
||||
return None
|
||||
d = dict(doc)
|
||||
if "_id" in d and "id" not in d:
|
||||
d["id"] = d.pop("_id")
|
||||
allowed = set(cls.model_fields.keys())
|
||||
known = {k: v for k, v in d.items() if k in allowed}
|
||||
try:
|
||||
return cls.model_validate(known)
|
||||
except Exception:
|
||||
for k in list(known.keys()):
|
||||
f = cls.model_fields.get(k)
|
||||
if f is None:
|
||||
continue
|
||||
annotation = getattr(f, "annotation", None)
|
||||
# Wrap None for required fields with usable defaults — job usage, etc.
|
||||
if known[k] is None and annotation is not None:
|
||||
from typing import get_origin, get_args
|
||||
origin = get_origin(annotation)
|
||||
args = get_args(annotation)
|
||||
# Default dict for JobUsage
|
||||
name = ""
|
||||
if hasattr(annotation, "__name__"):
|
||||
name = annotation.__name__
|
||||
for arg in args:
|
||||
if hasattr(arg, "__name__"):
|
||||
name = arg.__name__
|
||||
break
|
||||
if name == "JobUsage":
|
||||
known[k] = {"extra": {}}
|
||||
return cls.model_validate(known)
|
||||
Reference in New Issue
Block a user