-
Notifications
You must be signed in to change notification settings - Fork 123
/
Copy pathstac_object.py
644 lines (540 loc) · 22.7 KB
/
stac_object.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
from __future__ import annotations
from abc import ABC, abstractmethod
from html import escape
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Iterable,
List,
Optional,
Type,
TypeVar,
Union,
cast,
)
import pystac
from pystac import STACError
from pystac.html.jinja_env import get_jinja_env
from pystac.link import Link
from pystac.utils import (
StringEnum,
is_absolute_href,
make_absolute_href,
make_posix_style,
)
if TYPE_CHECKING:
from pystac.catalog import Catalog
S = TypeVar("S", bound="STACObject")
class STACObjectType(StringEnum):
CATALOG = "Catalog"
COLLECTION = "Collection"
ITEM = "Feature"
class STACObject(ABC):
"""A base class for other PySTAC classes that contains a variety of useful
methods for dealing with links, copying objects, accessing extensions, and reading
and writing files. You shouldn't use STACObject directly, but instead access this
functionality through the implementing classes.
"""
id: str
"""The ID of the STAC Object."""
links: List[Link]
"""A list of :class:`~pystac.Link` objects representing all links associated with
this STAC Object."""
stac_extensions: List[str]
"""A list of schema URIs for STAC Extensions implemented by this STAC Object."""
STAC_OBJECT_TYPE: STACObjectType
_allow_parent_to_override_href: bool = True
"""Private attribute for whether parent objects should override on normalization"""
def __init__(self, stac_extensions: List[str]) -> None:
self.links = []
self.stac_extensions = stac_extensions
def validate(self) -> List[Any]:
"""Validate this STACObject.
Returns a list of validation results, which depends on the validation
implementation. For JSON Schema validation, this will be a list
of schema URIs that were used during validation.
Raises:
STACValidationError
"""
import pystac.validation
return pystac.validation.validate(self)
def add_link(self, link: Link) -> None:
"""Add a link to this object's set of links.
Args:
link : The link to add.
"""
link.set_owner(self)
self.links.append(link)
def add_links(self, links: List[Link]) -> None:
"""Add links to this object's set of links.
Args:
links : The links to add.
"""
for link in links:
self.add_link(link)
def remove_links(self, rel: Union[str, pystac.RelType]) -> None:
"""Remove links to this object's set of links that match the given ``rel``.
Args:
rel : The :class:`~pystac.Link` ``rel`` to match on.
"""
self.links = [link for link in self.links if link.rel != rel]
def remove_hierarchical_links(self, add_canonical: bool = False) -> List[Link]:
"""Removes all hierarchical links from this object.
See :py:const:`pystac.link.HIERARCHICAL_LINKS` for a list of all
hierarchical links. If the object has a ``self`` href and
``add_canonical`` is True, a link with ``rel="canonical"`` is added.
Args:
add_canonical : If true, and this item has a ``self`` href, that
href is used to build a ``canonical`` link.
Returns:
List[Link]: All removed links
"""
keep = list()
self_href = self.get_self_href()
if add_canonical and self_href is not None:
keep.append(
Link("canonical", self_href, media_type=pystac.MediaType.GEOJSON)
)
remove = list()
for link in self.links:
if link.is_hierarchical():
remove.append(link)
else:
keep.append(link)
self.links = keep
return remove
def get_single_link(
self,
rel: Optional[Union[str, pystac.RelType]] = None,
media_type: Optional[Union[str, pystac.MediaType]] = None,
) -> Optional[Link]:
"""Get a single :class:`~pystac.Link` instance associated with this
object.
Args:
rel : If set, filter links such that only those
matching this relationship are returned.
media_type: If set, filter the links such that only
those matching media_type are returned
Returns:
Optional[:class:`~pystac.Link`]: First link that matches ``rel``
and/or ``media_type``, or else the first link associated with
this object.
"""
if rel is None and media_type is None:
return next(iter(self.links), None)
return next(
(
link
for link in self.links
if (rel is None or link.rel == rel)
and (media_type is None or link.media_type == media_type)
),
None,
)
def get_links(
self,
rel: Optional[Union[str, pystac.RelType]] = None,
media_type: Optional[Union[str, pystac.MediaType]] = None,
) -> List[Link]:
"""Gets the :class:`~pystac.Link` instances associated with this object.
Args:
rel : If set, filter links such that only those
matching this relationship are returned.
media_type: If set, filter the links such that only
those matching media_type are returned
Returns:
List[:class:`~pystac.Link`]: A list of links that match ``rel`` and/
or ``media_type`` if set, or else all links associated with this
object.
"""
if rel is None and media_type is None:
return self.links
else:
return [
link
for link in self.links
if (rel is None or link.rel == rel)
and (media_type is None or link.media_type == media_type)
]
def clear_links(self, rel: Optional[Union[str, pystac.RelType]] = None) -> None:
"""Clears all :class:`~pystac.Link` instances associated with this object.
Args:
rel : If set, only clear links that match this relationship.
"""
if rel is not None:
self.links = [link for link in self.links if link.rel != rel]
else:
self.links = []
def get_root_link(self) -> Optional[Link]:
"""Get the :class:`~pystac.Link` representing
the root for this object.
Returns:
:class:`~pystac.Link` or None: The root link for this object,
or ``None`` if no root link is set.
"""
return self.get_single_link(pystac.RelType.ROOT)
@property
def self_href(self) -> str:
"""Gets the absolute HREF that is represented by the ``rel == 'self'``
:class:`~pystac.Link`.
Raises:
ValueError: If the self_href is not set, this method will throw
a ValueError. Use get_self_href if there may not be an href set.
"""
result = self.get_self_href()
if result is None:
raise ValueError(f"{self} does not have a self_href set.")
return result
def get_self_href(self) -> Optional[str]:
"""Gets the absolute HREF that is represented by the ``rel == 'self'``
:class:`~pystac.Link`.
Returns:
str or None: The absolute HREF of this object, or ``None`` if
there is no self link defined.
Note:
A self link can exist for objects, even if the link is not read or
written to the JSON-serialized version of the object. Any object
read from :func:`STACObject.from_file <pystac.STACObject.from_file>` will
have the HREF the file was read from set as it's self HREF. All self
links have absolute (as opposed to relative) HREFs.
"""
self_link = self.get_single_link(pystac.RelType.SELF)
if self_link and self_link.has_target_href():
return self_link.get_target_str()
else:
return None
def set_self_href(self, href: Optional[str]) -> None:
"""Sets the absolute HREF that is represented by the ``rel == 'self'``
:class:`~pystac.Link`.
Args:
href : The absolute HREF of this object. If the given HREF
is not absolute, it will be transformed to an absolute
HREF based on the current working directory. If this is None
the call will clear the self HREF link.
"""
root_link = self.get_root_link()
if root_link is not None and root_link.is_resolved():
cast(pystac.Catalog, root_link.target)._resolved_objects.remove(self)
self.remove_links(pystac.RelType.SELF)
if href is not None:
self.add_link(Link.self_href(href))
if root_link is not None and root_link.is_resolved():
cast(pystac.Catalog, root_link.target)._resolved_objects.cache(self)
def get_root(self) -> Optional[Catalog]:
"""Get the :class:`~pystac.Catalog` or :class:`~pystac.Collection` to
the root for this object. The root is represented by a
:class:`~pystac.Link` with ``rel == 'root'``.
Returns:
Catalog, Collection, or None:
The root object for this object, or ``None`` if no root link is set.
"""
root_link = self.get_root_link()
if root_link:
if not root_link.is_resolved():
root_link.resolve_stac_object()
# Use set_root, so Catalogs can merge ResolvedObjectCache instances.
self.set_root(cast(pystac.Catalog, root_link.target))
return cast(pystac.Catalog, root_link.target)
else:
return None
def set_root(self, root: Optional[Catalog]) -> None:
"""Sets the root :class:`~pystac.Catalog` or :class:`~pystac.Collection`
for this object.
Args:
root : The root
object to set. Passing in None will clear the root.
"""
root_link_index = next(
iter(
[
i
for i, link in enumerate(self.links)
if link.rel == pystac.RelType.ROOT
]
),
None,
)
# Remove from old root resolution cache
if root_link_index is not None:
root_link = self.links[root_link_index]
if root_link.is_resolved():
cast(pystac.Catalog, root_link.target)._resolved_objects.remove(self)
if root is None:
self.remove_links(pystac.RelType.ROOT)
else:
new_root_link = Link.root(root)
if root_link_index is not None:
self.links[root_link_index] = new_root_link
new_root_link.set_owner(self)
else:
self.add_link(new_root_link)
root._resolved_objects.cache(self)
def get_parent(self) -> Optional[Catalog]:
"""Get the :class:`~pystac.Catalog` or :class:`~pystac.Collection` to
the parent for this object. The root is represented by a
:class:`~pystac.Link` with ``rel == 'parent'``.
Returns:
Catalog, Collection, or None:
The parent object for this object,
or ``None`` if no root link is set.
"""
parent_link = self.get_single_link(pystac.RelType.PARENT)
if parent_link:
return cast(pystac.Catalog, parent_link.resolve_stac_object().target)
else:
return None
def set_parent(self, parent: Optional[Catalog]) -> None:
"""Sets the parent :class:`~pystac.Catalog` or :class:`~pystac.Collection`
for this object.
Args:
parent : The parent
object to set. Passing in None will clear the parent.
"""
self.remove_links(pystac.RelType.PARENT)
if parent is not None:
self.add_link(Link.parent(parent))
def get_stac_objects(
self,
rel: Union[str, pystac.RelType],
typ: Optional[Type[STACObject]] = None,
modify_links: Optional[Callable[[List[Link]], List[Link]]] = None,
) -> Iterable[STACObject]:
"""Gets the :class:`~pystac.STACObject` instances that are linked to
by links with their ``rel`` property matching the passed in argument.
Args:
rel : The relation to match each :class:`~pystac.Link`'s
``rel`` property against.
typ : If not ``None``, objects will only be yielded if they are instances of
``typ``.
modify_links : A function that modifies the list of links before they are
iterated over. For instance, this option can be used to sort the list
so that links matching a particular pattern are earlier in the iterator.
Returns:
Iterable[STACObjects]: A possibly empty iterable of STACObjects that are
connected to this object through links with the given ``rel`` and are of
type ``typ`` (if given).
"""
links = self.links[:]
if modify_links:
links = modify_links(links)
for i in range(0, len(links)):
link = links[i]
if link.rel == rel:
link.resolve_stac_object(root=self.get_root())
if typ is None or isinstance(link.target, typ):
yield cast(STACObject, link.target)
def save_object(
self,
include_self_link: bool = True,
dest_href: Optional[str] = None,
stac_io: Optional[pystac.StacIO] = None,
) -> None:
"""Saves this STAC Object to it's 'self' HREF.
Args:
include_self_link : If this is true, include the 'self' link with
this object. Otherwise, leave out the self link.
dest_href : Optional HREF to save the file to. If None, the object
will be saved to the object's self href.
stac_io: Optional instance of StacIO to use. If not provided, will use the
instance set on the object's root if available, otherwise will use the
default instance.
Raises:
:class:`~pystac.STACError`: If no self href is set, this error will be
raised.
Note:
When to include a self link is described in the :stac-spec:`Use of Links
section of the STAC best practices document
<best-practices.md#use-of-links>`
"""
if stac_io is None:
root = self.get_root()
if root is not None:
root_stac_io = root._stac_io
if root_stac_io is not None:
stac_io = root_stac_io
if stac_io is None:
stac_io = pystac.StacIO.default()
if dest_href is None:
self_href = self.get_self_href()
if self_href is None:
raise STACError(
"Self HREF must be set before saving without an explicit dest_href."
)
dest_href = self_href
stac_io.save_json(dest_href, self.to_dict(include_self_link=include_self_link))
def full_copy(
self,
root: Optional[Catalog] = None,
parent: Optional[Catalog] = None,
) -> STACObject:
"""Create a full copy of this STAC object and any stac objects linked to by
this object.
Args:
root : Optional root to set as the root of the copied object,
and any other copies that are contained by this object.
parent : Optional parent to set as the parent of the copy
of this object.
Returns:
STACObject: A full copy of this object, as well as any objects this object
links to.
"""
clone = self.clone()
if root is None and isinstance(clone, pystac.Catalog):
root = clone
# Set the root of the STAC Object using the base class,
# avoiding child class overrides
# extra logic which can be incompatible with the full copy.
STACObject.set_root(clone, cast(pystac.Catalog, root))
if parent:
clone.set_parent(parent)
link_rels = set(self._object_links())
for link in clone.links:
if link.rel in link_rels:
link.resolve_stac_object()
target = cast(STACObject, link.target)
if root is not None and target in root._resolved_objects:
cached_target = root._resolved_objects.get(target)
assert cached_target is not None
target = cached_target
else:
target_parent = None
if link.rel in [
pystac.RelType.CHILD,
pystac.RelType.ITEM,
] and isinstance(clone, pystac.Catalog):
target_parent = clone
copied_target = target.full_copy(root=root, parent=target_parent)
if root is not None:
root._resolved_objects.cache(copied_target)
target = copied_target
if link.rel in [pystac.RelType.CHILD, pystac.RelType.ITEM]:
target.set_root(root)
if isinstance(clone, pystac.Catalog):
target.set_parent(clone)
link.target = target
return clone
def resolve_links(self) -> None:
"""Ensure all STACObjects linked to by this STACObject are
resolved. This is important for operations such as changing
HREFs.
This method mutates the entire catalog tree.
"""
link_rels = set(self._object_links()) | {
pystac.RelType.ROOT,
pystac.RelType.PARENT,
}
for link in self.links:
if link.rel in link_rels:
if not link.is_resolved():
link.resolve_stac_object(root=self.get_root())
@abstractmethod
def _object_links(self) -> List[str]:
"""Inherited classes return a list of link 'rel' types that represent
STACObjects linked to by this object (not including root, parent or self).
This can include optional relations (which may not be present).
"""
raise NotImplementedError
@abstractmethod
def to_dict(
self, include_self_link: bool = True, transform_hrefs: bool = True
) -> Dict[str, Any]:
"""Returns this object as a dictionary.
Args:
include_self_link : If True, the dict will contain a self link
to this object. If False, the self link will be omitted.
transform_hrefs: If True, transform the HREF of hierarchical links
based on the type of catalog this object belongs to (if any).
I.e. if this object belongs to a root catalog that is
RELATIVE_PUBLISHED or SELF_CONTAINED,
hierarchical link HREFs will be transformed to be relative to the
catalog root.
dict: A serialization of the object.
"""
raise NotImplementedError
def _repr_html_(self) -> str:
jinja_env = get_jinja_env()
if jinja_env:
template = jinja_env.get_template("JSON.jinja2")
return str(template.render(dict=self.to_dict(transform_hrefs=False)))
else:
return escape(repr(self))
@abstractmethod
def clone(self) -> STACObject:
"""Clones this object.
Cloning an object will make a copy of all properties and links of the object;
however, it will not make copies of the targets of links (i.e. it is not a
deep copy). To copy a STACObject fully, with all linked elements also copied,
use :func:`STACObject.full_copy <pystac.STACObject.full_copy>`.
Returns:
STACObject: The clone of this object.
"""
raise NotImplementedError
@classmethod
def from_file(
cls: Type[S], href: str, stac_io: Optional[pystac.StacIO] = None
) -> S:
"""Reads a STACObject implementation from a file.
Args:
href : The HREF to read the object from.
stac_io: Optional instance of StacIO to use. If not provided, will use the
default instance.
Returns:
The specific STACObject implementation class that is represented
by the JSON read from the file located at HREF.
"""
if cls == STACObject:
return cast(S, pystac.read_file(href))
href = make_posix_style(href)
if stac_io is None:
stac_io = pystac.StacIO.default()
if not is_absolute_href(href):
href = make_absolute_href(href)
d = stac_io.read_json(href)
o = cls.from_dict(d, href=href, migrate=True, preserve_dict=False)
# If this is a root catalog, set the root to the catalog instance.
root_link = o.get_root_link()
if root_link is not None:
if not root_link.is_resolved():
if root_link.get_absolute_href() == href:
o.set_root(cast(pystac.Catalog, o))
return o
@classmethod
@abstractmethod
def from_dict(
cls: Type[S],
d: Dict[str, Any],
href: Optional[str] = None,
root: Optional[Catalog] = None,
migrate: bool = False,
preserve_dict: bool = True,
) -> S:
"""Parses this STACObject from the passed in dictionary.
Args:
d : The dict to parse.
href : Optional href that is the file location of the object being
parsed.
root : Optional root catalog for this object.
If provided, the root of the returned STACObject will be set
to this parameter.
migrate: Use True if this dict represents JSON from an older STAC object,
so that migrations are run against it.
preserve_dict: If False, the dict parameter ``d`` may be modified
during this method call. Otherwise the dict is not mutated.
Defaults to True, which results results in a deepcopy of the
parameter. Set to False when possible to avoid the performance
hit of a deepcopy.
Returns:
STACObject: The STACObject parsed from this dict.
"""
raise NotImplementedError
@classmethod
@abstractmethod
def matches_object_type(cls, d: Dict[str, Any]) -> bool:
"""Returns a boolean indicating whether the given dictionary represents a valid
instance of this :class:`~STACObject` sub-class.
Args:
d : A dictionary to identify
"""
raise NotImplementedError