Source code for id_translation.toml.meta._base_metadata
import json
from abc import ABC, abstractmethod
from collections.abc import Iterable
from datetime import UTC, datetime, timedelta
from importlib.metadata import version as get_version
from pathlib import Path
from typing import Any, Literal, Self, TypeAlias
import pandas
from rics.strings import format_seconds as fmt_sec
from id_translation.translator_typing import CacheMissReasonType
[docs]
class BaseMetadata(ABC):
"""Base implementation for Metadata types.
Args:
versions: Versions, e.g. ``{'python': '3.11.11'`, 'your-package': '1.0.0'}``.
created: The time at which the metadata was originally created.
"""
MaxAge: TypeAlias = str | pandas.Timedelta | timedelta | None
def __init__(
self,
versions: dict[str, str] | None = None,
created: datetime | None = None,
) -> None:
self.versions = self.get_package_versions([]) if versions is None else versions
self.created = created or datetime.now(UTC)
@abstractmethod
def _to_dict(self, to_json: dict[str, Any]) -> dict[str, Any]:
"""Turn `to_json` into JSON-serializable types."""
@classmethod
@abstractmethod
def _deserialize(cls, from_json: dict[str, Any]) -> dict[str, Any]:
"""Turn `from_json` into desired types."""
@abstractmethod
def _is_equivalent(self, other: Self) -> str:
"""Implementation-specific equivalence check."""
[docs]
def is_equivalent(self, other: Self) -> str:
"""Compute equivalency with `other`.
Args:
other: Another metadata instance.
Returns:
A string `reason_not_equivalent` or an empty string.
"""
if not isinstance(other, self.__class__):
return f"Expected class={self.__class__.__name__} but got {other.__class__}"
for package, version in self.versions.items():
other_version = other.versions.get(package)
if other_version != version:
return f"Expected {package}={version!r} (your environment) but got {package}={other_version!r}"
return self._is_equivalent(other)
[docs]
def to_dict(self) -> dict[str, Any]:
"""Get a dict representation of this ``BaseMetadata``."""
raw = self.__dict__.copy()
kwargs = dict(
versions=raw.pop("versions"),
created=raw.pop("created").isoformat(),
)
kwargs.update(self._to_dict(raw))
assert not raw, f"Not serialized: {raw}." # noqa: S101
return kwargs
[docs]
def to_json(self) -> str:
"""Get a dict representation of this ``BaseMetadata``."""
kwargs = self.to_dict()
return json.dumps(kwargs, indent=4)
[docs]
@classmethod
def from_json(cls, s: str) -> "BaseMetadata":
"""Create ``BaseMetadata`` from a JSON string `s`."""
raw = json.loads(s)
kwargs = dict(
versions=raw.pop("versions"),
created=datetime.fromisoformat(raw.pop("created")),
)
kwargs.update(cls._deserialize(raw))
assert not raw, f"Not deserialized: {raw}." # noqa: S101
return cls(**kwargs)
[docs]
def use_cached(
self,
metadata_path: Path,
max_age: MaxAge,
) -> tuple[Literal[True], str, None] | tuple[Literal[False], str, CacheMissReasonType]:
"""Check status of stored metadata config based a desired configuration ``self``.
Args:
metadata_path: Path of stored metadata.
max_age: Maximum age of stored metadata. Pass zero to force recreation. Smaller than zero = never expire.
Returns:
A tuple ``(True, expires_at, None)`` or ``(False, reason, reason_type)``.
"""
if not metadata_path.exists():
return False, "no cache metadata found", "metadata-missing"
stored_config = self.from_json(metadata_path.read_text())
reason_not_equivalent = self.is_equivalent(stored_config)
if reason_not_equivalent:
return False, f"cached instance is not equivalent: {reason_not_equivalent}", "metadata-changed"
if max_age is None:
return True, "does not expire", None
delta = pandas.Timedelta(max_age)
expires_at = (stored_config.created + abs(delta)).replace(microsecond=0)
offset = fmt_sec(round(abs(datetime.now(UTC) - expires_at).total_seconds()))
if expires_at <= stored_config.created:
return False, f"expired at {expires_at.isoformat()} ({offset} ago)", "too-old"
else:
return True, f"expires at {expires_at.isoformat()} (in {offset})", None
[docs]
@classmethod
def get_package_versions(cls, extra_packages: Iterable[str]) -> dict[str, str]:
"""Extract package versions using ``importlib.metadata``."""
assert not isinstance(extra_packages, str) # noqa: S101
packages = ["rics", "id-translation", "sqlalchemy", "pandas", *extra_packages]
return {package: get_version(package) for package in packages}