from __future__ import annotations
from collections.abc import Iterator
from typing import TYPE_CHECKING, Any, cast
from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator
from ._compat import Self, dataclass_transform
from .errors import humanise_validation_error
if TYPE_CHECKING:
# Imported under TYPE_CHECKING only so that type checkers keep treating
# subclasses of BaseCZMLObject as pydantic models; at runtime the metaclass
# is taken from BaseModel itself to avoid depending on a private import path.
from pydantic._internal._model_construction import ModelMetaclass
else:
ModelMetaclass = type(BaseModel)
NON_DELETE_PROPERTIES = ["id", "delete"]
@dataclass_transform(kw_only_default=True, field_specifiers=(Field,))
class _HumanErrorsMeta(ModelMetaclass):
"""Rewrites the error raised by ``Model(...)`` into something readable.
The rewrite is hooked in here rather than on ``__init__`` deliberately: a
model that defines its own ``__init__`` opts out of pydantic's native
construction path, which both slows down nested validation and discards the
union information that :func:`~czml3.errors.humanise_validation_error` needs.
"""
def __call__(cls, *args: Any, **kwargs: Any) -> Any:
try:
return super().__call__(*args, **kwargs)
except ValidationError as error:
raise humanise_validation_error(error, cast(type[BaseModel], cls)) from None
[docs]
class BaseCZMLObject(BaseModel, metaclass=_HumanErrorsMeta):
model_config = ConfigDict(extra="forbid")
[docs]
@classmethod
def model_validate(cls, *args: Any, **kwargs: Any) -> Self:
"""Validate an object against this model.
args and kwargs are passed to `BaseModel.model_validate()`.
:return: The validated model instance
:raises pydantic.ValidationError: If the object is invalid, with a
human-readable message
"""
try:
return super().model_validate(*args, **kwargs)
except ValidationError as error:
raise humanise_validation_error(error, cls) from None
[docs]
@classmethod
def model_validate_json(cls, *args: Any, **kwargs: Any) -> Self:
"""Validate a JSON string against this model.
args and kwargs are passed to `BaseModel.model_validate_json()`.
:return: The validated model instance
:raises pydantic.ValidationError: If the JSON is invalid, with a
human-readable message
"""
try:
return super().model_validate_json(*args, **kwargs)
except ValidationError as error:
raise humanise_validation_error(error, cls) from None
[docs]
@model_validator(mode="after")
def check_delete(self) -> Self:
if hasattr(self, "delete") and self.delete:
for k in type(self).model_fields:
if k not in NON_DELETE_PROPERTIES and getattr(self, k) is not None:
setattr(self, k, None)
return self
def __repr_args__(self) -> Iterator[tuple[str | None, Any]]:
# Only show the properties that are set: most CZML objects have dozens
# of optional properties, almost all of them None.
for name, value in super().__repr_args__():
if value is not None:
yield name, value
def __str__(self) -> str:
return self.to_json()
[docs]
def dumps(self, **kwargs: Any) -> str:
"""Serialize the object to a JSON string.
kwargs are passed to `BaseModel.model_dump_json()`.
:return: JSON string representation of the object with None values excluded
:rtype: str
"""
return self.model_dump_json(exclude_none=True, **kwargs)
[docs]
def to_json(self, *, indent: int = 4, **kwargs: Any) -> str:
"""Return the object as a formatted JSON string.
kwargs are passed to `BaseModel.model_dump_json()`.
:param indent: Number of spaces for indentation, defaults to 4
:type indent: int, optional
:return: Formatted JSON string representation with None values excluded
:rtype: str
"""
return self.model_dump_json(exclude_none=True, indent=indent, **kwargs)
[docs]
def to_dict(self, **kwargs: Any) -> dict[str, Any]:
"""Return the object as a dictionary.
kwargs are passed to `BaseModel.model_dump()`.
:return: Dictionary representation of the object with None values excluded
:rtype: dict
"""
return self.model_dump(exclude_none=True, **kwargs)