diff --git a/mreg_cli/api/abstracts.py b/mreg_cli/api/abstracts.py index 9894a9a5..f0ffc2fa 100644 --- a/mreg_cli/api/abstracts.py +++ b/mreg_cli/api/abstracts.py @@ -10,7 +10,6 @@ from pydantic.fields import FieldInfo from mreg_cli.api.endpoints import Endpoint -from mreg_cli.api.history import HistoryItem, HistoryResource from mreg_cli.exceptions import ( CreateError, EntityAlreadyExists, @@ -508,21 +507,3 @@ def create( raise CreateError(f"Failed to create {cls} with {params} @ {cls.endpoint()}.") return None - - def history(self, resource: HistoryResource) -> list[HistoryItem]: - """Get the history of the object. - - :param resource: The resource type to get the history for. - - :returns: The history of the object. - """ - name = self.id_for_endpoint() - return HistoryItem.get(str(name), resource) - - def output_history(self, resource: HistoryResource) -> None: - """Output the history of the object. - - :param resource: The resource type to get the history for. - """ - items = self.history(resource) - HistoryItem.output_multiple(str(self.id_for_endpoint()), items) diff --git a/mreg_cli/api/models.py b/mreg_cli/api/models.py index 24eeb1d5..806525fd 100644 --- a/mreg_cli/api/models.py +++ b/mreg_cli/api/models.py @@ -5,20 +5,23 @@ import ipaddress import re from datetime import date, datetime -from typing import Any, Literal, Self, cast +from typing import Any, ClassVar, Literal, Self, cast from pydantic import ( AliasChoices, BaseModel, + ConfigDict, Field, computed_field, field_validator, model_validator, ) +from typing_extensions import Unpack from mreg_cli.api.abstracts import APIMixin, FrozenModel, FrozenModelWithTimestamps from mreg_cli.api.endpoints import Endpoint from mreg_cli.api.fields import IPAddressField, MACAddressField, NameList +from mreg_cli.api.history import HistoryItem, HistoryResource from mreg_cli.config import MregCliConfig from mreg_cli.exceptions import ( CliWarning, @@ -334,6 +337,44 @@ def rename(self, new_name: str) -> Self: return self.patch({self.__name_field__: new_name}) +ClassVarNotSet = object() + + +def AbstractClassVar() -> Any: + """Hack to implement an abstract class variable on a Pydantic model.""" + return ClassVarNotSet + + +class WithHistory(BaseModel, APIMixin): + """Resource that supports history lookups. + + Subclasses must implement the `history_resource` class variable. + """ + + history_resource: ClassVar[HistoryResource] = AbstractClassVar() + + def __init_subclass__(cls, **kwargs: Unpack[ConfigDict]): + """Ensure that subclasses implement the history_resource class var.""" + # NOTE: Only works for Pydantic model subclasses! + for attr in cls.__class_vars__: + if getattr(cls, attr) == ClassVarNotSet: + raise NotImplementedError( + f"Subclass {cls.__name__} must implement abstract class var `{attr}`." + ) + return super().__init_subclass__(**kwargs) + + @classmethod + def get_history(cls, name: str) -> list[HistoryItem]: + """Get the history for the object.""" + return HistoryItem.get(name, cls.history_resource) + + @classmethod + def output_history(cls, name: str) -> None: + """Output the history for the object.""" + history = cls.get_history(name) + HistoryItem.output_multiple(name, history) + + class NameServer(FrozenModelWithTimestamps, WithTTL): """Model for representing a nameserver within a DNS zone.""" @@ -1068,7 +1109,7 @@ def output(self, padding: int = 14) -> None: output_manager.add_line(f"{'Description:':<{padding}}{self.description}") -class Role(HostPolicy): +class Role(HostPolicy, WithHistory): """Model for a role.""" id: int # noqa: A003 @@ -1076,6 +1117,8 @@ class Role(HostPolicy): atoms: NameList labels: list[int] + history_resource: ClassVar[HistoryResource] = HistoryResource.HostPolicy_Role + @classmethod def endpoint(cls) -> Endpoint: """Return the endpoint for the class.""" @@ -1272,12 +1315,14 @@ def delete(self) -> bool: return super().delete() -class Atom(HostPolicy): +class Atom(HostPolicy, WithHistory): """Model for an atom.""" id: int # noqa: A003 roles: NameList + history_resource: ClassVar[HistoryResource] = HistoryResource.HostPolicy_Atom + @classmethod def endpoint(cls) -> Endpoint: """Return the endpoint for the class.""" @@ -2109,7 +2154,7 @@ def output(self, padding: int = 14): OutputManager().add_line(f"{'LOC:':<{padding}}{self.loc}") -class Host(FrozenModelWithTimestamps, WithTTL, APIMixin): +class Host(FrozenModelWithTimestamps, WithTTL, WithHistory, APIMixin): """Model for an individual host.""" id: int # noqa: A003 @@ -2129,6 +2174,8 @@ class Host(FrozenModelWithTimestamps, WithTTL, APIMixin): # Note, we do not use WithZone here as this is optional and we resolve it differently. zone: int | None = None + history_resource: ClassVar[HistoryResource] = HistoryResource.Host + @field_validator("name", mode="before") @classmethod def validate_name(cls, value: str) -> HostT: @@ -2819,7 +2866,7 @@ def _format(name: str, contact: str, comment: str) -> None: _format(str(i.name), i.contact, i.comment or "") -class HostGroup(FrozenModelWithTimestamps, WithName, APIMixin): +class HostGroup(FrozenModelWithTimestamps, WithName, WithHistory, APIMixin): """Model for a hostgroup.""" id: int # noqa: A003 @@ -2830,6 +2877,8 @@ class HostGroup(FrozenModelWithTimestamps, WithName, APIMixin): hosts: NameList owners: NameList + history_resource: ClassVar[HistoryResource] = HistoryResource.Group + @classmethod def endpoint(cls) -> Endpoint: """Return the endpoint for the class.""" diff --git a/mreg_cli/commands/group.py b/mreg_cli/commands/group.py index 8a9db6cf..ace0872c 100644 --- a/mreg_cli/commands/group.py +++ b/mreg_cli/commands/group.py @@ -5,7 +5,6 @@ import argparse from typing import Any -from mreg_cli.api.history import HistoryResource from mreg_cli.api.models import Host, HostGroup from mreg_cli.commands.base import BaseCommand from mreg_cli.commands.registry import CommandRegistry @@ -323,4 +322,4 @@ def history(args: argparse.Namespace) -> None: :param args: argparse.Namespace (name) """ - HostGroup.get_by_name_or_raise(args.name).output_history(HistoryResource.Group) + HostGroup.output_history(args.name) diff --git a/mreg_cli/commands/host_submodules/core.py b/mreg_cli/commands/host_submodules/core.py index 86d43a75..2560f884 100644 --- a/mreg_cli/commands/host_submodules/core.py +++ b/mreg_cli/commands/host_submodules/core.py @@ -494,4 +494,5 @@ def history(args: argparse.Namespace) -> None: :param args: argparse.Namespace (name) """ - Host.get_by_any_means_or_raise(args.name).output_history(HistoryResource.Host) + hostname = HostT(hostname=args.name) + Host.output_history(hostname.hostname) diff --git a/mreg_cli/commands/policy.py b/mreg_cli/commands/policy.py index 84c02705..507e5694 100644 --- a/mreg_cli/commands/policy.py +++ b/mreg_cli/commands/policy.py @@ -466,8 +466,7 @@ def atom_history(args: argparse.Namespace) -> None: """ name: str = args.name - atom = Atom.get_by_name_or_raise(name) - atom.output_history(HistoryResource.HostPolicy_Atom) + Atom.output_history(name) @command_registry.register_command( @@ -485,5 +484,4 @@ def role_history(args: argparse.Namespace) -> None: """ name: str = args.name - role = Role.get_by_name_or_raise(name) - role.output_history(HistoryResource.HostPolicy_Role) + Role.output_history(name)