pystac-ext-eo 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,709 @@
1
+ """Implements the :stac-ext:`Electro-Optical Extension <eo>`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from typing import (
7
+ Any,
8
+ Generic,
9
+ Literal,
10
+ TypeVar,
11
+ cast,
12
+ )
13
+
14
+ import pystac
15
+ from pystac.extensions import projection, view
16
+ from pystac.extensions.base import (
17
+ ExtensionManagementMixin,
18
+ PropertiesExtension,
19
+ SummariesExtension,
20
+ )
21
+ from pystac.extensions.hooks import ExtensionHooks
22
+ from pystac.serialization.identify import STACJSONDescription, STACVersionID
23
+ from pystac.summaries import RangeSummary
24
+ from pystac.utils import get_required, map_opt
25
+
26
+ #: Generalized version of :class:`~pystac.Item`, :class:`~pystac.Asset`,
27
+ #: pr :class:`~pystac.ItemAssetDefinition`
28
+ T = TypeVar("T", pystac.Item, pystac.Asset, pystac.ItemAssetDefinition)
29
+
30
+ SCHEMA_URI: str = "https://stac-extensions.github.io/eo/v1.1.0/schema.json"
31
+ SCHEMA_URIS: list[str] = [
32
+ "https://stac-extensions.github.io/eo/v1.0.0/schema.json",
33
+ SCHEMA_URI,
34
+ ]
35
+ PREFIX: str = "eo:"
36
+
37
+ # Field names
38
+ BANDS_PROP: str = PREFIX + "bands"
39
+ CLOUD_COVER_PROP: str = PREFIX + "cloud_cover"
40
+ SNOW_COVER_PROP: str = PREFIX + "snow_cover"
41
+
42
+
43
+ def validated_percentage(v: float | None) -> float | None:
44
+ if v is not None and not isinstance(v, (float, int)) or isinstance(v, bool):
45
+ raise ValueError(f"Invalid percentage: {v} must be number")
46
+ if v is not None and not 0 <= v <= 100:
47
+ raise ValueError(f"Invalid percentage: {v} must be between 0 and 100")
48
+ return v
49
+
50
+
51
+ class Band:
52
+ """Represents Band information attached to an Item that implements the eo extension.
53
+
54
+ Use :meth:`Band.create` to create a new Band.
55
+ """
56
+
57
+ properties: dict[str, Any]
58
+
59
+ def __init__(self, properties: dict[str, Any]) -> None:
60
+ self.properties = properties
61
+
62
+ def apply(
63
+ self,
64
+ name: str,
65
+ common_name: str | None = None,
66
+ description: str | None = None,
67
+ center_wavelength: float | None = None,
68
+ full_width_half_max: float | None = None,
69
+ solar_illumination: float | None = None,
70
+ ) -> None:
71
+ """
72
+ Sets the properties for this Band.
73
+
74
+ Args:
75
+ name : The name of the band (e.g., "B01", "B02", "B1", "B5", "QA").
76
+ common_name : The name commonly used to refer to the band to make it
77
+ easier to search for bands across instruments. See the :stac-ext:`list
78
+ of accepted common names <eo#common-band-names>`.
79
+ description : Description to fully explain the band.
80
+ center_wavelength : The center wavelength of the band, in micrometers (μm).
81
+ full_width_half_max : Full width at half maximum (FWHM). The width of the
82
+ band, as measured at half the maximum transmission, in micrometers (μm).
83
+ solar_illumination: The solar illumination of the band,
84
+ as measured at half the maximum transmission, in W/m2/micrometers.
85
+ """
86
+ self.name = name
87
+ self.common_name = common_name
88
+ self.description = description
89
+ self.center_wavelength = center_wavelength
90
+ self.full_width_half_max = full_width_half_max
91
+ self.solar_illumination = solar_illumination
92
+
93
+ @classmethod
94
+ def create(
95
+ cls,
96
+ name: str,
97
+ common_name: str | None = None,
98
+ description: str | None = None,
99
+ center_wavelength: float | None = None,
100
+ full_width_half_max: float | None = None,
101
+ solar_illumination: float | None = None,
102
+ ) -> Band:
103
+ """
104
+ Creates a new band.
105
+
106
+ Args:
107
+ name : The name of the band (e.g., "B01", "B02", "B1", "B5", "QA").
108
+ common_name : The name commonly used to refer to the band to make it easier
109
+ to search for bands across instruments. See the :stac-ext:`list of
110
+ accepted common names <eo#common-band-names>`.
111
+ description : Description to fully explain the band.
112
+ center_wavelength : The center wavelength of the band, in micrometers (μm).
113
+ full_width_half_max : Full width at half maximum (FWHM). The width of the
114
+ band, as measured at half the maximum transmission, in micrometers (μm).
115
+ solar_illumination: The solar illumination of the band,
116
+ as measured at half the maximum transmission, in W/m2/micrometers.
117
+ """
118
+ b = cls({})
119
+ b.apply(
120
+ name=name,
121
+ common_name=common_name,
122
+ description=description,
123
+ center_wavelength=center_wavelength,
124
+ full_width_half_max=full_width_half_max,
125
+ solar_illumination=solar_illumination,
126
+ )
127
+ return b
128
+
129
+ @property
130
+ def name(self) -> str:
131
+ """Get or sets the name of the band (e.g., "B01", "B02", "B1", "B5", "QA").
132
+
133
+ Returns:
134
+ str
135
+ """
136
+ return cast(str, get_required(self.properties.get("name"), self, "name"))
137
+
138
+ @name.setter
139
+ def name(self, v: str) -> None:
140
+ self.properties["name"] = v
141
+
142
+ @property
143
+ def common_name(self) -> str | None:
144
+ """Get or sets the name commonly used to refer to the band to make it easier
145
+ to search for bands across instruments. See the :stac-ext:`list of accepted
146
+ common names <eo#common-band-names>`.
147
+
148
+ Returns:
149
+ Optional[str]
150
+ """
151
+ return self.properties.get("common_name")
152
+
153
+ @common_name.setter
154
+ def common_name(self, v: str | None) -> None:
155
+ if v is not None:
156
+ self.properties["common_name"] = v
157
+ else:
158
+ self.properties.pop("common_name", None)
159
+
160
+ @property
161
+ def description(self) -> str | None:
162
+ """Get or sets the description to fully explain the band. CommonMark 0.29
163
+ syntax MAY be used for rich text representation.
164
+
165
+ Returns:
166
+ str
167
+ """
168
+ return self.properties.get("description")
169
+
170
+ @description.setter
171
+ def description(self, v: str | None) -> None:
172
+ if v is not None:
173
+ self.properties["description"] = v
174
+ else:
175
+ self.properties.pop("description", None)
176
+
177
+ @property
178
+ def center_wavelength(self) -> float | None:
179
+ """Get or sets the center wavelength of the band, in micrometers (μm).
180
+
181
+ Returns:
182
+ float
183
+ """
184
+ return self.properties.get("center_wavelength")
185
+
186
+ @center_wavelength.setter
187
+ def center_wavelength(self, v: float | None) -> None:
188
+ if v is not None:
189
+ self.properties["center_wavelength"] = v
190
+ else:
191
+ self.properties.pop("center_wavelength", None)
192
+
193
+ @property
194
+ def full_width_half_max(self) -> float | None:
195
+ """Get or sets the full width at half maximum (FWHM). The width of the band,
196
+ as measured at half the maximum transmission, in micrometers (μm).
197
+
198
+ Returns:
199
+ [float]
200
+ """
201
+ return self.properties.get("full_width_half_max")
202
+
203
+ @full_width_half_max.setter
204
+ def full_width_half_max(self, v: float | None) -> None:
205
+ if v is not None:
206
+ self.properties["full_width_half_max"] = v
207
+ else:
208
+ self.properties.pop("full_width_half_max", None)
209
+
210
+ @property
211
+ def solar_illumination(self) -> float | None:
212
+ """Get or sets the solar illumination of the band,
213
+ as measured at half the maximum transmission, in W/m2/micrometers.
214
+
215
+ Returns:
216
+ [float]
217
+ """
218
+ return self.properties.get("solar_illumination")
219
+
220
+ @solar_illumination.setter
221
+ def solar_illumination(self, v: float | None) -> None:
222
+ if v is not None:
223
+ self.properties["solar_illumination"] = v
224
+ else:
225
+ self.properties.pop("solar_illumination", None)
226
+
227
+ def __repr__(self) -> str:
228
+ return f"<Band name={self.properties.get('name')}>"
229
+
230
+ def to_dict(self) -> dict[str, Any]:
231
+ """Returns this band as a dictionary.
232
+
233
+ Returns:
234
+ dict: The serialization of this Band.
235
+ """
236
+ return self.properties
237
+
238
+ @staticmethod
239
+ def band_range(common_name: str) -> tuple[float, float] | None:
240
+ """Gets the band range for a common band name.
241
+
242
+ Args:
243
+ common_name : The common band name. Must be one of the :stac-ext:`list of
244
+ accepted common names <eo#common-band-names>`.
245
+
246
+ Returns:
247
+ Tuple[float, float] or None: The band range for this name as (min, max), or
248
+ None if this is not a recognized common name.
249
+ """
250
+ name_to_range = {
251
+ "coastal": (0.40, 0.45),
252
+ "blue": (0.45, 0.50),
253
+ "green": (0.50, 0.60),
254
+ "red": (0.60, 0.70),
255
+ "yellow": (0.58, 0.62),
256
+ "pan": (0.50, 0.70),
257
+ "rededge": (0.70, 0.75),
258
+ "nir": (0.75, 1.00),
259
+ "nir08": (0.75, 0.90),
260
+ "nir09": (0.85, 1.05),
261
+ "cirrus": (1.35, 1.40),
262
+ "swir16": (1.55, 1.75),
263
+ "swir22": (2.10, 2.30),
264
+ "lwir": (10.5, 12.5),
265
+ "lwir11": (10.5, 11.5),
266
+ "lwir12": (11.5, 12.5),
267
+ }
268
+
269
+ return name_to_range.get(common_name)
270
+
271
+ @staticmethod
272
+ def band_description(common_name: str) -> str | None:
273
+ """Returns a description of the band for one with a common name.
274
+
275
+ Args:
276
+ common_name : The common band name. Must be one of the :stac-ext:`list of
277
+ accepted common names <eo#common-band-names>`.
278
+
279
+ Returns:
280
+ str or None: If a recognized common name, returns a description including
281
+ the band range. Otherwise, returns None.
282
+ """
283
+ r = Band.band_range(common_name)
284
+ if r is not None:
285
+ return f"Common name: {common_name}, Range: {r[0]} to {r[1]}"
286
+ return None
287
+
288
+
289
+ class EOExtension(
290
+ Generic[T],
291
+ PropertiesExtension,
292
+ ExtensionManagementMixin[pystac.Item | pystac.Collection],
293
+ ):
294
+ """An abstract class that can be used to extend the properties of an
295
+ :class:`~pystac.Item` or :class:`~pystac.Asset` with properties from the
296
+ :stac-ext:`Electro-Optical Extension <eo>`. This class is generic over the type of
297
+ STAC Object to be extended (e.g. :class:`~pystac.Item`,
298
+ :class:`~pystac.Asset`).
299
+
300
+ To create a concrete instance of :class:`EOExtension`, use the
301
+ :meth:`EOExtension.ext` method. For example:
302
+
303
+ .. code-block:: python
304
+
305
+ >>> item: pystac.Item = ...
306
+ >>> eo_ext = EOExtension.ext(item)
307
+ """
308
+
309
+ name: Literal["eo"] = "eo"
310
+
311
+ def apply(
312
+ self,
313
+ bands: list[Band] | None = None,
314
+ cloud_cover: float | None = None,
315
+ snow_cover: float | None = None,
316
+ ) -> None:
317
+ """Applies Electro-Optical Extension properties to the extended
318
+ :class:`~pystac.Item` or :class:`~pystac.Asset`.
319
+
320
+ Args:
321
+ bands : A list of available bands where each item is a :class:`~Band`
322
+ object. If given, requires at least one band.
323
+ cloud_cover : The estimate of cloud cover as a percentage
324
+ (0-100) of the entire scene. If not available the field should not
325
+ be provided.
326
+ snow_cover : The estimate of snow cover as a percentage
327
+ (0-100) of the entire scene. If not available the field should not
328
+ be provided.
329
+ """
330
+ self.bands = bands
331
+ self.cloud_cover = validated_percentage(cloud_cover)
332
+ self.snow_cover = validated_percentage(snow_cover)
333
+
334
+ @property
335
+ def bands(self) -> list[Band] | None:
336
+ """Gets or sets a list of available bands where each item is a :class:`~Band`
337
+ object (or ``None`` if no bands have been set). If not available the field
338
+ should not be provided.
339
+ """
340
+ return self._get_bands()
341
+
342
+ @bands.setter
343
+ def bands(self, v: list[Band] | None) -> None:
344
+ self._set_property(
345
+ BANDS_PROP, map_opt(lambda bands: [b.to_dict() for b in bands], v)
346
+ )
347
+
348
+ def _get_bands(self) -> list[Band] | None:
349
+ return map_opt(
350
+ lambda bands: [Band(b) for b in bands],
351
+ self._get_property(BANDS_PROP, list[dict[str, Any]]),
352
+ )
353
+
354
+ @property
355
+ def cloud_cover(self) -> float | None:
356
+ """Get or sets the estimate of cloud cover as a percentage
357
+ (0-100) of the entire scene. If not available the field should not be provided.
358
+
359
+ Returns:
360
+ float or None
361
+ """
362
+ return self._get_property(CLOUD_COVER_PROP, float)
363
+
364
+ @cloud_cover.setter
365
+ def cloud_cover(self, v: float | None) -> None:
366
+ self._set_property(CLOUD_COVER_PROP, validated_percentage(v), pop_if_none=True)
367
+
368
+ @property
369
+ def snow_cover(self) -> float | None:
370
+ """Get or sets the estimate of snow cover as a percentage
371
+ (0-100) of the entire scene. If not available the field should not be provided.
372
+
373
+ Returns:
374
+ float or None
375
+ """
376
+ return self._get_property(SNOW_COVER_PROP, float)
377
+
378
+ @snow_cover.setter
379
+ def snow_cover(self, v: float | None) -> None:
380
+ self._set_property(SNOW_COVER_PROP, validated_percentage(v), pop_if_none=True)
381
+
382
+ @classmethod
383
+ def get_schema_uri(cls) -> str:
384
+ return SCHEMA_URI
385
+
386
+ @classmethod
387
+ def get_schema_uris(cls) -> list[str]:
388
+ import warnings
389
+
390
+ warnings.warn(
391
+ "get_schema_uris is deprecated and will be removed in v2",
392
+ DeprecationWarning,
393
+ )
394
+ return SCHEMA_URIS
395
+
396
+ @classmethod
397
+ def ext(cls, obj: T, add_if_missing: bool = False) -> EOExtension[T]:
398
+ """Extends the given STAC Object with properties from the
399
+ :stac-ext:`Electro-Optical Extension <eo>`.
400
+
401
+ This extension can be applied to instances of :class:`~pystac.Item` or
402
+ :class:`~pystac.Asset`.
403
+
404
+ Raises:
405
+
406
+ pystac.ExtensionTypeError : If an invalid object type is passed.
407
+ """
408
+ if isinstance(obj, pystac.Item):
409
+ cls.ensure_has_extension(obj, add_if_missing)
410
+ return cast(EOExtension[T], ItemEOExtension(obj))
411
+ elif isinstance(obj, pystac.Asset):
412
+ cls.ensure_owner_has_extension(obj, add_if_missing)
413
+ return cast(EOExtension[T], AssetEOExtension(obj))
414
+ elif isinstance(obj, pystac.ItemAssetDefinition):
415
+ cls.ensure_owner_has_extension(obj, add_if_missing)
416
+ return cast(EOExtension[T], ItemAssetsEOExtension(obj))
417
+ else:
418
+ raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
419
+
420
+ @classmethod
421
+ def summaries(
422
+ cls, obj: pystac.Collection, add_if_missing: bool = False
423
+ ) -> SummariesEOExtension:
424
+ """Returns the extended summaries object for the given collection."""
425
+ cls.ensure_has_extension(obj, add_if_missing)
426
+ return SummariesEOExtension(obj)
427
+
428
+
429
+ class ItemEOExtension(EOExtension[pystac.Item]):
430
+ """A concrete implementation of :class:`EOExtension` on an :class:`~pystac.Item`
431
+ that extends the properties of the Item to include properties defined in the
432
+ :stac-ext:`Electro-Optical Extension <eo>`.
433
+
434
+ This class should generally not be instantiated directly. Instead, call
435
+ :meth:`EOExtension.ext` on an :class:`~pystac.Item` to extend it.
436
+ """
437
+
438
+ item: pystac.Item
439
+ """The :class:`~pystac.Item` being extended."""
440
+
441
+ properties: dict[str, Any]
442
+ """The :class:`~pystac.Item` properties, including extension properties."""
443
+
444
+ def __init__(self, item: pystac.Item):
445
+ self.item = item
446
+ self.properties = item.properties
447
+
448
+ def _get_bands(self) -> list[Band] | None:
449
+ """Get or sets a list of :class:`~pystac.Band` objects that represent
450
+ the available bands.
451
+ """
452
+ bands = self._get_property(BANDS_PROP, list[dict[str, Any]])
453
+
454
+ # get assets with eo:bands even if not in item
455
+ if bands is None:
456
+ asset_bands: list[dict[str, Any]] = []
457
+ for _, value in self.item.get_assets().items():
458
+ if BANDS_PROP in value.extra_fields:
459
+ asset_bands.extend(
460
+ cast(list[dict[str, Any]], value.extra_fields.get(BANDS_PROP))
461
+ )
462
+ if any(asset_bands):
463
+ bands = asset_bands
464
+
465
+ if bands is not None:
466
+ return [Band(b) for b in bands]
467
+ return None
468
+
469
+ def get_assets(
470
+ self,
471
+ name: str | None = None,
472
+ common_name: str | None = None,
473
+ ) -> dict[str, pystac.Asset]:
474
+ """Get the item's assets where eo:bands are defined.
475
+
476
+ Args:
477
+ name: If set, filter the assets such that only those with a
478
+ matching ``eo:band.name`` are returned.
479
+ common_name: If set, filter the assets such that only those with a matching
480
+ ``eo:band.common_name`` are returned.
481
+
482
+ Returns:
483
+ Dict[str, Asset]: A dictionary of assets that match ``name``
484
+ and/or ``common_name`` if set or else all of this item's assets were
485
+ eo:bands are defined.
486
+ """
487
+ kwargs = {"name": name, "common_name": common_name}
488
+ return {
489
+ key: asset
490
+ for key, asset in self.item.get_assets().items()
491
+ if BANDS_PROP in asset.extra_fields
492
+ and all(
493
+ v is None or any(v == b.get(k) for b in asset.extra_fields[BANDS_PROP])
494
+ for k, v in kwargs.items()
495
+ )
496
+ }
497
+
498
+ def __repr__(self) -> str:
499
+ return f"<ItemEOExtension Item id={self.item.id}>"
500
+
501
+
502
+ class AssetEOExtension(EOExtension[pystac.Asset]):
503
+ """A concrete implementation of :class:`EOExtension` on an :class:`~pystac.Asset`
504
+ that extends the Asset fields to include properties defined in the
505
+ :stac-ext:`Electro-Optical Extension <eo>`.
506
+
507
+ This class should generally not be instantiated directly. Instead, call
508
+ :meth:`EOExtension.ext` on an :class:`~pystac.Asset` to extend it.
509
+ """
510
+
511
+ asset_href: str
512
+ """The ``href`` value of the :class:`~pystac.Asset` being extended."""
513
+
514
+ properties: dict[str, Any]
515
+ """The :class:`~pystac.Asset` fields, including extension properties."""
516
+
517
+ additional_read_properties: Iterable[dict[str, Any]] | None = None
518
+ """If present, this will be a list containing 1 dictionary representing the
519
+ properties of the owning :class:`~pystac.Item`."""
520
+
521
+ def _get_bands(self) -> list[Band] | None:
522
+ if BANDS_PROP not in self.properties:
523
+ return None
524
+ return list(
525
+ map(
526
+ lambda band: Band(band),
527
+ cast(list[dict[str, Any]], self.properties.get(BANDS_PROP)),
528
+ )
529
+ )
530
+
531
+ def __init__(self, asset: pystac.Asset):
532
+ self.asset_href = asset.href
533
+ self.properties = asset.extra_fields
534
+ if asset.owner and isinstance(asset.owner, pystac.Item):
535
+ self.additional_read_properties = [asset.owner.properties]
536
+
537
+ def __repr__(self) -> str:
538
+ return f"<AssetEOExtension Asset href={self.asset_href}>"
539
+
540
+
541
+ class ItemAssetsEOExtension(EOExtension[pystac.ItemAssetDefinition]):
542
+ properties: dict[str, Any]
543
+ asset_defn: pystac.ItemAssetDefinition
544
+
545
+ def _get_bands(self) -> list[Band] | None:
546
+ if BANDS_PROP not in self.properties:
547
+ return None
548
+ return list(
549
+ map(
550
+ lambda band: Band(band),
551
+ cast(list[dict[str, Any]], self.properties.get(BANDS_PROP)),
552
+ )
553
+ )
554
+
555
+ def __init__(self, item_asset: pystac.ItemAssetDefinition):
556
+ self.asset_defn = item_asset
557
+ self.properties = item_asset.properties
558
+
559
+
560
+ class SummariesEOExtension(SummariesExtension):
561
+ """A concrete implementation of :class:`~pystac.extensions.base.SummariesExtension`
562
+ that extends the ``summaries`` field of a :class:`~pystac.Collection` to include
563
+ properties defined in the :stac-ext:`Electro-Optical Extension <eo>`.
564
+ """
565
+
566
+ @property
567
+ def bands(self) -> list[Band] | None:
568
+ """Get or sets the summary of :attr:`EOExtension.bands` values
569
+ for this Collection.
570
+ """
571
+
572
+ return map_opt(
573
+ lambda bands: [Band(b) for b in bands],
574
+ self.summaries.get_list(BANDS_PROP),
575
+ )
576
+
577
+ @bands.setter
578
+ def bands(self, v: list[Band] | None) -> None:
579
+ self._set_summary(BANDS_PROP, map_opt(lambda x: [b.to_dict() for b in x], v))
580
+
581
+ @property
582
+ def cloud_cover(self) -> RangeSummary[float] | None:
583
+ """Get or sets the summary of :attr:`EOExtension.cloud_cover` values
584
+ for this Collection.
585
+ """
586
+ return self.summaries.get_range(CLOUD_COVER_PROP)
587
+
588
+ @cloud_cover.setter
589
+ def cloud_cover(self, v: RangeSummary[float] | None) -> None:
590
+ self._set_summary(CLOUD_COVER_PROP, v)
591
+
592
+ @property
593
+ def snow_cover(self) -> RangeSummary[float] | None:
594
+ """Get or sets the summary of :attr:`EOExtension.snow_cover` values
595
+ for this Collection.
596
+ """
597
+ return self.summaries.get_range(SNOW_COVER_PROP)
598
+
599
+ @snow_cover.setter
600
+ def snow_cover(self, v: RangeSummary[float] | None) -> None:
601
+ self._set_summary(SNOW_COVER_PROP, v)
602
+
603
+
604
+ class EOExtensionHooks(ExtensionHooks):
605
+ schema_uri: str = SCHEMA_URI
606
+ prev_extension_ids = {
607
+ "eo",
608
+ *[uri for uri in SCHEMA_URIS if uri != SCHEMA_URI],
609
+ }
610
+ stac_object_types = {pystac.STACObjectType.ITEM}
611
+
612
+ def migrate(
613
+ self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription
614
+ ) -> None:
615
+ if version < "0.9":
616
+ # Some eo fields became common_metadata
617
+ if (
618
+ "eo:platform" in obj["properties"]
619
+ and "platform" not in obj["properties"]
620
+ ):
621
+ obj["properties"]["platform"] = obj["properties"]["eo:platform"]
622
+ del obj["properties"]["eo:platform"]
623
+
624
+ if (
625
+ "eo:instrument" in obj["properties"]
626
+ and "instruments" not in obj["properties"]
627
+ ):
628
+ obj["properties"]["instruments"] = [obj["properties"]["eo:instrument"]]
629
+ del obj["properties"]["eo:instrument"]
630
+
631
+ if (
632
+ "eo:constellation" in obj["properties"]
633
+ and "constellation" not in obj["properties"]
634
+ ):
635
+ obj["properties"]["constellation"] = obj["properties"][
636
+ "eo:constellation"
637
+ ]
638
+ del obj["properties"]["eo:constellation"]
639
+
640
+ # Some eo fields became view extension fields
641
+ eo_to_view_fields = [
642
+ "off_nadir",
643
+ "azimuth",
644
+ "incidence_angle",
645
+ "sun_azimuth",
646
+ "sun_elevation",
647
+ ]
648
+
649
+ for field in eo_to_view_fields:
650
+ if f"eo:{field}" in obj["properties"]:
651
+ if "stac_extensions" not in obj:
652
+ obj["stac_extensions"] = []
653
+ if view.SCHEMA_URI not in obj["stac_extensions"]:
654
+ obj["stac_extensions"].append(view.SCHEMA_URI)
655
+ if f"view:{field}" not in obj["properties"]:
656
+ obj["properties"][f"view:{field}"] = obj["properties"][
657
+ f"eo:{field}"
658
+ ]
659
+ del obj["properties"][f"eo:{field}"]
660
+
661
+ # eo:epsg became proj:epsg in Projection Extension <2.0.0 and became
662
+ # proj:code in Projection Extension 2.0.0
663
+ eo_epsg = PREFIX + "epsg"
664
+ proj_epsg = projection.PREFIX + "epsg"
665
+ proj_code = projection.PREFIX + "code"
666
+ if (
667
+ eo_epsg in obj["properties"]
668
+ and proj_epsg not in obj["properties"]
669
+ and proj_code not in obj["properties"]
670
+ ):
671
+ obj["stac_extensions"] = obj.get("stac_extensions", [])
672
+ if set(obj["stac_extensions"]).intersection(
673
+ projection.ProjectionExtensionHooks.pre_2
674
+ ):
675
+ obj["properties"][proj_epsg] = obj["properties"].pop(eo_epsg)
676
+ else:
677
+ obj["properties"][proj_code] = (
678
+ f"EPSG:{obj['properties'].pop(eo_epsg)}"
679
+ )
680
+ if not projection.ProjectionExtensionHooks().has_extension(obj):
681
+ obj["stac_extensions"].append(
682
+ projection.ProjectionExtension.get_schema_uri()
683
+ )
684
+
685
+ if not any(prop.startswith(PREFIX) for prop in obj["properties"]):
686
+ obj["stac_extensions"].remove(EOExtension.get_schema_uri())
687
+
688
+ if version < "1.0.0-beta.1" and info.object_type == pystac.STACObjectType.ITEM:
689
+ # gsd moved from eo to common metadata
690
+ if "eo:gsd" in obj["properties"]:
691
+ obj["properties"]["gsd"] = obj["properties"]["eo:gsd"]
692
+ del obj["properties"]["eo:gsd"]
693
+
694
+ # The way bands were declared in assets changed.
695
+ # In 1.0.0-beta.1 they are inlined into assets as
696
+ # opposed to having indices back into a property-level array.
697
+ if "eo:bands" in obj["properties"]:
698
+ bands = obj["properties"]["eo:bands"]
699
+ for asset in obj["assets"].values():
700
+ if "eo:bands" in asset:
701
+ new_bands: list[dict[str, Any]] = []
702
+ for band_index in asset["eo:bands"]:
703
+ new_bands.append(bands[band_index])
704
+ asset["eo:bands"] = new_bands
705
+
706
+ super().migrate(obj, version, info)
707
+
708
+
709
+ EO_EXTENSION_HOOKS: ExtensionHooks = EOExtensionHooks()
File without changes
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: pystac-ext-eo
3
+ Version: 1.1.0
4
+ Summary: Electro-Optical extension for PySTAC
5
+ Project-URL: Documentation, https://pystac.readthedocs.io
6
+ Project-URL: Repository, https://github.com/stac-utils/pystac
7
+ Project-URL: Issues, https://github.com/stac-utils/pystac/issues
8
+ Project-URL: Changelog, https://github.com/stac-utils/pystac/blob/main/CHANGELOG.md
9
+ Project-URL: Discussions, https://github.com/radiantearth/stac-spec/discussions/categories/stac-software
10
+ License: Apache-2.0
11
+ Keywords: STAC,catalog,eo,imagery,pystac,raster
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Natural Language :: English
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: pystac-core
23
+ Description-Content-Type: text/markdown
24
+
25
+ # pystac-ext-eo
26
+
27
+ [PySTAC](https://pypi.org/project/pystac/) extension package for the [Electro-Optical Extension](https://github.com/stac-extensions/eo).
28
+ This extension provides fields for describing electro-optical sensor data, including spectral band information, cloud cover, and snow cover.
29
+
30
+ ## Supported versions
31
+
32
+ - [v1.1.0](https://stac-extensions.github.io/eo/v1.1.0/schema.json)
33
+ - [v1.0.0](https://stac-extensions.github.io/eo/v1.0.0/schema.json)
34
+
35
+ ## Versioning
36
+
37
+ This package's version corresponds to the version of the extension specification it targets.
38
+ When we release updates to the package code without changing the target extension version, we use [post releases](https://packaging.python.org/en/latest/discussions/versioning/#post-releases), e.g. `1.1.0.post1`.
@@ -0,0 +1,5 @@
1
+ pystac/extensions/eo.py,sha256=eUp78damK68KVo0dMeZCtHAA9kelGD_AkYEY441oN1w,25788
2
+ pystac/extensions/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ pystac_ext_eo-1.1.0.dist-info/METADATA,sha256=tJcR04d1yDcc3IluvPl-0JLq5E8_bJuo-v8Ian1VAtI,1869
4
+ pystac_ext_eo-1.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
5
+ pystac_ext_eo-1.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any