Skip to content

Commit 4116383

Browse files
authored
Merge pull request #199 from schwehr/scientific-extension
Add support for the scientific extension.
2 parents 37895cd + 9d72587 commit 4116383

File tree

3 files changed

+641
-1
lines changed

3 files changed

+641
-1
lines changed

pystac/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class STACError(Exception):
3636
import pystac.extensions.projection
3737
import pystac.extensions.sar
3838
import pystac.extensions.sat
39+
import pystac.extensions.scientific
3940
import pystac.extensions.single_file_stac
4041
import pystac.extensions.timestamps
4142
import pystac.extensions.version
@@ -45,7 +46,8 @@ 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,
48-
extensions.sat.SAT_EXTENSION_DEFINITION, extensions.single_file_stac.SFS_EXTENSION_DEFINITION,
49+
extensions.sat.SAT_EXTENSION_DEFINITION, extensions.scientific.SCIENTIFIC_EXTENSION_DEFINITION,
50+
extensions.single_file_stac.SFS_EXTENSION_DEFINITION,
4951
extensions.timestamps.TIMESTAMPS_EXTENSION_DEFINITION,
5052
extensions.version.VERSION_EXTENSION_DEFINITION, extensions.view.VIEW_EXTENSION_DEFINITION
5153
])

pystac/extensions/scientific.py

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
"""Implement the scientific extension.
2+
3+
https://github.com/radiantearth/stac-spec/tree/dev/extensions/scientific
4+
5+
For a description of Digital Object Identifiers (DOIs), see the DOI Handbook:
6+
7+
https://doi.org/10.1000/182
8+
"""
9+
10+
import copy
11+
from typing import Dict, List, Optional
12+
from urllib import parse
13+
14+
import pystac
15+
from pystac import Extensions
16+
from pystac.extensions import base
17+
18+
# STAC fields strings.
19+
PREFIX: str = 'sci:'
20+
DOI: str = PREFIX + 'doi'
21+
CITATION: str = PREFIX + 'citation'
22+
PUBLICATIONS: str = PREFIX + 'publications'
23+
24+
# Link type.
25+
CITE_AS: str = 'cite-as'
26+
27+
DOI_URL_BASE = 'https://doi.org/'
28+
29+
30+
def doi_to_url(doi: str) -> str:
31+
return DOI_URL_BASE + parse.quote(doi)
32+
33+
34+
class Publication:
35+
"""Helper for Publication entries."""
36+
def __init__(self, doi: str, citation: str) -> None:
37+
self.doi = doi
38+
self.citation = citation
39+
40+
def __eq__(self, other):
41+
if not isinstance(other, Publication):
42+
return NotImplemented
43+
44+
return self.doi == other.doi and self.citation == other.citation
45+
46+
def __repr__(self) -> str:
47+
return f'<Publication doi={self.doi} target={self.citation}>'
48+
49+
def to_dict(self) -> Dict[str, str]:
50+
return copy.deepcopy({'doi': self.doi, 'citation': self.citation})
51+
52+
@staticmethod
53+
def from_dict(d: Dict[str, str]):
54+
return Publication(d['doi'], d['citation'])
55+
56+
def get_link(self) -> pystac.Link:
57+
return pystac.Link(CITE_AS, doi_to_url(self.doi))
58+
59+
60+
def remove_link(links: List[pystac.Link], doi: str):
61+
url = doi_to_url(doi)
62+
for i, a_link in enumerate(links):
63+
if a_link.rel != CITE_AS:
64+
continue
65+
if a_link.target == url:
66+
del links[i]
67+
break
68+
69+
70+
class ScientificItemExt(base.ItemExtension):
71+
"""ScientificItemExt extends Item to add citations and DOIs to a STAC Item.
72+
73+
Args:
74+
item (Item): The item to be extended.
75+
76+
Attributes:
77+
item (Item): The item that is being extended.
78+
79+
Note:
80+
Using ScientificItemExt to directly wrap an item will add the 'scientific'
81+
extension ID to the item's stac_extensions.
82+
"""
83+
item: pystac.Item
84+
85+
def __init__(self, an_item: pystac.Item) -> None:
86+
self.item = an_item
87+
88+
def apply(self,
89+
doi: Optional[str] = None,
90+
citation: Optional[str] = None,
91+
publications: Optional[List[Publication]] = None) -> None:
92+
"""Applies scientific extension properties to the extended Item.
93+
94+
Args:
95+
doi (str): Optional DOI string for the item. Must not be a DOI link.
96+
citation (str): Optional human-readable reference.
97+
publications (List[Publication]): Optional list of relevant publications
98+
referencing and describing the data.
99+
"""
100+
if doi:
101+
self.doi = doi
102+
if citation:
103+
self.citation = citation
104+
if publications:
105+
self.publications = publications
106+
107+
@classmethod
108+
def from_item(cls, an_item: pystac.Item):
109+
return cls(an_item)
110+
111+
@classmethod
112+
def _object_links(cls) -> List:
113+
return []
114+
115+
@property
116+
def doi(self) -> str:
117+
"""Get or sets the DOI for the item.
118+
119+
Returns:
120+
str
121+
"""
122+
return self.item.properties.get(DOI)
123+
124+
@doi.setter
125+
def doi(self, v: str) -> None:
126+
if DOI in self.item.properties:
127+
if v == self.item.properties[DOI]:
128+
return
129+
remove_link(self.item.links, self.item.properties[DOI])
130+
131+
self.item.properties[DOI] = v
132+
url = doi_to_url(self.doi)
133+
self.item.add_link(pystac.Link(CITE_AS, url))
134+
135+
@property
136+
def citation(self) -> str:
137+
"""Get or sets the citation for the item.
138+
139+
Returns:
140+
str
141+
"""
142+
return self.item.properties.get(CITATION)
143+
144+
@citation.setter
145+
def citation(self, v: str) -> None:
146+
self.item.properties[CITATION] = v
147+
148+
@property
149+
def publications(self) -> List[Publication]:
150+
"""Get or sets the publication list for the item.
151+
152+
Returns:
153+
List of Publication instances.
154+
"""
155+
return [Publication.from_dict(pub) for pub in self.item.properties.get(PUBLICATIONS, [])]
156+
157+
@publications.setter
158+
def publications(self, v: List[Publication]) -> None:
159+
self.item.properties[PUBLICATIONS] = [pub.to_dict() for pub in v]
160+
for pub in v:
161+
self.item.add_link(pub.get_link())
162+
163+
# None for publication will clear all.
164+
def remove_publication(self, publication: Optional[Publication] = None) -> None:
165+
"""Removes publications from the item.
166+
167+
Args:
168+
publication (Publication): The specific publication to remove of None to remove all.
169+
"""
170+
if PUBLICATIONS not in self.item.properties:
171+
return
172+
173+
if not publication:
174+
for one_pub in self.item.ext.scientific.publications:
175+
remove_link(self.item.links, one_pub.doi)
176+
177+
del self.item.properties[PUBLICATIONS]
178+
return
179+
180+
# One publication and link to remove
181+
remove_link(self.item.links, publication.doi)
182+
to_remove = publication.to_dict()
183+
self.item.properties[PUBLICATIONS].remove(to_remove)
184+
185+
if not self.item.properties[PUBLICATIONS]:
186+
del self.item.properties[PUBLICATIONS]
187+
188+
189+
class ScientificCollectionExt(base.CollectionExtension):
190+
"""ScientificCollectionExt extends Collection to add citations and DOIs to a STAC Collection.
191+
192+
Args:
193+
collection (Collection): The collection to be extended.
194+
195+
Attributes:
196+
collection (Collection): The collection that is being extended.
197+
198+
Note:
199+
Using ScientificCollectionExt to directly wrap a collection will add the 'scientific'
200+
extension ID to the collection's stac_extensions.
201+
"""
202+
collection: pystac.Collection
203+
204+
def __init__(self, a_collection):
205+
self.collection = a_collection
206+
207+
def apply(self,
208+
doi: Optional[str] = None,
209+
citation: Optional[str] = None,
210+
publications: Optional[List[Publication]] = None):
211+
"""Applies scientific extension properties to the extended Collection.
212+
213+
Args:
214+
doi (str): Optional DOI string for the collection. Must not be a DOI link.
215+
citation (str): Optional human-readable reference.
216+
publications (List[Publication]): Optional list of relevant publications
217+
referencing and describing the data.
218+
"""
219+
if doi:
220+
self.doi = doi
221+
if citation:
222+
self.citation = citation
223+
if publications:
224+
self.publications = publications
225+
226+
@classmethod
227+
def from_collection(cls, a_collection: pystac.Collection):
228+
return cls(a_collection)
229+
230+
@classmethod
231+
def _object_links(cls) -> List:
232+
return []
233+
234+
@property
235+
def doi(self) -> str:
236+
"""Get or sets the DOI for the collection.
237+
238+
Returns:
239+
str
240+
"""
241+
return self.collection.extra_fields.get(DOI)
242+
243+
@doi.setter
244+
def doi(self, v: str) -> None:
245+
if DOI in self.collection.extra_fields:
246+
if v == self.collection.extra_fields[DOI]:
247+
return
248+
remove_link(self.collection.links, self.collection.extra_fields[DOI])
249+
self.collection.extra_fields[DOI] = v
250+
url = doi_to_url(self.doi)
251+
self.collection.add_link(pystac.Link(CITE_AS, url))
252+
253+
@property
254+
def citation(self) -> str:
255+
"""Get or sets the citation for the collection.
256+
257+
Returns:
258+
str
259+
"""
260+
return self.collection.extra_fields.get(CITATION)
261+
262+
@citation.setter
263+
def citation(self, v: str) -> None:
264+
self.collection.extra_fields[CITATION] = v
265+
266+
@property
267+
def publications(self) -> List[Publication]:
268+
"""Get or sets the publication list for the collection.
269+
270+
Returns:
271+
List of Publication instances.
272+
"""
273+
return [
274+
Publication.from_dict(p) for p in self.collection.extra_fields.get(PUBLICATIONS, [])
275+
]
276+
277+
@publications.setter
278+
def publications(self, v: List[Publication]) -> None:
279+
self.collection.extra_fields[PUBLICATIONS] = [pub.to_dict() for pub in v]
280+
for pub in v:
281+
self.collection.add_link(pub.get_link())
282+
283+
# None for publication will clear all.
284+
def remove_publication(self, publication: Optional[Publication] = None) -> None:
285+
"""Removes publications from the collection.
286+
287+
Args:
288+
publication (Publication): The specific publication to remove of None to remove all.
289+
"""
290+
if PUBLICATIONS not in self.collection.extra_fields:
291+
return
292+
293+
if not publication:
294+
for one_pub in self.collection.ext.scientific.publications:
295+
remove_link(self.collection.links, one_pub.doi)
296+
297+
del self.collection.extra_fields[PUBLICATIONS]
298+
return
299+
300+
# One publication and link to remove
301+
remove_link(self.collection.links, publication.doi)
302+
to_remove = publication.to_dict()
303+
self.collection.extra_fields[PUBLICATIONS].remove(to_remove)
304+
305+
if not self.collection.extra_fields[PUBLICATIONS]:
306+
del self.collection.extra_fields[PUBLICATIONS]
307+
308+
309+
SCIENTIFIC_EXTENSION_DEFINITION = base.ExtensionDefinition(Extensions.SCIENTIFIC, [
310+
base.ExtendedObject(pystac.Item, ScientificItemExt),
311+
base.ExtendedObject(pystac.Collection, ScientificCollectionExt)
312+
])

0 commit comments

Comments
 (0)