Source code for edges_analysis.gsdata.history
"""Classes for defining the history of a GSData / GSFlag object."""
from __future__ import annotations
import astropy
import datetime
import edges_cal
import edges_io
import numpy as np
import read_acq
import yaml
from attrs import asdict, define, evolve, field
from attrs import validators as vld
from hickleable import hickleable
from .. import __version__
[docs]
@hickleable()
@define(frozen=True, slots=False)
class Stamp:
"""Class representing a historical record of a process applying to an object.
Parameters
----------
message
A message describing the process. Optional -- either this or the function
must be defined.
function
The name of the function that was applied. Optional -- either this or the
message must be defined.
parameter(s)
The parameters passed to the function. Optional -- if ``function`` is defined,
this should be specified.
versions
A dictionary of the versions of the software used to perform the process.
Created by default when the History is created.
timestamp
A datetime object corresponding to the time the process was performed.
By default, this is set to the time that the Stamp object is created.
"""
message: str = field(default="")
function: str = field(default="")
parameters: dict = field(factory=dict)
versions: dict = field()
timestamp: datetime.datetime = field(factory=datetime.datetime.now)
@function.validator
def _function_vld(self, _, value):
if not value and not self.message:
raise ValueError("History record must have a message or a function")
@versions.default
def _versions_default(self):
return {
"edges-analysis": __version__,
"edges-cal": edges_cal.__version__,
"read_acq": read_acq.__version__,
"edges-io": edges_io.__version__,
"numpy": np.__version__,
"astropy": astropy.__version__,
}
def _to_yaml_dict(self):
dct = asdict(self)
dct["timestamp"] = dct["timestamp"].isoformat()
return dct
def __repr__(self):
"""Technical representation of the history record."""
return yaml.dump(self._to_yaml_dict())
def __str__(self):
"""Human-readable representation of the history record."""
pstring = " ".join(f"{k}: {v}" for k, v in self.parameters.items())
vstring = " | ".join(f"{k} ({v})" for k, v in self.versions.items())
return f"""{self.timestamp.isoformat()}
function: {self.function}
message : {self.message}
parameters:
{pstring}
versions: {vstring}
"""
[docs]
def pretty(self):
"""Return a rich-compatible string representation of the history record."""
pstring = " ".join(
f"[green]{k}[/]: [dim]{v}[/]" for k, v in self.parameters.items()
)
vstring = " | ".join(f"{k} ([blue]{v}[/])" for k, v in self.versions.items())
return f"""[bold underline blue]{self.timestamp.isoformat()}[/]
[bold green]function[/] : {self.function}
[bold green]message [/] : {self.message}
[bold green]parameters[/]:
{pstring}
[bold green]versions[/] : {vstring}
"""
[docs]
@classmethod
def from_repr(cls, repr_string: str):
"""Create a Stamp object from a string representation."""
dct = yaml.load(repr_string, Loader=yaml.FullLoader)
return cls.from_yaml_dict(dct)
[docs]
@classmethod
def from_yaml_dict(cls, d: dict) -> Stamp:
"""Create a Stamp object from a dictionary representing a history record."""
d["timestamp"] = datetime.datetime.strptime(
d["timestamp"], "%Y-%m-%dT%H:%M:%S.%f"
)
return cls(**d)
[docs]
@hickleable()
@define(slots=False)
class History:
"""A collection of Stamp objects defining the history."""
stamps: tuple[Stamp] = field(
factory=tuple,
converter=tuple,
validator=vld.deep_iterable(vld.instance_of(Stamp), vld.instance_of(tuple)),
)
def __attrs_post_init__(self):
"""Define the timestamps as keys."""
self._keysdates = tuple(stamp.timestamp for stamp in self.stamps)
self._keystring = tuple(stamp.timestamp.isoformat() for stamp in self.stamps)
def __repr__(self):
"""Technical representation of the history."""
out = tuple(s._to_yaml_dict() for s in self.stamps)
return yaml.dump(out)
def __str__(self):
"""Human-readable representation of the history."""
return "\n\n".join(str(s) for s in self.stamps)
[docs]
def pretty(self):
"""Return a rich-compatible string representation of the history."""
return "\n\n".join(s.pretty() for s in self.stamps)
def __getitem__(self, key):
"""Return the Stamp object corresponding to the given key."""
if isinstance(key, int):
return self.stamps[key]
elif isinstance(key, str):
if key not in self._keystring:
raise KeyError(
f"{key} not in history. Make sure the key is in ISO format."
)
return self.stamps[self._keystring.index(key)]
elif isinstance(key, datetime.datetime):
if key not in self._keysdates:
raise KeyError(f"{key} not in history")
return self.stamps[self._keysdates.index(key)]
else:
raise KeyError(
f"{key} not a valid key. Must be int, ISO date string, or datetime."
)
[docs]
@classmethod
def from_repr(cls, repr_string: str):
"""Create a History object from a string representation."""
d = yaml.load(repr_string, Loader=yaml.FullLoader)
return cls(stamps=[Stamp.from_yaml_dict(s) for s in d])
[docs]
def add(self, stamp: Stamp | dict):
"""Add a stamp to the history."""
if isinstance(stamp, dict):
stamp = Stamp(**stamp)
if not isinstance(stamp, Stamp):
raise TypeError("stamp must be a Stamp or a dictionary")
return evolve(self, stamps=self.stamps + (stamp,))
def __len__(self):
"""Returns the number of stamps."""
return len(self.stamps)
def __iter__(self):
"""Iterate over the stamps."""
return iter(self.stamps)