Skip to content

Commit 2bc468f

Browse files
committed
WIP: Add concept for composable UMM generators
1 parent 119fd08 commit 2bc468f

4 files changed

Lines changed: 424 additions & 0 deletions

File tree

mandible/umm_generator/__init__.py

Whitespace-only changes.

mandible/umm_generator/base.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import collections
2+
import datetime
3+
import inspect
4+
from typing import Any, Dict, Optional, Type
5+
6+
7+
class MISSING:
8+
__slots__ = ()
9+
10+
11+
class Umm:
12+
_attributes = {}
13+
14+
def __init_subclass__(cls, **kwargs):
15+
super().__init_subclass__(**kwargs)
16+
17+
# TODO(reweeden): Make this work with multiple inheritance?
18+
parent_cls = super(cls, cls)
19+
attributes = {**parent_cls._attributes}
20+
21+
for name, typ in get_annotations(cls).items():
22+
# TODO(reweeden): What if we're overwriting an attribute from the
23+
# parent and the types don't match?
24+
attributes[name] = (typ, cls.__dict__.get(name, MISSING))
25+
26+
# Update attributes with unannotated default values
27+
for name, value in inspect.getmembers(cls):
28+
if name.startswith("_") or inspect.isfunction(value):
29+
continue
30+
31+
if name not in attributes:
32+
attributes[name] = (Any, value)
33+
34+
cls._attributes = attributes
35+
36+
def __init__(
37+
self,
38+
metadata: Dict[str, Any],
39+
debug_name: Optional[str] = None,
40+
):
41+
if debug_name is None:
42+
debug_name = self.__class__.__name__
43+
for name, (typ, default) in self._attributes.items():
44+
attr_debug_name = f"{debug_name}.{name}"
45+
try:
46+
value = self._init_attr_value(
47+
name,
48+
attr_debug_name,
49+
typ,
50+
default,
51+
metadata,
52+
)
53+
setattr(self, name, value)
54+
except RuntimeError:
55+
raise
56+
except Exception as e:
57+
raise RuntimeError(
58+
f"Encountered an error initializing "
59+
f"'{attr_debug_name}': {e}",
60+
) from e
61+
62+
def _init_attr_value(
63+
self,
64+
attr_name: str,
65+
debug_name: Optional[str],
66+
typ: type,
67+
default: Any,
68+
metadata: dict,
69+
) -> Any:
70+
if inspect.isclass(typ) and issubclass(typ, Umm):
71+
if type(self) is typ:
72+
# TODO(reweeden): Error type?
73+
raise RuntimeError(
74+
f"Self-reference detected for attribute '{debug_name}'",
75+
)
76+
77+
return typ(metadata, debug_name=debug_name)
78+
79+
value = default
80+
# TODO(reweeden): Ability to set handler function manually?
81+
# For example:
82+
# class Foo(Umm):
83+
# Attribute: str = Attr()
84+
#
85+
# @Attribute.getter
86+
# def get_attribute(self, metadata):
87+
# ...
88+
handler_name = f"get_{attr_name}"
89+
handler = getattr(self, handler_name, None)
90+
91+
if value is MISSING:
92+
if handler is None:
93+
if (
94+
hasattr(typ, "__origin__")
95+
and hasattr(typ, "__args__")
96+
and issubclass(typ.__origin__, collections.abc.Sequence)
97+
):
98+
for cls in typ.__args__:
99+
if not issubclass(cls, Umm):
100+
# TODO(reweeden): Error type?
101+
raise RuntimeError(
102+
f"Non-Umm element of tuple type found for "
103+
f"'{debug_name}'",
104+
)
105+
return tuple(
106+
cls(metadata, debug_name=debug_name)
107+
for cls in typ.__args__
108+
)
109+
110+
# TODO(reweeden): Error type?
111+
raise RuntimeError(
112+
f"Missing value for '{debug_name}'. "
113+
f"Try implementing a '{handler_name}' method",
114+
)
115+
116+
return handler(metadata)
117+
elif value is not MISSING and handler is not None:
118+
# TODO(reweeden): Error type?
119+
raise RuntimeError(
120+
f"Found both explicit value and handler function for "
121+
f"'{debug_name}'",
122+
)
123+
124+
return value
125+
126+
def to_dict(self) -> Dict[str, Any]:
127+
return _to_dict(self)
128+
129+
130+
def get_annotations(cls) -> Dict[str, Type[Any]]:
131+
if hasattr(inspect, "get_annotations"):
132+
return inspect.get_annotations(cls, eval_str=True)
133+
134+
# TODO(reweeden): String evaluation
135+
return dict(cls.__annotations__)
136+
137+
138+
def _to_dict(obj: Any) -> Any:
139+
if isinstance(obj, Umm):
140+
return {
141+
name: _to_dict(value)
142+
for name in obj._attributes
143+
# Filter out optional keys, marked by having a `None` value
144+
if (value := getattr(obj, name)) is not None
145+
}
146+
147+
if isinstance(obj, collections.abc.Sequence) and not isinstance(obj, str):
148+
return [_to_dict(item) for item in obj]
149+
150+
# TODO(reweeden): Serialize to string here, or do that via JSON encoder?
151+
if isinstance(obj, datetime.datetime):
152+
return obj
153+
154+
return obj

