Skip to content

Commit

Permalink
Merge pull request #48 from lsst/tickets/DM-27477
Browse files Browse the repository at this point in the history
DM-27477: Add JSON serialization for ObservationInfo
  • Loading branch information
timj committed Feb 17, 2021
2 parents 425d611 + f084d3e commit 05ce667
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 50 deletions.
30 changes: 30 additions & 0 deletions python/astro_metadata_translator/observationGroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ def __iter__(self):
def __eq__(self, other):
"""Compares equal if all the members are equal in the same order.
"""
if not isinstance(other, ObservationGroup):
return NotImplemented

for info1, info2 in zip(self, other):
if info1 != info2:
return False
Expand Down Expand Up @@ -188,3 +191,30 @@ def property_values(self, property):
All the distinct values for that property within this group.
"""
return {getattr(obs_info, property) for obs_info in self}

def to_simple(self):
"""Convert the group to simplified form.
Returns
-------
simple : `list` of `dict`
Simple form is a list containing the simplified dict form of
each `ObservationInfo`.
"""
return [obsinfo.to_simple() for obsinfo in self]

@classmethod
def from_simple(cls, simple):
"""Convert simplified form back to `ObservationGroup`
Parameters
----------
simple : `list` of `dict`
Object returned by `to_simple`.
Returns
-------
group : `ObservationGroup`
Reconstructed group.
"""
return cls((ObservationInfo.from_simple(o) for o in simple))
127 changes: 114 additions & 13 deletions python/astro_metadata_translator/observationInfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import itertools
import logging
import copy
import json
import math

import astropy.time
from astropy.coordinates import SkyCoord, AltAz
Expand Down Expand Up @@ -267,18 +269,22 @@ def __str__(self):
def __eq__(self, other):
"""Compares equal if standard properties are equal
"""
if type(self) != type(other):
return False

for p in self._PROPERTIES:
# Use string comparison since SkyCoord.__eq__ seems unreliable
# otherwise. Should have per-type code so that floats and
# quantities can be compared properly.
v1 = f"{getattr(self, p)}"
v2 = f"{getattr(other, p)}"
if v1 != v2:
if not isinstance(other, ObservationInfo):
return NotImplemented

# Compare simplified forms.
# Cannot compare directly because nan will not equate as equal
# whereas they should be equal for our purposes
self_simple = self.to_simple()
other_simple = other.to_simple()

for k, self_value in self_simple.items():
other_value = other_simple[k]
if self_value != other_value:
if math.isnan(self_value) and math.isnan(other_value):
# If both are nan this is fine
continue
return False

return True

def __lt__(self, other):
Expand Down Expand Up @@ -310,6 +316,100 @@ def __setstate__(self, state):
property = f"_{p}"
setattr(self, property, state[p])

def to_simple(self):
"""Convert the contents of this object to simple dict form.
The keys of the dict are the standard properties but the values
can be simplified to support JSON serialization. For example a
SkyCoord might be represented as an ICRS RA/Dec tuple rather than
a full SkyCoord representation.
Any properties with `None` value will be skipped.
Can be converted back to an `ObservationInfo` using `from_simple()`.
Returns
-------
simple : `dict` of [`str`, `Any`]
Simple dict of all properties.
"""
simple = {}

for p in self._PROPERTIES:
property = f"_{p}"
value = getattr(self, property)
if value is None:
continue

# Access the function to simplify the property
simplifier = self._PROPERTIES[p][3]

if simplifier is None:
simple[p] = value
continue

simple[p] = simplifier(value)

return simple

def to_json(self):
"""Serialize the object to JSON string.
Returns
-------
j : `str`
The properties of the ObservationInfo in JSON string form.
"""
return json.dumps(self.to_simple())

@classmethod
def from_simple(cls, simple):
"""Convert the entity returned by `to_simple` back into an
`ObservationInfo`.
Parameters
----------
simple : `dict` [`str`, `Any`]
The dict returned by `to_simple()`
Returns
-------
obsinfo : `ObservationInfo`
New object constructed from the dict.
"""
processed = {}
for k, v in simple.items():

if v is None:
continue

# Access the function to convert from simple form
complexifier = cls._PROPERTIES[k][4]

if complexifier is not None:
v = complexifier(v, **processed)

processed[k] = v

return cls.makeObservationInfo(**processed)

@classmethod
def from_json(cls, json_str):
"""Create `ObservationInfo` from JSON string.
Parameters
----------
json_str : `str`
The JSON representation.
Returns
-------
obsinfo : `ObservationInfo`
Reconstructed object.
"""
simple = json.loads(json_str)
return cls.from_simple(simple)

@classmethod
def makeObservationInfo(cls, **kwargs): # noqa: N802
"""Construct an `ObservationInfo` from the supplied parameters.
Expand Down Expand Up @@ -344,7 +444,8 @@ def makeObservationInfo(cls, **kwargs): # noqa: N802
unused.remove(p)

if unused:
raise KeyError(f"Unrecognized properties provided: {', '.join(unused)}")
n = len(unused)
raise KeyError(f"Unrecognized propert{'y' if n == 1 else 'ies'} provided: {', '.join(unused)}")

return obsinfo

Expand Down Expand Up @@ -386,7 +487,7 @@ def getter(self):
# getter methods.
for name, description in ObservationInfo._PROPERTIES.items():
setattr(ObservationInfo, f"_{name}", None)
setattr(ObservationInfo, name, property(_make_property(name, *description)))
setattr(ObservationInfo, name, property(_make_property(name, *description[:3])))


def makeObservationInfo(**kwargs): # noqa: N802
Expand Down

0 comments on commit 05ce667

Please sign in to comment.