from typing import Any, Dict, Generic, List, Optional, TypeVar from pydantic import BaseModel, ConfigDict, Field, model_validator def to_camel(s: str) -> str: parts = s.split("_") return parts[0] + "".join(p.title() for p in parts[1:]) class ApiModel(BaseModel): model_config = ConfigDict( alias_generator=to_camel, populate_by_name=True, ser_json_inf_nan="null", arbitrary_types_allowed=True, ) class BaseLinkedModel(ApiModel): @model_validator(mode="before") @classmethod def auto_populate_ids(cls, data: dict[str, Any]) -> dict[str, Any]: if isinstance(data, dict): for key, value in data.copy().items(): if not key.endswith("_id") and hasattr(value, "id") and value is not None: id_key = key + "_id" if id_key in data: # If the id_key already exists, ensure it matches the value's id if data[id_key] != value.id: raise ValueError( f"ID mismatch for {key}: {data[id_key]} != {value.id}" ) else: # If the id_key does not exist, set it to the value's id data[id_key] = value.id return data class ProblemDetails(ApiModel): type: str = Field(default="about:blank") title: str status: int detail: Optional[str] = None instance: Optional[str] = None errors: Optional[Dict[str, Any]] = None T = TypeVar("T") class Page(ApiModel, Generic[T]): items: List[T] next_cursor: Optional[str] = Field(default=None, alias="nextCursor") prev_cursor: Optional[str] = Field(default=None, alias="prevCursor") total: Optional[int] = Field(default=None, description="Optional total count")