mandible/umm_generator/umm_g.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
from datetime import datetime
2+
from typing import Any, Dict, Optional, Sequence, Union
3+
4+
from .base import Umm
5+
6+
UMM_DATE_FORMAT = "%Y-%m-%d"
7+
UMM_DATETIME_FORMAT = f"{UMM_DATE_FORMAT}T%H:%M:%SZ"
8+
9+
10+
# AdditionalAttributes
11+
class AdditionalAttribute(Umm):
12+
Name: str
13+
Values: Sequence[str]
14+
15+
16+
# CollectionReference
17+
class CollectionReferenceShortNameVersion(Umm):
18+
ShortName: str
19+
Version: str
20+
21+
22+
class CollectionReferenceEntryTitle(Umm):
23+
EntryTitle: str
24+
25+
26+
CollectionReference = Union[
27+
CollectionReferenceShortNameVersion,
28+
CollectionReferenceEntryTitle,
29+
]
30+
31+
32+
# DataGranule
33+
# ArchiveAndDistributionInformation
34+
class Checksum(Umm):
35+
Value: str
36+
Algorithm: str
37+
38+
39+
class ArchiveAndDistributionInformation(Umm):
40+
Name: str
41+
SizeInBytes: Optional[int] = None
42+
Size: Optional[int] = None
43+
SizeUnit: Optional[str] = None
44+
Format: Optional[str] = None
45+
FormatType: Optional[str] = None
46+
MimeType: Optional[str]
47+
Checksum: Optional[Checksum] = None
48+
49+
50+
class Identifier(Umm):
51+
IdentifierType: str
52+
Identifier: str
53+
IdentifierName: Optional[str] = None
54+
55+
56+
class DataGranule(Umm):
57+
ArchiveAndDistributionInformation: Optional[
58+
Sequence[ArchiveAndDistributionInformation]
59+
] = None
60+
DayNightFlag: str = "Unspecified"
61+
Identifiers: Optional[Sequence[Identifier]] = None
62+
ProductionDateTime: datetime
63+
ReprocessingActual: Optional[str] = None
64+
ReprocessingPlanned: Optional[str] = None
65+
66+
67+
# MetadataSpecification
68+
class MetadataSpecification(Umm):
69+
Name: str = "UMM-G"
70+
URL: str = "https://cdn.earthdata.nasa.gov/umm/granule/v1.6.5"
71+
Version: str = "1.6.5"
72+
73+
74+
class UmmG(Umm):
75+
# Sorted?
76+
AdditionalAttributes: Optional[Sequence[AdditionalAttribute]] = None
77+
CollectionReference: CollectionReference
78+
DataGranule: Optional[DataGranule] = None
79+
GranuleUR: str
80+
MetadataSpecification: MetadataSpecification
81+
# OrbitCalculatedSpatialDomains: Optional[self.get_orbit_calculated_spatial_domains()]
82+
# PGEVersionClass: Optional[self.get_pge_version_class()]
83+
# Platforms: Optional[self.get_platforms()]
84+
# Projects: Optional[self.get_projects()]
85+
# ProviderDates: self.get_provider_dates(),
86+
# RelatedUrls: Optional[self.get_related_urls()]
87+
# SpatialExtent: Optional[self.get_spatial_extent()]
88+
# TemporalExtent: Optional[self.get_temporal_extent()]
89+
# InputGranules: Optional[self.get_input_granules()]
90+
91+
def get_GranuleUR(self, metadata: Dict[str, Any]) -> str:
92+
return metadata["granule"]["granuleId"]

0 commit comments

Comments
 (0)