pystac-ext-classification 2.0.0rc0__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,764 @@
1
+ """Implements the :stac-ext:`Classification <classification>`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import warnings
7
+ from collections.abc import Iterable
8
+ from re import Pattern
9
+ from typing import (
10
+ Any,
11
+ Generic,
12
+ Literal,
13
+ TypeVar,
14
+ cast,
15
+ )
16
+
17
+ import pystac
18
+ from pystac.extensions.base import (
19
+ ExtensionManagementMixin,
20
+ PropertiesExtension,
21
+ SummariesExtension,
22
+ )
23
+ from pystac.extensions.hooks import ExtensionHooks
24
+ from pystac.extensions.raster import RasterBand
25
+ from pystac.serialization.identify import STACJSONDescription, STACVersionID
26
+ from pystac.utils import get_required, map_opt
27
+
28
+ #: Generalized version of :class:`~pystac.Item`, :class:`~pystac.Asset`,
29
+ #: :class:`~pystac.ItemAssetDefinition` or :class:`~pystac.extensions.raster.RasterBand`
30
+ T = TypeVar("T", pystac.Item, pystac.Asset, pystac.ItemAssetDefinition, RasterBand)
31
+
32
+ SCHEMA_URI_PATTERN: str = (
33
+ "https://stac-extensions.github.io/classification/v{version}/schema.json"
34
+ )
35
+ DEFAULT_VERSION: str = "2.0.0"
36
+ SUPPORTED_VERSIONS: list[str] = ["2.0.0", "1.1.0", "1.0.0"]
37
+
38
+ # Field names
39
+ PREFIX: str = "classification:"
40
+ BITFIELDS_PROP: str = PREFIX + "bitfields"
41
+ CLASSES_PROP: str = PREFIX + "classes"
42
+ RASTER_BANDS_PROP: str = "raster:bands"
43
+
44
+ COLOR_HINT_PATTERN: Pattern[str] = re.compile("^([0-9A-Fa-f]{6})$")
45
+
46
+
47
+ class Classification:
48
+ """Represents a single category of a classification.
49
+
50
+ Use Classification.create to create a new Classification.
51
+ """
52
+
53
+ properties: dict[str, Any]
54
+
55
+ def __init__(self, properties: dict[str, Any]) -> None:
56
+ self.properties = properties
57
+
58
+ def apply(
59
+ self,
60
+ value: int,
61
+ description: str | None = None,
62
+ name: str | None = None,
63
+ title: str | None = None,
64
+ color_hint: str | None = None,
65
+ nodata: bool | None = None,
66
+ percentage: float | None = None,
67
+ count: int | None = None,
68
+ ) -> None:
69
+ """
70
+ Set the properties for a new Classification.
71
+
72
+ Args:
73
+ value: The integer value corresponding to this class
74
+ description: The description of this class
75
+ name: Short name of the class for machine readability. Must consist only
76
+ of letters, numbers, -, and _ characters. Required as of v2.0 of
77
+ this extension.
78
+ title: Human-readable name for use in, e.g., a map legend
79
+ color_hint: An optional hexadecimal string-encoded representation of the
80
+ RGB color that is suggested to represent this class (six hexadecimal
81
+ characters, all capitalized)
82
+ nodata: If set to true classifies a value as a no-data value.
83
+ percentage: The percentage of data values that belong to this class
84
+ in comparison to all data values, in percent (0-100).
85
+ count: The number of data values that belong to this class.
86
+ """
87
+ self.value = value
88
+ # TODO pystac v2.0: make `name` non-optional, move it before
89
+ # `description` in the arg list, and remove this check
90
+ if name is None:
91
+ raise Exception(
92
+ "As of v2.0.0 of the classification extension, 'name' is required"
93
+ )
94
+ self.name = name
95
+ self.title = title
96
+ self.description = description
97
+ self.color_hint = color_hint
98
+ self.nodata = nodata
99
+ self.percentage = percentage
100
+ self.count = count
101
+
102
+ if color_hint is not None:
103
+ match = COLOR_HINT_PATTERN.match(color_hint)
104
+ assert (
105
+ color_hint is None or match is not None and match.group() == color_hint
106
+ ), "Must format color hints as '^([0-9A-F]{6})$'"
107
+
108
+ @classmethod
109
+ def create(
110
+ cls,
111
+ value: int,
112
+ description: str | None = None,
113
+ name: str | None = None,
114
+ title: str | None = None,
115
+ color_hint: str | None = None,
116
+ nodata: bool | None = None,
117
+ percentage: float | None = None,
118
+ count: int | None = None,
119
+ ) -> Classification:
120
+ """
121
+ Create a new Classification.
122
+
123
+ Args:
124
+ value: The integer value corresponding to this class
125
+ description: The optional long-form description of this class
126
+ name: Short name of the class for machine readability. Must consist only
127
+ of letters, numbers, -, and _ characters. Required as of v2.0 of
128
+ this extension.
129
+ title: Human-readable name for use in, e.g., a map legend
130
+ color_hint: An optional hexadecimal string-encoded representation of the
131
+ RGB color that is suggested to represent this class (six hexadecimal
132
+ characters, all capitalized)
133
+ nodata: If set to true classifies a value as a no-data value.
134
+ percentage: The percentage of data values that belong to this class
135
+ in comparison to all data values, in percent (0-100).
136
+ count: The number of data values that belong to this class.
137
+ """
138
+ c = cls({})
139
+ c.apply(
140
+ value=value,
141
+ name=name,
142
+ title=title,
143
+ description=description,
144
+ color_hint=color_hint,
145
+ nodata=nodata,
146
+ percentage=percentage,
147
+ count=count,
148
+ )
149
+ return c
150
+
151
+ @property
152
+ def value(self) -> int:
153
+ """Get or set the class value
154
+
155
+ Returns:
156
+ int
157
+ """
158
+ return cast(int, get_required(self.properties.get("value"), self, "value"))
159
+
160
+ @value.setter
161
+ def value(self, v: int) -> None:
162
+ self.properties["value"] = v
163
+
164
+ @property
165
+ def description(self) -> str | None:
166
+ """Get or set the description of the class
167
+
168
+ Returns:
169
+ str
170
+ """
171
+ return self.properties.get("description")
172
+
173
+ @description.setter
174
+ def description(self, v: str | None) -> None:
175
+ if v is not None:
176
+ self.properties["description"] = v
177
+ else:
178
+ self.properties.pop("description", None)
179
+
180
+ @property
181
+ def name(self) -> str:
182
+ """Get or set the name of the class
183
+
184
+ Returns:
185
+ Optional[str]
186
+ """
187
+ return cast(str, get_required(self.properties.get("name"), self, "name"))
188
+
189
+ @name.setter
190
+ def name(self, v: str) -> None:
191
+ if v is None:
192
+ raise Exception(
193
+ "`name` was converted to a required attribute in classification"
194
+ " version v2.0, so cannot be set to None"
195
+ )
196
+ self.properties["name"] = v
197
+
198
+ @property
199
+ def title(self) -> str | None:
200
+ return self.properties.get("title")
201
+
202
+ @title.setter
203
+ def title(self, v: str | None) -> None:
204
+ if v is not None:
205
+ self.properties["title"] = v
206
+ else:
207
+ self.properties.pop("title", None)
208
+
209
+ @property
210
+ def color_hint(self) -> str | None:
211
+ """Get or set the optional color hint for this class.
212
+
213
+ The color hint must be a six-character string of capitalized hexadecimal
214
+ characters ([0-9A-F]).
215
+
216
+ Returns:
217
+ Optional[str]
218
+ """
219
+ return self.properties.get("color_hint")
220
+
221
+ @color_hint.setter
222
+ def color_hint(self, v: str | None) -> None:
223
+ if v is not None:
224
+ match = COLOR_HINT_PATTERN.match(v)
225
+ assert v is None or match is not None and match.group() == v, (
226
+ "Must format color hints as '^([0-9A-F]{6})$'"
227
+ )
228
+ self.properties["color_hint"] = v
229
+ else:
230
+ self.properties.pop("color_hint", None)
231
+
232
+ @property
233
+ def nodata(self) -> bool | None:
234
+ """Get or set the nodata value for this class.
235
+
236
+ Returns:
237
+ bool | None
238
+ """
239
+ return self.properties.get("nodata")
240
+
241
+ @nodata.setter
242
+ def nodata(self, v: bool | None) -> None:
243
+ if v is not None:
244
+ self.properties["nodata"] = v
245
+ else:
246
+ self.properties.pop("nodata", None)
247
+
248
+ @property
249
+ def percentage(self) -> float | None:
250
+ """Get or set the percentage value for this class.
251
+
252
+ Returns:
253
+ Optional[float]
254
+ """
255
+ return self.properties.get("percentage")
256
+
257
+ @percentage.setter
258
+ def percentage(self, v: float | None) -> None:
259
+ if v is not None:
260
+ self.properties["percentage"] = v
261
+ else:
262
+ self.properties.pop("percentage", None)
263
+
264
+ @property
265
+ def count(self) -> int | None:
266
+ """Get or set the count value for this class.
267
+
268
+ Returns:
269
+ Optional[int]
270
+ """
271
+ return self.properties.get("count")
272
+
273
+ @count.setter
274
+ def count(self, v: int | None) -> None:
275
+ if v is not None:
276
+ self.properties["count"] = v
277
+ else:
278
+ self.properties.pop("count", None)
279
+
280
+ def to_dict(self) -> dict[str, Any]:
281
+ """Returns the dictionary encoding of this class
282
+
283
+ Returns:
284
+ dict: The serialization of the Classification
285
+ """
286
+ return self.properties
287
+
288
+ def __eq__(self, other: object) -> bool:
289
+ if not isinstance(other, Classification):
290
+ raise NotImplementedError
291
+ return (
292
+ self.value == other.value
293
+ and self.description == other.description
294
+ and self.name == other.name
295
+ and self.color_hint == other.color_hint
296
+ )
297
+
298
+ def __repr__(self) -> str:
299
+ return f"<Classification value={self.value} description={self.description}>"
300
+
301
+
302
+ class Bitfield:
303
+ """Encodes the representation of values as bits in an integer.
304
+
305
+ Use Bitfield.create to create a new Bitfield.
306
+ """
307
+
308
+ properties: dict[str, Any]
309
+
310
+ def __init__(self, properties: dict[str, Any]):
311
+ self.properties = properties
312
+
313
+ def apply(
314
+ self,
315
+ offset: int,
316
+ length: int,
317
+ classes: list[Classification],
318
+ roles: list[str] | None = None,
319
+ description: str | None = None,
320
+ name: str | None = None,
321
+ ) -> None:
322
+ """Sets the properties for this Bitfield.
323
+
324
+ Args:
325
+ offset: describes the position of the least significant bit captured
326
+ by this bitfield, with zero indicating the least significant binary
327
+ digit
328
+ length: the number of bits described by this bitfield
329
+ classes: a list of Classification objects describing the various levels
330
+ captured by this bitfield
331
+ roles: the optional role of this bitfield (see `Asset Roles
332
+ <https://github.com/radiantearth/stac-spec/blob/master/item-spec/item-spec.md#asset-roles>`)
333
+ description: an optional short description of the classification
334
+ name: the optional name of the class for machine readability
335
+ """
336
+ self.offset = offset
337
+ self.length = length
338
+ self.classes = classes
339
+ self.roles = roles
340
+ self.description = description
341
+ self.name = name
342
+
343
+ assert offset >= 0, "Non-negative offsets only"
344
+ assert length >= 1, "Positive field lengths only"
345
+ assert len(classes) > 0, "Must specify at least one class"
346
+ assert roles is None or len(roles) > 0, (
347
+ "When set, roles must contain at least one item"
348
+ )
349
+
350
+ @classmethod
351
+ def create(
352
+ cls,
353
+ offset: int,
354
+ length: int,
355
+ classes: list[Classification],
356
+ roles: list[str] | None = None,
357
+ description: str | None = None,
358
+ name: str | None = None,
359
+ ) -> Bitfield:
360
+ """Sets the properties for this Bitfield.
361
+
362
+ Args:
363
+ offset: describes the position of the least significant bit captured
364
+ by this bitfield, with zero indicating the least significant binary
365
+ digit
366
+ length: the number of bits described by this bitfield
367
+ classes: a list of Classification objects describing the various levels
368
+ captured by this bitfield
369
+ roles: the optional role of this bitfield (see `Asset Roles
370
+ <https://github.com/radiantearth/stac-spec/blob/master/item-spec/item-spec.md#asset-roles>`)
371
+ description: an optional short description of the classification
372
+ name: the optional name of the class for machine readability
373
+ """
374
+ b = cls({})
375
+ b.apply(
376
+ offset=offset,
377
+ length=length,
378
+ classes=classes,
379
+ roles=roles,
380
+ description=description,
381
+ name=name,
382
+ )
383
+ return b
384
+
385
+ @property
386
+ def offset(self) -> int:
387
+ """Get or set the offset of the bitfield.
388
+
389
+ Describes the position of the least significant bit captured by this
390
+ bitfield, with zero indicating the least significant binary digit
391
+
392
+ Returns:
393
+ int
394
+ """
395
+ return cast(int, get_required(self.properties.get("offset"), self, "offset"))
396
+
397
+ @offset.setter
398
+ def offset(self, v: int) -> None:
399
+ self.properties["offset"] = v
400
+
401
+ @property
402
+ def length(self) -> int:
403
+ """Get or set the length (number of bits) of the bitfield
404
+
405
+ Returns:
406
+ int
407
+ """
408
+ return cast(int, get_required(self.properties.get("length"), self, "length"))
409
+
410
+ @length.setter
411
+ def length(self, v: int) -> None:
412
+ self.properties["length"] = v
413
+
414
+ @property
415
+ def classes(self) -> list[Classification]:
416
+ """Get or set the class definitions for the bitfield
417
+
418
+ Returns:
419
+ List[Classification]
420
+ """
421
+
422
+ return [
423
+ Classification(d)
424
+ for d in cast(
425
+ list[dict[str, Any]],
426
+ get_required(
427
+ self.properties.get("classes"),
428
+ self,
429
+ "classes",
430
+ ),
431
+ )
432
+ ]
433
+
434
+ @classes.setter
435
+ def classes(self, v: list[Classification]) -> None:
436
+ self.properties["classes"] = [c.to_dict() for c in v]
437
+
438
+ @property
439
+ def roles(self) -> list[str] | None:
440
+ """Get or set the role of the bitfield.
441
+
442
+ See `Asset Roles
443
+ <https://github.com/radiantearth/stac-spec/blob/master/item-spec/item-spec.md#asset-roles>`
444
+
445
+ Returns:
446
+ Optional[List[str]]
447
+ """
448
+ return self.properties.get("roles")
449
+
450
+ @roles.setter
451
+ def roles(self, v: list[str] | None) -> None:
452
+ if v is not None:
453
+ self.properties["roles"] = v
454
+ else:
455
+ self.properties.pop("roles", None)
456
+
457
+ @property
458
+ def description(self) -> str | None:
459
+ """Get or set the optional description of a bitfield.
460
+
461
+ Returns:
462
+ Optional[str]
463
+ """
464
+ return self.properties.get("description")
465
+
466
+ @description.setter
467
+ def description(self, v: str | None) -> None:
468
+ if v is not None:
469
+ self.properties["description"] = v
470
+ else:
471
+ self.properties.pop("description", None)
472
+
473
+ @property
474
+ def name(self) -> str | None:
475
+ """Get or set the optional name of the bitfield.
476
+
477
+ Returns:
478
+ Optional[str]
479
+ """
480
+ return self.properties.get("name")
481
+
482
+ @name.setter
483
+ def name(self, v: str | None) -> None:
484
+ if v is not None:
485
+ self.properties["name"] = v
486
+ else:
487
+ self.properties.pop("name", None)
488
+
489
+ def __repr__(self) -> str:
490
+ return (
491
+ f"<Bitfield offset={self.offset} length={self.length} "
492
+ f"classes={self.classes}>"
493
+ )
494
+
495
+ def to_dict(self) -> dict[str, Any]:
496
+ """Returns the dictionary encoding of this bitfield
497
+
498
+ Returns:
499
+ dict: The serialization of the Bitfield
500
+ """
501
+ return self.properties
502
+
503
+
504
+ class ClassificationExtension(
505
+ Generic[T],
506
+ PropertiesExtension,
507
+ ExtensionManagementMixin[pystac.Item | pystac.Collection],
508
+ ):
509
+ """An abstract class that can be used to extend the properties of
510
+ :class:`~pystac.Item`, :class:`~pystac.Asset`,
511
+ :class:`~pystac.extensions.raster.RasterBand`, or
512
+ :class:`~pystac.ItemAssetDefinition` with properties from the
513
+ :stac-ext:`Classification Extension <classification>`. This class is generic
514
+ over the type of STAC object being extended.
515
+
516
+ This class is not to be instantiated directly. One can either directly use the
517
+ subclass corresponding to the object you are extending, or the `ext` class
518
+ method can be used to construct the proper class for you.
519
+ """
520
+
521
+ name: Literal["classification"] = "classification"
522
+ properties: dict[str, Any]
523
+ """The :class:`~pystac.Asset` fields, including extension properties."""
524
+
525
+ def apply(
526
+ self,
527
+ classes: list[Classification] | None = None,
528
+ bitfields: list[Bitfield] | None = None,
529
+ ) -> None:
530
+ """Applies the classification extension fields to the extended object.
531
+
532
+ Note: one may set either the classes or bitfields objects, but not both.
533
+
534
+ Args:
535
+ classes: a list of
536
+ :class:`~pystac.extensions.classification.Classification` objects
537
+ describing the various classes in the classification
538
+ """
539
+ assert (
540
+ classes is None
541
+ and bitfields is not None
542
+ or bitfields is None
543
+ and classes is not None
544
+ ), "Must set exactly one of `classes` or `bitfields`"
545
+ if classes:
546
+ self.classes = classes
547
+ if bitfields:
548
+ self.bitfields = bitfields
549
+
550
+ @property
551
+ def classes(self) -> list[Classification] | None:
552
+ """Get or set the classes for the base object
553
+
554
+ Note: Setting the classes will clear the object's bitfields if they are
555
+ not None
556
+
557
+ Returns:
558
+ Optional[List[Classification]]
559
+ """
560
+ return self._get_classes()
561
+
562
+ @classes.setter
563
+ def classes(self, v: list[Classification] | None) -> None:
564
+ if self._get_bitfields() is not None:
565
+ self.bitfields = None
566
+ self._set_property(
567
+ CLASSES_PROP, map_opt(lambda classes: [c.to_dict() for c in classes], v)
568
+ )
569
+
570
+ def _get_classes(self) -> list[Classification] | None:
571
+ return map_opt(
572
+ lambda classes: [Classification(c) for c in classes],
573
+ self._get_property(CLASSES_PROP, list[dict[str, Any]]),
574
+ )
575
+
576
+ @property
577
+ def bitfields(self) -> list[Bitfield] | None:
578
+ """Get or set the bitfields for the base object
579
+
580
+ Note: Setting the bitfields will clear the object's classes if they are
581
+ not None
582
+
583
+ Returns:
584
+ Optional[List[Bitfield]]
585
+ """
586
+ return self._get_bitfields()
587
+
588
+ @bitfields.setter
589
+ def bitfields(self, v: list[Bitfield] | None) -> None:
590
+ if self._get_classes() is not None:
591
+ self.classes = None
592
+ self._set_property(
593
+ BITFIELDS_PROP,
594
+ map_opt(lambda bitfields: [b.to_dict() for b in bitfields], v),
595
+ )
596
+
597
+ def _get_bitfields(self) -> list[Bitfield] | None:
598
+ return map_opt(
599
+ lambda bitfields: [Bitfield(b) for b in bitfields],
600
+ self._get_property(BITFIELDS_PROP, list[dict[str, Any]]),
601
+ )
602
+
603
+ @classmethod
604
+ def get_schema_uri(cls) -> str:
605
+ return SCHEMA_URI_PATTERN.format(version=DEFAULT_VERSION)
606
+
607
+ @classmethod
608
+ def get_schema_uris(cls) -> list[str]:
609
+ warnings.warn(
610
+ "get_schema_uris is deprecated and will be removed in v2",
611
+ DeprecationWarning,
612
+ )
613
+ return [SCHEMA_URI_PATTERN.format(version=v) for v in SUPPORTED_VERSIONS]
614
+
615
+ @classmethod
616
+ def ext(cls, obj: T, add_if_missing: bool = False) -> ClassificationExtension[T]:
617
+ """Extends the given STAC object with propertied from the
618
+ :stac-ext:`Classification Extension <classification>`
619
+
620
+ This extension can be applied to instances of :class:`~pystac.Item`,
621
+ :class:`~pystac.Asset`,
622
+ :class:`~pystac.ItemAssetDefinition`, or
623
+ :class:`~pystac.extensions.raster.RasterBand`.
624
+
625
+ Raises:
626
+ pystac.ExtensionTypeError : If an invalid object type is passed
627
+ """
628
+ if isinstance(obj, pystac.Item):
629
+ cls.ensure_has_extension(obj, add_if_missing)
630
+ return cast(ClassificationExtension[T], ItemClassificationExtension(obj))
631
+ elif isinstance(obj, pystac.Asset):
632
+ cls.ensure_owner_has_extension(obj, add_if_missing)
633
+ return cast(ClassificationExtension[T], AssetClassificationExtension(obj))
634
+ elif isinstance(obj, pystac.ItemAssetDefinition):
635
+ cls.ensure_owner_has_extension(obj, add_if_missing)
636
+ return cast(
637
+ ClassificationExtension[T], ItemAssetsClassificationExtension(obj)
638
+ )
639
+ elif isinstance(obj, RasterBand):
640
+ return cast(
641
+ ClassificationExtension[T], RasterBandClassificationExtension(obj)
642
+ )
643
+ else:
644
+ raise pystac.ExtensionTypeError(cls._ext_error_message(obj))
645
+
646
+ @classmethod
647
+ def summaries(
648
+ cls, obj: pystac.Collection, add_if_missing: bool = False
649
+ ) -> SummariesClassificationExtension:
650
+ cls.ensure_has_extension(obj, add_if_missing)
651
+ return SummariesClassificationExtension(obj)
652
+
653
+
654
+ class ItemClassificationExtension(ClassificationExtension[pystac.Item]):
655
+ item: pystac.Item
656
+
657
+ properties: dict[str, Any]
658
+
659
+ def __init__(self, item: pystac.Item):
660
+ self.item = item
661
+ self.properties = item.properties
662
+
663
+ def __repr__(self) -> str:
664
+ return f"<ItemClassificationExtension Item id={self.item.id}>"
665
+
666
+
667
+ class AssetClassificationExtension(ClassificationExtension[pystac.Asset]):
668
+ asset: pystac.Asset
669
+ asset_href: str
670
+ properties: dict[str, Any]
671
+ additional_read_properties: Iterable[dict[str, Any]] | None
672
+
673
+ def __init__(self, asset: pystac.Asset):
674
+ self.asset = asset
675
+ self.asset_href = asset.href
676
+ self.properties = asset.extra_fields
677
+ if asset.owner and isinstance(asset.owner, pystac.Item):
678
+ self.additional_read_properties = [asset.owner.properties]
679
+
680
+ def __repr__(self) -> str:
681
+ return f"<AssetClassificationExtension Asset href={self.asset_href}>"
682
+
683
+
684
+ class ItemAssetsClassificationExtension(
685
+ ClassificationExtension[pystac.ItemAssetDefinition]
686
+ ):
687
+ properties: dict[str, Any]
688
+ asset_defn: pystac.ItemAssetDefinition
689
+
690
+ def __init__(self, item_asset: pystac.ItemAssetDefinition):
691
+ self.asset_defn = item_asset
692
+ self.properties = item_asset.properties
693
+
694
+ def __repr__(self) -> str:
695
+ return (
696
+ f"<ItemAssetsClassificationExtension ItemAssetDefinition={self.asset_defn}"
697
+ )
698
+
699
+
700
+ class RasterBandClassificationExtension(ClassificationExtension[RasterBand]):
701
+ properties: dict[str, Any]
702
+ raster_band: RasterBand
703
+
704
+ def __init__(self, raster_band: RasterBand):
705
+ self.raster_band = raster_band
706
+ self.properties = raster_band.properties
707
+
708
+ def __repr__(self) -> str:
709
+ return f"<RasterBandClassificationExtension RasterBand={self.raster_band}>"
710
+
711
+
712
+ class SummariesClassificationExtension(SummariesExtension):
713
+ @property
714
+ def classes(self) -> list[Classification] | None:
715
+ return map_opt(
716
+ lambda classes: [Classification(c) for c in classes],
717
+ self.summaries.get_list(CLASSES_PROP),
718
+ )
719
+
720
+ @classes.setter
721
+ def classes(self, v: list[Classification] | None) -> None:
722
+ self._set_summary(CLASSES_PROP, map_opt(lambda x: [c.to_dict() for c in x], v))
723
+
724
+ @property
725
+ def bitfields(self) -> list[Bitfield] | None:
726
+ return map_opt(
727
+ lambda bitfields: [Bitfield(b) for b in bitfields],
728
+ self.summaries.get_list(BITFIELDS_PROP),
729
+ )
730
+
731
+ @bitfields.setter
732
+ def bitfields(self, v: list[Bitfield] | None) -> None:
733
+ self._set_summary(
734
+ BITFIELDS_PROP, map_opt(lambda x: [b.to_dict() for b in x], v)
735
+ )
736
+
737
+
738
+ class ClassificationExtensionHooks(ExtensionHooks):
739
+ schema_uri: str = SCHEMA_URI_PATTERN.format(version=DEFAULT_VERSION)
740
+ prev_extension_ids = {
741
+ SCHEMA_URI_PATTERN.format(version=v)
742
+ for v in SUPPORTED_VERSIONS
743
+ if v != DEFAULT_VERSION
744
+ }
745
+ stac_object_types = {pystac.STACObjectType.ITEM}
746
+
747
+ def migrate(
748
+ self, obj: dict[str, Any], version: STACVersionID, info: STACJSONDescription
749
+ ) -> None:
750
+ if SCHEMA_URI_PATTERN.format(version="1.0.0") in info.extensions:
751
+ for asset in obj.get("assets", {}).values():
752
+ classification_classes = asset.get(CLASSES_PROP, None)
753
+ if classification_classes is None or not isinstance(
754
+ classification_classes, list
755
+ ):
756
+ continue
757
+ for class_object in classification_classes:
758
+ if "color-hint" in class_object:
759
+ class_object["color_hint"] = class_object["color-hint"]
760
+ del class_object["color-hint"]
761
+ super().migrate(obj, version, info)
762
+
763
+
764
+ CLASSIFICATION_EXTENSION_HOOKS: ExtensionHooks = ClassificationExtensionHooks()
File without changes
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: pystac-ext-classification
3
+ Version: 2.0.0rc0
4
+ Summary: Classification 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,classification,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-classification
26
+
27
+ [PySTAC](https://pypi.org/project/pystac/) extension package for the [Classification Extension](https://github.com/stac-extensions/classification).
28
+ This extension provides a way to describe categorical data and classification schemes for raster and vector data, including class names, descriptions, color hints, and regional mappings.
29
+
30
+ ## Supported versions
31
+
32
+ - [v2.0.0](https://stac-extensions.github.io/classification/v2.0.0/schema.json)
33
+ - [v1.1.0](https://stac-extensions.github.io/classification/v1.1.0/schema.json)
34
+ - [v1.0.0](https://stac-extensions.github.io/classification/v1.0.0/schema.json)
35
+
36
+ ## Versioning
37
+
38
+ This package's version corresponds to the version of the extension specification it targets.
39
+ 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. `2.0.0.post1`.
@@ -0,0 +1,5 @@
1
+ pystac/extensions/classification.py,sha256=1j-qsqljsddrS42nZmpUZRFORE2G8FnnXC1N3CLrgPg,25157
2
+ pystac/extensions/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ pystac_ext_classification-2.0.0rc0.dist-info/METADATA,sha256=L4xijyXKqwvyXMhjCxar7y34eWZtRRwqRuERDNMVUTo,2068
4
+ pystac_ext_classification-2.0.0rc0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
5
+ pystac_ext_classification-2.0.0rc0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any