Skip to content

Commit 41e1979

Browse files
committed
WIP: Add concept for composable UMM generators
1 parent e31dbea commit 41e1979

4 files changed

Lines changed: 218 additions & 0 deletions

File tree

mandible/umm_generator/__init__.py

Whitespace-only changes.

mandible/umm_generator/base.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import inspect
2+
from typing import Any, Dict, Type
3+
4+
5+
class MISSING:
6+
__slots__ = ()
7+
8+
9+
class Umm:
10+
_attributes = {}
11+
12+
def __init_subclass__(cls, **kwargs):
13+
super().__init_subclass__(**kwargs)
14+
15+
# TODO(reweeden): Make this work with multiple inheritance?
16+
parent_cls = super(cls, cls)
17+
attributes = {**parent_cls._attributes}
18+
19+
for name, typ in get_annotations(cls).items():
20+
# TODO(reweeden): What if we're overwriting an attribute from the
21+
# parent and the types don't match?
22+
attributes[name] = (typ, getattr(cls, name, MISSING))
23+
24+
# Update attributes with unannotated default values
25+
for name, value in inspect.getmembers(cls):
26+
if name.startswith("_") or inspect.isfunction(value):
27+
continue
28+
29+
if name not in attributes:
30+
attributes[name] = (Any, value)
31+
else:
32+
typ, _ = attributes[name]
33+
attributes[name] = (typ, value)
34+
35+
cls._attributes = attributes
36+
37+
def __init__(self, metadata: Dict[str, Any]):
38+
for name, (typ, default) in self._attributes.items():
39+
if inspect.isclass(typ) and issubclass(typ, Umm):
40+
if type(self) is typ:
41+
# TODO(reweeden): Error type?
42+
raise RuntimeError(
43+
f"Self-reference detected for attribute '{name}'",
44+
)
45+
46+
setattr(self, name, typ(metadata))
47+
else:
48+
value = default
49+
if value is MISSING:
50+
# TODO(reweeden): Ability to set handler function manually?
51+
# For example:
52+
# class Foo(Umm):
53+
# Attribute: str = Attr()
54+
#
55+
# @Attribute.getter
56+
# def get_attribute(self, metadata):
57+
# ...
58+
handler_name = f"get_{name}"
59+
handler = getattr(self, handler_name, None)
60+
if handler:
61+
value = handler(metadata)
62+
63+
if value is MISSING:
64+
# TODO(reweeden): Error type?
65+
raise RuntimeError(
66+
f"Missing value for '{name}'. "
67+
f"Try implementing a 'get_{name}' method",
68+
)
69+
setattr(self, name, value)
70+
71+
def to_dict(self) -> Dict[str, Any]:
72+
return _to_dict(self)
73+
74+
75+
def get_annotations(cls) -> Dict[str, Type[Any]]:
76+
if hasattr(inspect, "get_annotations"):
77+
return inspect.get_annotations(cls, eval_str=True)
78+
79+
# TODO(reweeden): String evaluation
80+
return dict(cls.__annotations__)
81+
82+
83+
def _to_dict(obj: Any) -> Any:
84+
if isinstance(obj, Umm):
85+
return {
86+
name: _to_dict(
87+
getattr(obj, name),
88+
)
89+
for name in obj._attributes
90+
}
91+
92+
return obj

mandible/umm_generator/umm_g.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from typing import Any, Dict, Sequence
2+
3+
from .base import Umm
4+
5+
6+
class AdditionalAttribute(Umm):
7+
Name: str
8+
Values: Sequence[str]
9+
10+
11+
class CollectionReference(Umm):
12+
ShortName: str
13+
Version: str
14+
15+
16+
# class DataGranule:
17+
# "ArchiveAndDistributionInformation": self.get_archive_and_distribution_information(),
18+
# "DayNightFlag": "Unspecified",
19+
# "Identifiers": self.get_identifiers(),
20+
# "ProductionDateTime": to_umm_str(self.get_product_creation_time()),
21+
22+
23+
class MetadataSpecification(Umm):
24+
Name: str = "UMM-G"
25+
URL: str = "https://cdn.earthdata.nasa.gov/umm/granule/v1.6.5"
26+
Version: str = "1.6.5"
27+
28+
29+
class UmmG(Umm):
30+
# Sorted?
31+
AdditionalAttributes: Sequence[AdditionalAttribute]
32+
CollectionReference: CollectionReference
33+
# DataGranule: DataGranule
34+
GranuleUR: str
35+
MetadataSpecification: MetadataSpecification
36+
# OrbitCalculatedSpatialDomains: self.get_orbit_calculated_spatial_domains(),
37+
# PGEVersionClass: self.get_pge_version_class(),
38+
# Platforms: self.get_platforms(),
39+
# Projects: self.get_projects(),
40+
# ProviderDates: self.get_provider_dates(),
41+
# RelatedUrls: self.get_related_urls(),
42+
# SpatialExtent: self.get_spatial_extent(),
43+
# TemporalExtent: self.get_temporal_extent(),
44+
# InputGranules: self.get_input_granules(),
45+
46+
def get_GranuleUR(self, metadata: Dict[str, Any]) -> str:
47+
return metadata["granule"]["granuleId"]

tests/test_umm_generator.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import pytest
2+
3+
from mandible.umm_generator.base import Umm
4+
from mandible.umm_generator.umm_g import CollectionReference, UmmG
5+
6+
7+
def test_custom_umm():
8+
class TestComponent(Umm):
9+
Field1: str
10+
Field2: int
11+
12+
def get_Field1(self, metadata) -> str:
13+
return metadata["field_1"]
14+
15+
def get_Field2(self, metadata) -> int:
16+
return metadata["field_2"]
17+
18+
class TestMain(Umm):
19+
Name: str
20+
Component: TestComponent
21+
22+
def get_Name(self, metadata) -> str:
23+
return metadata["name"]
24+
25+
metadata = {
26+
"field_1": "Value 1",
27+
"field_2": "Value 2",
28+
"name": "Test Name",
29+
}
30+
item = TestMain(metadata)
31+
32+
assert item.Name == "Test Name"
33+
assert item.to_dict() == {
34+
"Name": "Test Name",
35+
"Component": {
36+
"Field1": "Value 1",
37+
"Field2": "Value 2",
38+
}
39+
}
40+
41+
42+
def test_umm_g_abstract():
43+
with pytest.raises(Exception):
44+
_ = UmmG({})
45+
46+
47+
def test_umm_g():
48+
class CustomCollectionReference(CollectionReference):
49+
ShortName: str = "FOOBAR"
50+
Version: str = "10"
51+
52+
class BasicUmmG(UmmG):
53+
AdditionalAttributes = []
54+
CollectionReference: CustomCollectionReference
55+
56+
metadata = {
57+
"granule": {
58+
"granuleId": "SomeGranuleId",
59+
},
60+
}
61+
umm_g = BasicUmmG(metadata)
62+
63+
assert umm_g.AdditionalAttributes == []
64+
assert umm_g.CollectionReference.ShortName == "FOOBAR"
65+
assert umm_g.CollectionReference.Version == "10"
66+
67+
assert umm_g.to_dict() == {
68+
"AdditionalAttributes": [],
69+
"CollectionReference": {
70+
"ShortName": "FOOBAR",
71+
"Version": "10",
72+
},
73+
"GranuleUR": "SomeGranuleId",
74+
"MetadataSpecification": {
75+
"Name": "UMM-G",
76+
"URL": "https://cdn.earthdata.nasa.gov/umm/granule/v1.6.5",
77+
"Version": "1.6.5",
78+
},
79+
}

0 commit comments

Comments
 (0)