pystac-ext-classification 2.0.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,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.0
|
|
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.0.dist-info/METADATA,sha256=dghiPGPdhF_Cd6o2Jz7Woy2qqUKssyQQGKFn-kcp5A8,2065
|
|
4
|
+
pystac_ext_classification-2.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
5
|
+
pystac_ext_classification-2.0.0.dist-info/RECORD,,
|