Skip to content

Commit 6688d07

Browse files
authored
Merge branch 'develop' into scientific-extension
2 parents af38dc1 + 37895cd commit 6688d07

File tree

5 files changed

+261
-1
lines changed

5 files changed

+261
-1
lines changed

docs/api.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,19 @@ SarItemExt
327327
:undoc-members:
328328
:show-inheritance:
329329

330+
SAT Extension
331+
-------------
332+
333+
Implements the `SAT Extension <https://github.com/radiantearth/stac-spec/tree/v1.0.0-beta.2/extensions/sat>`_.
334+
335+
SatItemExt
336+
~~~~~~~~~~~~~~~~~~~~~~~~
337+
338+
.. autoclass:: pystac.extensions.sar.SatItemExt
339+
:members:
340+
:undoc-members:
341+
:show-inheritance:
342+
330343
Single File STAC Extension
331344
--------------------------
332345

pystac/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class STACError(Exception):
3535
import pystac.extensions.pointcloud
3636
import pystac.extensions.projection
3737
import pystac.extensions.sar
38+
import pystac.extensions.sat
3839
import pystac.extensions.scientific
3940
import pystac.extensions.single_file_stac
4041
import pystac.extensions.timestamps
@@ -45,6 +46,7 @@ class STACError(Exception):
4546
extensions.eo.EO_EXTENSION_DEFINITION, extensions.label.LABEL_EXTENSION_DEFINITION,
4647
extensions.pointcloud.POINTCLOUD_EXTENSION_DEFINITION,
4748
extensions.projection.PROJECTION_EXTENSION_DEFINITION, extensions.sar.SAR_EXTENSION_DEFINITION,
49+
extensions.sat.SAT_EXTENSION_DEFINITION, extensions.single_file_stac.SFS_EXTENSION_DEFINITION,
4850
extensions.scientific.SCIENTIFIC_EXTENSION_DEFINITION,
4951
extensions.single_file_stac.SFS_EXTENSION_DEFINITION,
5052
extensions.timestamps.TIMESTAMPS_EXTENSION_DEFINITION,

pystac/extensions/sat.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""Implement the Satellite (SAT) Extension.
2+
3+
https://github.com/radiantearth/stac-spec/tree/dev/extensions/sat
4+
"""
5+
6+
import enum
7+
from typing import List, Optional
8+
9+
import pystac
10+
from pystac import Extensions
11+
from pystac import item
12+
from pystac.extensions import base
13+
14+
ORBIT_STATE: str = 'sat:orbit_state'
15+
RELATIVE_ORBIT: str = 'sat:relative_orbit'
16+
17+
18+
class OrbitState(enum.Enum):
19+
ASCENDING: str = 'ascending'
20+
DESCENDING: str = 'descending'
21+
GEOSTATIONARY: str = 'geostationary'
22+
23+
24+
class SatItemExt(base.ItemExtension):
25+
"""SatItemExt extends Item to add sat properties to a STAC Item.
26+
27+
Args:
28+
item (Item): The item to be extended.
29+
30+
Attributes:
31+
item (Item): The item that is being extended.
32+
33+
Note:
34+
Using SatItemExt to directly wrap an item will add the 'sat'
35+
extension ID to the item's stac_extensions.
36+
"""
37+
item: pystac.Item
38+
39+
def __init__(self, an_item: item.Item) -> None:
40+
self.item = an_item
41+
42+
def apply(self, orbit_state: Optional[OrbitState] = None, relative_orbit: Optional[str] = None):
43+
"""Applies ext extension properties to the extended Item.
44+
45+
Must specify at least one of orbit_state or relative_orbit.
46+
47+
Args:
48+
orbit_state (OrbitState): Optional state of the orbit. Either ascending or descending
49+
for polar orbiting satellites, or geostationary for geosynchronous satellites.
50+
relative_orbit (int): Optional non-negative integer of the orbit number at the time
51+
of acquisition.
52+
"""
53+
if orbit_state is None and relative_orbit is None:
54+
raise pystac.STACError('Must provide at least one of: orbit_state or relative_orbit')
55+
if orbit_state:
56+
self.orbit_state = orbit_state
57+
if relative_orbit:
58+
self.relative_orbit = relative_orbit
59+
60+
@classmethod
61+
def from_item(cls, an_item: item.Item):
62+
return cls(an_item)
63+
64+
@classmethod
65+
def _object_links(cls) -> List:
66+
return []
67+
68+
@property
69+
def orbit_state(self) -> Optional[OrbitState]:
70+
"""Get or sets an orbit state of the item.
71+
72+
Returns:
73+
OrbitState or None
74+
"""
75+
if ORBIT_STATE not in self.item.properties:
76+
return
77+
return OrbitState(self.item.properties.get(ORBIT_STATE))
78+
79+
@orbit_state.setter
80+
def orbit_state(self, v: Optional[OrbitState]) -> None:
81+
if v is None:
82+
if self.relative_orbit is None:
83+
raise pystac.STACError('Must set relative_orbit before clearing orbit_state')
84+
if ORBIT_STATE in self.item.properties:
85+
del self.item.properties[ORBIT_STATE]
86+
else:
87+
self.item.properties[ORBIT_STATE] = v.value
88+
89+
@property
90+
def relative_orbit(self) -> int:
91+
"""Get or sets a relative orbit number of the item.
92+
93+
Returns:
94+
int or None
95+
"""
96+
return self.item.properties.get(RELATIVE_ORBIT)
97+
98+
@relative_orbit.setter
99+
def relative_orbit(self, v: int) -> None:
100+
if v is None and self.orbit_state is None:
101+
raise pystac.STACError('Must set orbit_state before clearing relative_orbit')
102+
if v is None:
103+
if RELATIVE_ORBIT in self.item.properties:
104+
del self.item.properties[RELATIVE_ORBIT]
105+
return
106+
if v < 0:
107+
raise pystac.STACError(f'relative_orbit must be >= 0. Found {v}.')
108+
109+
self.item.properties[RELATIVE_ORBIT] = v
110+
111+
112+
SAT_EXTENSION_DEFINITION = base.ExtensionDefinition(Extensions.SAT, [
113+
base.ExtendedObject(pystac.Item, SatItemExt),
114+
])

pystac/layout.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class LayoutTemplate:
2424
2525
- The object's attributes
2626
- Keys in the ``properties`` attribute, if it exists.
27-
- Keys in the ``extra_fiels`` attribute, if it exists.
27+
- Keys in the ``extra_fields`` attribute, if it exists.
2828
2929
Some special keys can be used in template variables:
3030

tests/extensions/test_sat.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"""Tests for pystac.extensions.sat."""
2+
3+
import datetime
4+
import unittest
5+
6+
import pystac
7+
from pystac.extensions import sat
8+
9+
10+
def make_item() -> pystac.Item:
11+
"""Create basic test items that are only slightly different."""
12+
asset_id = 'an/asset'
13+
start = datetime.datetime(2018, 1, 2)
14+
item = pystac.Item(id=asset_id, geometry=None, bbox=None, datetime=start, properties={})
15+
16+
item.ext.enable(pystac.Extensions.SAT)
17+
return item
18+
19+
20+
class SatTest(unittest.TestCase):
21+
def setUp(self):
22+
super().setUp()
23+
self.item = make_item()
24+
25+
def test_stac_extensions(self):
26+
self.assertEqual([pystac.Extensions.SAT], self.item.stac_extensions)
27+
28+
def test_no_args_fails(self):
29+
with self.assertRaises(pystac.STACError):
30+
self.item.ext.sat.apply()
31+
32+
def test_orbit_state(self):
33+
orbit_state = sat.OrbitState.ASCENDING
34+
self.item.ext.sat.apply(orbit_state)
35+
self.assertEqual(orbit_state, self.item.ext.sat.orbit_state)
36+
self.assertNotIn(sat.RELATIVE_ORBIT, self.item.properties)
37+
self.assertFalse(self.item.ext.sat.relative_orbit)
38+
self.item.validate()
39+
40+
def test_relative_orbit(self):
41+
relative_orbit = 1234
42+
self.item.ext.sat.apply(None, relative_orbit)
43+
self.assertEqual(relative_orbit, self.item.ext.sat.relative_orbit)
44+
self.assertNotIn(sat.ORBIT_STATE, self.item.properties)
45+
self.assertFalse(self.item.ext.sat.orbit_state)
46+
self.item.validate()
47+
48+
def test_relative_orbit_no_negative(self):
49+
negative_relative_orbit = -2
50+
with self.assertRaises(pystac.STACError):
51+
self.item.ext.sat.apply(None, negative_relative_orbit)
52+
53+
self.item.ext.sat.apply(None, 123)
54+
with self.assertRaises(pystac.STACError):
55+
self.item.ext.sat.relative_orbit = negative_relative_orbit
56+
57+
def test_both(self):
58+
orbit_state = sat.OrbitState.DESCENDING
59+
relative_orbit = 4321
60+
self.item.ext.sat.apply(orbit_state, relative_orbit)
61+
self.assertEqual(orbit_state, self.item.ext.sat.orbit_state)
62+
self.assertEqual(relative_orbit, self.item.ext.sat.relative_orbit)
63+
self.item.validate()
64+
65+
def test_modify(self):
66+
self.item.ext.sat.apply(sat.OrbitState.DESCENDING, 999)
67+
68+
orbit_state = sat.OrbitState.GEOSTATIONARY
69+
self.item.ext.sat.orbit_state = orbit_state
70+
relative_orbit = 1000
71+
self.item.ext.sat.relative_orbit = relative_orbit
72+
self.assertEqual(orbit_state, self.item.ext.sat.orbit_state)
73+
self.assertEqual(relative_orbit, self.item.ext.sat.relative_orbit)
74+
self.item.validate()
75+
76+
def test_from_dict(self):
77+
orbit_state = sat.OrbitState.GEOSTATIONARY
78+
relative_orbit = 1001
79+
d = {
80+
'type': 'Feature',
81+
'stac_version': '1.0.0-beta.2',
82+
'id': 'an/asset',
83+
'properties': {
84+
'sat:orbit_state': orbit_state.value,
85+
'sat:relative_orbit': relative_orbit,
86+
'datetime': '2018-01-02T00:00:00Z'
87+
},
88+
'geometry': None,
89+
'links': [],
90+
'assets': {},
91+
'stac_extensions': ['sat']
92+
}
93+
item = pystac.Item.from_dict(d)
94+
self.assertEqual(orbit_state, item.ext.sat.orbit_state)
95+
self.assertEqual(relative_orbit, item.ext.sat.relative_orbit)
96+
97+
def test_to_from_dict(self):
98+
orbit_state = sat.OrbitState.GEOSTATIONARY
99+
relative_orbit = 1002
100+
self.item.ext.sat.apply(orbit_state, relative_orbit)
101+
d = self.item.to_dict()
102+
self.assertEqual(orbit_state.value, d['properties'][sat.ORBIT_STATE])
103+
self.assertEqual(relative_orbit, d['properties'][sat.RELATIVE_ORBIT])
104+
105+
item = pystac.Item.from_dict(d)
106+
self.assertEqual(orbit_state, item.ext.sat.orbit_state)
107+
self.assertEqual(relative_orbit, item.ext.sat.relative_orbit)
108+
109+
def test_clear_orbit_state(self):
110+
self.item.ext.sat.apply(sat.OrbitState.DESCENDING, 999)
111+
112+
self.item.ext.sat.orbit_state = None
113+
self.assertIsNone(self.item.ext.sat.orbit_state)
114+
self.item.validate()
115+
116+
def test_clear_relative_orbit(self):
117+
self.item.ext.sat.apply(sat.OrbitState.DESCENDING, 999)
118+
119+
self.item.ext.sat.relative_orbit = None
120+
self.assertIsNone(self.item.ext.sat.relative_orbit)
121+
self.item.validate()
122+
123+
def test_clear_orbit_state_fail(self):
124+
self.item.ext.sat.apply(sat.OrbitState.DESCENDING)
125+
with self.assertRaises(pystac.STACError):
126+
self.item.ext.sat.orbit_state = None
127+
128+
def test_clear_orbit_relative_orbit(self):
129+
self.item.ext.sat.apply(None, 1)
130+
with self.assertRaises(pystac.STACError):
131+
self.item.ext.sat.relative_orbit = None

0 commit comments

Comments
 (0)