• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geo-engine / geoengine-python / 16472413208

23 Jul 2025 01:08PM UTC coverage: 76.961% (+0.02%) from 76.94%
16472413208

push

github

web-flow
get volume and metadata by name, add iterator for datasets (#237)

* get volume and metadata by name, add iterator for datasets

* update tests

* update test

* adapt tests and global exported methds

* update examples

* linter reorder

* add dataset_metadata_by_name to test

2836 of 3685 relevant lines covered (76.96%)

0.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.02
geoengine/workflow_builder/operators.py
1
"""This module contains helpers to create workflow operators for the Geo Engine API."""
2

3
from __future__ import annotations
1✔
4

5
import datetime
1✔
6
from abc import abstractmethod
1✔
7
from dataclasses import dataclass
1✔
8
from enum import Enum
1✔
9
from typing import Any, Literal, cast
1✔
10

11
import geoengine_openapi_client
1✔
12
import numpy as np
1✔
13

14
from geoengine.datasets import DatasetName
1✔
15
from geoengine.types import Measurement, RasterBandDescriptor
1✔
16

17
# pylint: disable=too-many-lines
18

19

20
class Operator:
1✔
21
    """Base class for all operators."""
22

23
    @abstractmethod
1✔
24
    def name(self) -> str:
1✔
25
        """Returns the name of the operator."""
26

27
    @abstractmethod
1✔
28
    def to_dict(self) -> dict[str, Any]:
1✔
29
        """Returns a dictionary representation of the operator that can be used to create a JSON request for the API."""
30

31
    @abstractmethod
1✔
32
    def data_type(self) -> Literal["Raster", "Vector"]:
1✔
33
        """Returns the type of the operator."""
34

35
    def to_workflow_dict(self) -> dict[str, Any]:
1✔
36
        """Returns a dictionary representation of a workflow that calls the operator" \
37
             "that can be used to create a JSON request for the workflow API."""
38

39
        return {
1✔
40
            "type": self.data_type(),
41
            "operator": self.to_dict(),
42
        }
43

44
    @classmethod
1✔
45
    def from_workflow_dict(cls, workflow) -> Operator:
1✔
46
        """Returns an operator from a workflow dictionary."""
47
        if workflow["type"] == "Raster":
1✔
48
            return RasterOperator.from_operator_dict(workflow["operator"])
1✔
49
        if workflow["type"] == "Vector":
×
50
            return VectorOperator.from_operator_dict(workflow["operator"])
×
51

52
        raise NotImplementedError(f"Unknown workflow type {workflow['type']}")
×
53

54

55
class RasterOperator(Operator):
1✔
56
    """Base class for all raster operators."""
57

58
    @abstractmethod
1✔
59
    def to_dict(self) -> dict[str, Any]:
1✔
60
        pass
×
61

62
    def data_type(self) -> Literal["Raster", "Vector"]:
1✔
63
        return "Raster"
1✔
64

65
    @classmethod
1✔
66
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterOperator:  # pylint: disable=too-many-return-statements
1✔
67
        """Returns an operator from a dictionary."""
68
        if operator_dict["type"] == "GdalSource":
1✔
69
            return GdalSource.from_operator_dict(operator_dict)
1✔
70
        if operator_dict["type"] == "RasterScaling":
1✔
71
            return RasterScaling.from_operator_dict(operator_dict)
1✔
72
        if operator_dict["type"] == "RasterTypeConversion":
1✔
73
            return RasterTypeConversion.from_operator_dict(operator_dict)
1✔
74
        if operator_dict["type"] == "Reprojection":
×
75
            return Reprojection.from_operator_dict(operator_dict).as_raster()
×
76
        if operator_dict["type"] == "Interpolation":
×
77
            return Interpolation.from_operator_dict(operator_dict)
×
78
        if operator_dict["type"] == "Expression":
×
79
            return Expression.from_operator_dict(operator_dict)
×
80
        if operator_dict["type"] == "BandwiseExpression":
×
81
            return BandwiseExpression.from_operator_dict(operator_dict)
×
82
        if operator_dict["type"] == "TimeShift":
×
83
            return TimeShift.from_operator_dict(operator_dict).as_raster()
×
84
        if operator_dict["type"] == "TemporalRasterAggregation":
×
85
            return TemporalRasterAggregation.from_operator_dict(operator_dict)
×
86
        if operator_dict["type"] == "RasterStacker":
×
87
            return RasterStacker.from_operator_dict(operator_dict)
×
88
        if operator_dict["type"] == "BandNeighborhoodAggregate":
×
89
            return BandNeighborhoodAggregate.from_operator_dict(operator_dict)
×
90

91
        raise NotImplementedError(f"Unknown operator type {operator_dict['type']}")
×
92

93

94
class VectorOperator(Operator):
1✔
95
    """Base class for all vector operators."""
96

97
    @abstractmethod
1✔
98
    def to_dict(self) -> dict[str, Any]:
1✔
99
        pass
×
100

101
    def data_type(self) -> Literal["Raster", "Vector"]:
1✔
102
        return "Vector"
×
103

104
    @classmethod
1✔
105
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> VectorOperator:
1✔
106
        """Returns an operator from a dictionary."""
107
        if operator_dict["type"] == "OgrSource":
1✔
108
            return OgrSource.from_operator_dict(operator_dict)
1✔
109
        if operator_dict["type"] == "Reprojection":
×
110
            return Reprojection.from_operator_dict(operator_dict).as_vector()
×
111
        if operator_dict["type"] == "RasterVectorJoin":
×
112
            return RasterVectorJoin.from_operator_dict(operator_dict)
×
113
        if operator_dict["type"] == "PointInPolygonFilter":
×
114
            return PointInPolygonFilter.from_operator_dict(operator_dict)
×
115
        if operator_dict["type"] == "TimeShift":
×
116
            return TimeShift.from_operator_dict(operator_dict).as_vector()
×
117
        if operator_dict["type"] == "VectorExpression":
×
118
            return VectorExpression.from_operator_dict(operator_dict)
×
119
        raise NotImplementedError(f"Unknown operator type {operator_dict['type']}")
×
120

121

122
class GdalSource(RasterOperator):
1✔
123
    """A GDAL source operator."""
124

125
    dataset: str
1✔
126

127
    def __init__(self, dataset: str | DatasetName):
1✔
128
        """Creates a new GDAL source operator."""
129
        if isinstance(dataset, DatasetName):
1✔
130
            dataset = str(dataset)
1✔
131
        self.dataset = dataset
1✔
132

133
    def name(self) -> str:
1✔
134
        return "GdalSource"
1✔
135

136
    def to_dict(self) -> dict[str, Any]:
1✔
137
        return {"type": self.name(), "params": {"data": self.dataset}}
1✔
138

139
    @classmethod
1✔
140
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> GdalSource:
1✔
141
        """Returns an operator from a dictionary."""
142
        if operator_dict["type"] != "GdalSource":
1✔
143
            raise ValueError("Invalid operator type")
×
144

145
        return GdalSource(cast(str, operator_dict["params"]["data"]))
1✔
146

147

148
class OgrSource(VectorOperator):
1✔
149
    """An OGR source operator."""
150

151
    dataset: str
1✔
152
    attribute_projection: str | None = None
1✔
153
    attribute_filters: str | None = None
1✔
154

155
    def __init__(
1✔
156
        self,
157
        dataset: str | DatasetName,
158
        attribute_projection: str | None = None,
159
        attribute_filters: str | None = None,
160
    ):
161
        """Creates a new OGR source operator."""
162
        if isinstance(dataset, DatasetName):
1✔
163
            dataset = str(dataset)
×
164
        self.dataset = dataset
1✔
165
        self.attribute_projection = attribute_projection
1✔
166
        self.attribute_filters = attribute_filters
1✔
167

168
    def name(self) -> str:
1✔
169
        return "OgrSource"
1✔
170

171
    def to_dict(self) -> dict[str, Any]:
1✔
172
        return {
1✔
173
            "type": self.name(),
174
            "params": {
175
                "data": self.dataset,
176
                "attributeProjection": self.attribute_projection,
177
                "attributeFilters": self.attribute_filters,
178
            },
179
        }
180

181
    @classmethod
1✔
182
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> OgrSource:
1✔
183
        """Returns an operator from a dictionary."""
184
        if operator_dict["type"] != "OgrSource":
1✔
185
            raise ValueError("Invalid operator type")
×
186

187
        params = operator_dict["params"]
1✔
188
        return OgrSource(
1✔
189
            cast(str, params["data"]),
190
            attribute_projection=cast(str | None, params.get("attributeProjection")),
191
            attribute_filters=cast(str | None, params.get("attributeFilters")),
192
        )
193

194

195
class Interpolation(RasterOperator):
1✔
196
    """An interpolation operator."""
197

198
    source: RasterOperator
1✔
199
    interpolation: Literal["biLinear", "nearestNeighbor"] = "biLinear"
1✔
200
    input_resolution: tuple[float, float] | None = None
1✔
201

202
    def __init__(
1✔
203
        self,
204
        source_operator: RasterOperator,
205
        interpolation: Literal["biLinear", "nearestNeighbor"] = "biLinear",
206
        input_resolution: tuple[float, float] | None = None,
207
    ):
208
        """Creates a new interpolation operator."""
209
        self.source = source_operator
1✔
210
        self.interpolation = interpolation
1✔
211
        self.input_resolution = input_resolution
1✔
212

213
    def name(self) -> str:
1✔
214
        return "Interpolation"
1✔
215

216
    def to_dict(self) -> dict[str, Any]:
1✔
217
        input_res: dict[str, str | float]
218
        if self.input_resolution is None:
1✔
219
            input_res = {"type": "source"}
1✔
220
        else:
221
            input_res = {"type": "value", "x": self.input_resolution[0], "y": self.input_resolution[1]}
×
222

223
        return {
1✔
224
            "type": self.name(),
225
            "params": {"interpolation": self.interpolation, "inputResolution": input_res},
226
            "sources": {"raster": self.source.to_dict()},
227
        }
228

229
    @classmethod
1✔
230
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Interpolation:
1✔
231
        """Returns an operator from a dictionary."""
232
        if operator_dict["type"] != "Interpolation":
1✔
233
            raise ValueError("Invalid operator type")
×
234

235
        source = RasterOperator.from_operator_dict(cast(dict[str, Any], operator_dict["sources"]["raster"]))
1✔
236

237
        def parse_input_params(params: dict[str, Any]) -> tuple[float, float] | None:
1✔
238
            if "type" not in params:
1✔
239
                return None
×
240
            if params["type"] == "source":
1✔
241
                return None
1✔
242
            if params["type"] == "value":
×
243
                return (float(params["x"]), float(params["y"]))
×
244
            raise ValueError(f"Invalid input resolution type {params['type']}")
×
245

246
        input_resolution = parse_input_params(cast(dict[str, Any], operator_dict["params"]["inputResolution"]))
1✔
247

248
        return Interpolation(
1✔
249
            source_operator=source,
250
            interpolation=cast(Literal["biLinear", "nearestNeighbor"], operator_dict["params"]["interpolation"]),
251
            input_resolution=input_resolution,
252
        )
253

254

255
class ColumnNames:
1✔
256
    """Base class for deriving column names from bands of a raster."""
257

258
    @abstractmethod
1✔
259
    def to_dict(self) -> dict[str, Any]:
1✔
260
        pass
×
261

262
    @classmethod
1✔
263
    def from_dict(cls, rename_dict: dict[str, Any]) -> ColumnNames:
1✔
264
        """Returns a ColumnNames object from a dictionary."""
265
        if rename_dict["type"] == "default":
1✔
266
            return ColumnNamesDefault()
×
267
        if rename_dict["type"] == "suffix":
1✔
268
            return ColumnNamesSuffix(cast(list[str], rename_dict["values"]))
×
269
        if rename_dict["type"] == "names":
1✔
270
            return ColumnNamesNames(cast(list[str], rename_dict["values"]))
1✔
271
        raise ValueError("Invalid rename type")
×
272

273
    @classmethod
1✔
274
    def default(cls) -> ColumnNames:
1✔
275
        return ColumnNamesDefault()
×
276

277
    @classmethod
1✔
278
    def suffix(cls, values: list[str]) -> ColumnNames:
1✔
279
        return ColumnNamesSuffix(values)
×
280

281
    @classmethod
1✔
282
    def rename(cls, values: list[str]) -> ColumnNames:
1✔
283
        return ColumnNamesNames(values)
×
284

285

286
class ColumnNamesDefault(ColumnNames):
1✔
287
    """column names with default suffix."""
288

289
    def to_dict(self) -> dict[str, Any]:
1✔
290
        return {"type": "default"}
×
291

292

293
class ColumnNamesSuffix(ColumnNames):
1✔
294
    """Rename bands with custom suffixes."""
295

296
    suffixes: list[str]
1✔
297

298
    def __init__(self, suffixes: list[str]) -> None:
1✔
299
        self.suffixes = suffixes
×
300
        super().__init__()
×
301

302
    def to_dict(self) -> dict[str, Any]:
1✔
303
        return {"type": "suffix", "values": self.suffixes}
×
304

305

306
class ColumnNamesNames(ColumnNames):
1✔
307
    """Rename bands with new names."""
308

309
    new_names: list[str]
1✔
310

311
    def __init__(self, new_names: list[str]) -> None:
1✔
312
        self.new_names = new_names
1✔
313
        super().__init__()
1✔
314

315
    def to_dict(self) -> dict[str, Any]:
1✔
316
        return {"type": "names", "values": self.new_names}
1✔
317

318

319
class RasterVectorJoin(VectorOperator):
1✔
320
    """A RasterVectorJoin operator."""
321

322
    raster_sources: list[RasterOperator]
1✔
323
    vector_source: VectorOperator
1✔
324
    names: ColumnNames
1✔
325
    temporal_aggregation: Literal["none", "first", "mean"] = "none"
1✔
326
    temporal_aggregation_ignore_nodata: bool = False
1✔
327
    feature_aggregation: Literal["first", "mean"] = "mean"
1✔
328
    feature_aggregation_ignore_nodata: bool = False
1✔
329

330
    # pylint: disable=too-many-arguments,too-many-positional-arguments
331
    def __init__(
1✔
332
        self,
333
        raster_sources: list[RasterOperator],
334
        vector_source: VectorOperator,
335
        names: ColumnNames,
336
        temporal_aggregation: Literal["none", "first", "mean"] = "none",
337
        temporal_aggregation_ignore_nodata: bool = False,
338
        feature_aggregation: Literal["first", "mean"] = "mean",
339
        feature_aggregation_ignore_nodata: bool = False,
340
    ):
341
        """Creates a new RasterVectorJoin operator."""
342
        self.raster_source = raster_sources
1✔
343
        self.vector_source = vector_source
1✔
344
        self.names = names
1✔
345
        self.temporal_aggregation = temporal_aggregation
1✔
346
        self.temporal_aggregation_ignore_nodata = temporal_aggregation_ignore_nodata
1✔
347
        self.feature_aggregation = feature_aggregation
1✔
348
        self.feature_aggregation_ignore_nodata = feature_aggregation_ignore_nodata
1✔
349

350
    def name(self) -> str:
1✔
351
        return "RasterVectorJoin"
1✔
352

353
    def to_dict(self) -> dict[str, Any]:
1✔
354
        return {
1✔
355
            "type": self.name(),
356
            "params": {
357
                "names": self.names.to_dict(),
358
                "temporalAggregation": self.temporal_aggregation,
359
                "temporalAggregationIgnoreNoData": self.temporal_aggregation_ignore_nodata,
360
                "featureAggregation": self.feature_aggregation,
361
                "featureAggregationIgnoreNoData": self.feature_aggregation_ignore_nodata,
362
            },
363
            "sources": {
364
                "vector": self.vector_source.to_dict(),
365
                "rasters": [raster_source.to_dict() for raster_source in self.raster_source],
366
            },
367
        }
368

369
    @classmethod
1✔
370
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterVectorJoin:
1✔
371
        """Returns an operator from a dictionary."""
372
        if operator_dict["type"] != "RasterVectorJoin":
1✔
373
            raise ValueError("Invalid operator type")
×
374

375
        vector_source = VectorOperator.from_operator_dict(cast(dict[str, Any], operator_dict["sources"]["vector"]))
1✔
376
        raster_sources = [
1✔
377
            RasterOperator.from_operator_dict(raster_source)
378
            for raster_source in cast(list[dict[str, Any]], operator_dict["sources"]["rasters"])
379
        ]
380

381
        params = operator_dict["params"]
1✔
382
        return RasterVectorJoin(
1✔
383
            raster_sources=raster_sources,
384
            vector_source=vector_source,
385
            names=ColumnNames.from_dict(params["names"]),
386
            temporal_aggregation=cast(Literal["none", "first", "mean"], params["temporalAggregation"]),
387
            temporal_aggregation_ignore_nodata=cast(bool, params["temporalAggregationIgnoreNoData"]),
388
            feature_aggregation=cast(Literal["first", "mean"], params["featureAggregation"]),
389
            feature_aggregation_ignore_nodata=cast(bool, params["featureAggregationIgnoreNoData"]),
390
        )
391

392

393
class PointInPolygonFilter(VectorOperator):
1✔
394
    """A PointInPolygonFilter operator."""
395

396
    point_source: VectorOperator
1✔
397
    polygon_source: VectorOperator
1✔
398

399
    def __init__(
1✔
400
        self,
401
        point_source: VectorOperator,
402
        polygon_source: VectorOperator,
403
    ):
404
        """Creates a new PointInPolygonFilter filter operator."""
405
        self.point_source = point_source
1✔
406
        self.polygon_source = polygon_source
1✔
407

408
    def name(self) -> str:
1✔
409
        return "PointInPolygonFilter"
1✔
410

411
    def to_dict(self) -> dict[str, Any]:
1✔
412
        return {
1✔
413
            "type": self.name(),
414
            "params": {},
415
            "sources": {"points": self.point_source.to_dict(), "polygons": self.polygon_source.to_dict()},
416
        }
417

418
    @classmethod
1✔
419
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> PointInPolygonFilter:
1✔
420
        """Returns an operator from a dictionary."""
421
        if operator_dict["type"] != "PointInPolygonFilter":
1✔
422
            raise ValueError("Invalid operator type")
×
423

424
        point_source = VectorOperator.from_operator_dict(cast(dict[str, Any], operator_dict["sources"]["points"]))
1✔
425
        polygon_source = VectorOperator.from_operator_dict(cast(dict[str, Any], operator_dict["sources"]["polygons"]))
1✔
426

427
        return PointInPolygonFilter(
1✔
428
            point_source=point_source,
429
            polygon_source=polygon_source,
430
        )
431

432

433
class RasterScaling(RasterOperator):
1✔
434
    """A RasterScaling operator.
435

436
    This operator scales the values of a raster by a given slope and offset.
437

438
    The scaling is done as follows:
439
    y = (x - offset) / slope
440

441
    The unscale mode is the inverse of the scale mode:
442
    x = y * slope + offset
443

444
    """
445

446
    source: RasterOperator
1✔
447
    slope: float | str | None = None
1✔
448
    offset: float | str | None = None
1✔
449
    scaling_mode: Literal["mulSlopeAddOffset", "subOffsetDivSlope"] = "mulSlopeAddOffset"
1✔
450
    output_measurement: str | None = None
1✔
451

452
    def __init__(
1✔
453
        self,
454
        # pylint: disable=too-many-arguments,too-many-positional-arguments
455
        source: RasterOperator,
456
        slope: float | str | None = None,
457
        offset: float | str | None = None,
458
        scaling_mode: Literal["mulSlopeAddOffset", "subOffsetDivSlope"] = "mulSlopeAddOffset",
459
        output_measurement: str | None = None,
460
    ):
461
        """Creates a new RasterScaling operator."""
462
        self.source = source
1✔
463
        self.slope = slope
1✔
464
        self.offset = offset
1✔
465
        self.scaling_mode = scaling_mode
1✔
466
        self.output_measurement = output_measurement
1✔
467
        if output_measurement is not None:
1✔
468
            raise NotImplementedError("Custom output measurement is not yet implemented")
×
469

470
    def name(self) -> str:
1✔
471
        return "RasterScaling"
1✔
472

473
    def to_dict(self) -> dict[str, Any]:
1✔
474
        def offset_scale_dict(key_or_value: float | str | None) -> dict[str, Any]:
1✔
475
            if key_or_value is None:
1✔
476
                return {"type": "auto"}
1✔
477

478
            if isinstance(key_or_value, float):
1✔
479
                return {"type": "constant", "value": key_or_value}
1✔
480

481
            if isinstance(key_or_value, int):
×
482
                return {"type": "constant", "value": float(key_or_value)}
×
483

484
            # TODO: incorporate `domain` field
485
            return {"type": "metadataKey", "key": key_or_value}
×
486

487
        return {
1✔
488
            "type": self.name(),
489
            "params": {
490
                "offset": offset_scale_dict(self.offset),
491
                "slope": offset_scale_dict(self.slope),
492
                "scalingMode": self.scaling_mode,
493
            },
494
            "sources": {"raster": self.source.to_dict()},
495
        }
496

497
    @classmethod
1✔
498
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterScaling:
1✔
499
        if operator_dict["type"] != "RasterScaling":
1✔
500
            raise ValueError("Invalid operator type")
×
501

502
        source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
1✔
503
        params = operator_dict["params"]
1✔
504

505
        def offset_slope_reverse(key_or_value: dict[str, Any] | None) -> float | str | None:
1✔
506
            if key_or_value is None:
1✔
507
                return None
×
508
            if key_or_value["type"] == "constant":
1✔
509
                return key_or_value["value"]
1✔
510
            if key_or_value["type"] == "metadataKey":
1✔
511
                return key_or_value["key"]
×
512
            return None
1✔
513

514
        return RasterScaling(
1✔
515
            source_operator,
516
            slope=offset_slope_reverse(params["slope"]),
517
            offset=offset_slope_reverse(params["offset"]),
518
            scaling_mode=params["scalingMode"],
519
            output_measurement=params.get("outputMeasurement", None),
520
        )
521

522

523
class RasterTypeConversion(RasterOperator):
1✔
524
    """A RasterTypeConversion operator."""
525

526
    source: RasterOperator
1✔
527
    output_data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"]
1✔
528

529
    def __init__(
1✔
530
        self,
531
        source: RasterOperator,
532
        output_data_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"],
533
    ):
534
        """Creates a new RasterTypeConversion operator."""
535
        self.source = source
1✔
536
        self.output_data_type = output_data_type
1✔
537

538
    def name(self) -> str:
1✔
539
        return "RasterTypeConversion"
1✔
540

541
    def to_dict(self) -> dict[str, Any]:
1✔
542
        return {
1✔
543
            "type": self.name(),
544
            "params": {"outputDataType": self.output_data_type},
545
            "sources": {"raster": self.source.to_dict()},
546
        }
547

548
    @classmethod
1✔
549
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterTypeConversion:
1✔
550
        if operator_dict["type"] != "RasterTypeConversion":
1✔
551
            raise ValueError("Invalid operator type")
×
552

553
        source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
1✔
554

555
        return RasterTypeConversion(source_operator, output_data_type=operator_dict["params"]["outputDataType"])
1✔
556

557

558
class Reprojection(Operator):
1✔
559
    """A Reprojection operator."""
560

561
    source: Operator
1✔
562
    target_spatial_reference: str
1✔
563

564
    def __init__(self, source: Operator, target_spatial_reference: str):
1✔
565
        """Creates a new Reprojection operator."""
566
        self.source = source
1✔
567
        self.target_spatial_reference = target_spatial_reference
1✔
568

569
    def data_type(self) -> Literal["Raster", "Vector"]:
1✔
570
        return self.source.data_type()
×
571

572
    def name(self) -> str:
1✔
573
        return "Reprojection"
1✔
574

575
    def to_dict(self) -> dict[str, Any]:
1✔
576
        return {
1✔
577
            "type": self.name(),
578
            "params": {"targetSpatialReference": self.target_spatial_reference},
579
            "sources": {"source": self.source.to_dict()},
580
        }
581

582
    def as_vector(self) -> VectorOperator:
1✔
583
        """Casts this operator to a VectorOperator."""
584
        if self.data_type() != "Vector":
×
585
            raise TypeError("Cannot cast to VectorOperator")
×
586
        return cast(VectorOperator, self)
×
587

588
    def as_raster(self) -> RasterOperator:
1✔
589
        """Casts this operator to a RasterOperator."""
590
        if self.data_type() != "Raster":
×
591
            raise TypeError("Cannot cast to RasterOperator")
×
592
        return cast(RasterOperator, self)
×
593

594
    @classmethod
1✔
595
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Reprojection:
1✔
596
        """Constructs the operator from the given dictionary."""
597
        if operator_dict["type"] != "Reprojection":
1✔
598
            raise ValueError("Invalid operator type")
×
599

600
        source_operator: RasterOperator | VectorOperator
601
        try:
1✔
602
            source_operator = RasterOperator.from_operator_dict(operator_dict["sources"]["source"])
1✔
603
        except ValueError:
×
604
            source_operator = VectorOperator.from_operator_dict(operator_dict["sources"]["source"])
×
605

606
        return Reprojection(
1✔
607
            source=cast(Operator, source_operator),
608
            target_spatial_reference=operator_dict["params"]["targetSpatialReference"],
609
        )
610

611

612
class Expression(RasterOperator):
1✔
613
    """An Expression operator."""
614

615
    expression: str
1✔
616
    source: RasterOperator
1✔
617
    output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32"
1✔
618
    map_no_data: bool = False
1✔
619
    output_band: RasterBandDescriptor | None = None
1✔
620

621
    # pylint: disable=too-many-arguments,too-many-positional-arguments
622
    def __init__(
1✔
623
        self,
624
        expression: str,
625
        source: RasterOperator,
626
        output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32",
627
        map_no_data: bool = False,
628
        output_band: RasterBandDescriptor | None = None,
629
    ):
630
        """Creates a new Expression operator."""
631
        self.expression = expression
1✔
632
        self.source = source
1✔
633
        self.output_type = output_type
1✔
634
        self.map_no_data = map_no_data
1✔
635
        self.output_band = output_band
1✔
636

637
    def name(self) -> str:
1✔
638
        return "Expression"
1✔
639

640
    def to_dict(self) -> dict[str, Any]:
1✔
641
        params = {
1✔
642
            "expression": self.expression,
643
            "outputType": self.output_type,
644
            "mapNoData": self.map_no_data,
645
        }
646
        if self.output_band is not None:
1✔
647
            params["outputBand"] = self.output_band.to_api_dict().to_dict()
1✔
648

649
        return {"type": self.name(), "params": params, "sources": {"raster": self.source.to_dict()}}
1✔
650

651
    @classmethod
1✔
652
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Expression:
1✔
653
        if operator_dict["type"] != "Expression":
1✔
654
            raise ValueError("Invalid operator type")
×
655

656
        output_band = None
1✔
657
        if "outputBand" in operator_dict["params"] and operator_dict["params"]["outputBand"] is not None:
1✔
658
            raster_band_descriptor = geoengine_openapi_client.RasterBandDescriptor.from_dict(
1✔
659
                operator_dict["params"]["outputBand"]
660
            )
661
            if raster_band_descriptor is None:
1✔
662
                raise ValueError("Invalid output band")
×
663
            output_band = RasterBandDescriptor.from_response(raster_band_descriptor)
1✔
664

665
        return Expression(
1✔
666
            expression=operator_dict["params"]["expression"],
667
            source=RasterOperator.from_operator_dict(operator_dict["sources"]["raster"]),
668
            output_type=operator_dict["params"]["outputType"],
669
            map_no_data=operator_dict["params"]["mapNoData"],
670
            output_band=output_band,
671
        )
672

673

674
class BandwiseExpression(RasterOperator):
1✔
675
    """A bandwise Expression operator."""
676

677
    expression: str
1✔
678
    source: RasterOperator
1✔
679
    output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32"
1✔
680
    map_no_data: bool = False
1✔
681

682
    # pylint: disable=too-many-arguments
683
    def __init__(
1✔
684
        self,
685
        expression: str,
686
        source: RasterOperator,
687
        output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] = "F32",
688
        map_no_data: bool = False,
689
    ):
690
        """Creates a new Expression operator."""
691
        self.expression = expression
×
692
        self.source = source
×
693
        self.output_type = output_type
×
694
        self.map_no_data = map_no_data
×
695

696
    def name(self) -> str:
1✔
697
        return "BandwiseExpression"
×
698

699
    def to_dict(self) -> dict[str, Any]:
1✔
700
        params = {
×
701
            "expression": self.expression,
702
            "outputType": self.output_type,
703
            "mapNoData": self.map_no_data,
704
        }
705

706
        return {"type": self.name(), "params": params, "sources": {"raster": self.source.to_dict()}}
×
707

708
    @classmethod
1✔
709
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> BandwiseExpression:
1✔
710
        if operator_dict["type"] != "BandwiseExpression":
×
711
            raise ValueError("Invalid operator type")
×
712

713
        return BandwiseExpression(
×
714
            expression=operator_dict["params"]["expression"],
715
            source=RasterOperator.from_operator_dict(operator_dict["sources"]["raster"]),
716
            output_type=operator_dict["params"]["outputType"],
717
            map_no_data=operator_dict["params"]["mapNoData"],
718
        )
719

720

721
class GeoVectorDataType(Enum):
1✔
722
    """The output type of geometry vector data."""
723

724
    MULTI_POINT = "MultiPoint"
1✔
725
    MULTI_LINE_STRING = "MultiLineString"
1✔
726
    MULTI_POLYGON = "MultiPolygon"
1✔
727

728

729
class VectorExpression(VectorOperator):
1✔
730
    """The `VectorExpression` operator."""
731

732
    source: VectorOperator
1✔
733

734
    expression: str
1✔
735
    input_columns: list[str]
1✔
736
    output_column: str | GeoVectorDataType
1✔
737
    geometry_column_name = None
1✔
738
    output_measurement: Measurement | None = None
1✔
739

740
    # pylint: disable=too-many-arguments
741
    def __init__(
1✔
742
        self,
743
        source: VectorOperator,
744
        *,
745
        expression: str,
746
        input_columns: list[str],
747
        output_column: str | GeoVectorDataType,
748
        geometry_column_name: str | None = None,
749
        output_measurement: Measurement | None = None,
750
    ):
751
        """Creates a new VectorExpression operator."""
752
        self.source = source
×
753

754
        self.expression = expression
×
755
        self.input_columns = input_columns
×
756
        self.output_column = output_column
×
757

758
        self.geometry_column_name = geometry_column_name
×
759
        self.output_measurement = output_measurement
×
760

761
    def name(self) -> str:
1✔
762
        return "VectorExpression"
×
763

764
    def to_dict(self) -> dict[str, Any]:
1✔
765
        output_column_dict = None
×
766
        if isinstance(self.output_column, GeoVectorDataType):
×
767
            output_column_dict = {
×
768
                "type": "geometry",
769
                "value": self.output_column.value,
770
            }
771
        elif isinstance(self.output_column, str):
×
772
            output_column_dict = {
×
773
                "type": "column",
774
                "value": self.output_column,
775
            }
776
        else:
777
            raise NotImplementedError("Invalid output column type")
×
778

779
        params = {
×
780
            "expression": self.expression,
781
            "inputColumns": self.input_columns,
782
            "outputColumn": output_column_dict,
783
        }  # type: dict[str, Any]
784

785
        if self.geometry_column_name:
×
786
            params["geometryColumnName"] = self.geometry_column_name
×
787

788
        if self.output_measurement:
×
789
            params["outputMeasurement"] = self.output_measurement.to_api_dict().to_dict()
×
790

791
        return {"type": self.name(), "params": params, "sources": {"vector": self.source.to_dict()}}
×
792

793
    @classmethod
1✔
794
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> VectorExpression:
1✔
795
        if operator_dict["type"] != "Expression":
×
796
            raise ValueError("Invalid operator type")
×
797

798
        geometry_column_name = None
×
799
        if "geometryColumnName" in operator_dict["params"]:
×
800
            geometry_column_name = operator_dict["params"]["geometryColumnName"]
×
801

802
        output_measurement = None
×
803
        if "outputMeasurement" in operator_dict["params"]:
×
804
            output_measurement = Measurement.from_response(operator_dict["params"]["outputMeasurement"])
×
805

806
        return VectorExpression(
×
807
            source=VectorOperator.from_operator_dict(operator_dict["sources"]["vector"]),
808
            expression=operator_dict["params"]["expression"],
809
            input_columns=operator_dict["params"]["inputColumns"],
810
            output_column=operator_dict["params"]["outputColumn"],
811
            geometry_column_name=geometry_column_name,
812
            output_measurement=output_measurement,
813
        )
814

815

816
class TemporalRasterAggregation(RasterOperator):
1✔
817
    """A TemporalRasterAggregation operator."""
818

819
    # pylint: disable=too-many-instance-attributes
820

821
    source: RasterOperator
1✔
822
    aggregation_type: Literal["mean", "min", "max", "median", "count", "sum", "first", "last", "percentileEstimate"]
1✔
823
    ignore_no_data: bool = False
1✔
824
    window_granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"] = "days"
1✔
825
    window_size: int = 1
1✔
826
    output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] | None = None
1✔
827
    percentile: float | None = None
1✔
828
    window_ref: np.datetime64 | None = None
1✔
829

830
    # pylint: disable=too-many-arguments,too-many-positional-arguments
831
    def __init__(
1✔
832
        self,
833
        source: RasterOperator,
834
        aggregation_type: Literal[
835
            "mean", "min", "max", "median", "count", "sum", "first", "last", "percentileEstimate"
836
        ],
837
        ignore_no_data: bool = False,
838
        granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"] = "days",
839
        window_size: int = 1,
840
        output_type: Literal["U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "F32", "F64"] | None = None,
841
        percentile: float | None = None,
842
        window_reference: datetime.datetime | np.datetime64 | None = None,
843
    ):
844
        """Creates a new TemporalRasterAggregation operator."""
845
        self.source = source
1✔
846
        self.aggregation_type = aggregation_type
1✔
847
        self.ignore_no_data = ignore_no_data
1✔
848
        self.window_granularity = granularity
1✔
849
        self.window_size = window_size
1✔
850
        self.output_type = output_type
1✔
851
        if self.aggregation_type == "percentileEstimate":
1✔
852
            if percentile is None:
×
853
                raise ValueError("Percentile must be set for percentileEstimate")
×
854
            if percentile <= 0.0 or percentile > 1.0:
×
855
                raise ValueError("Percentile must be > 0.0 and <= 1.0")
×
856
            self.percentile = percentile
×
857
        if window_reference is not None:
1✔
858
            if isinstance(window_reference, np.datetime64):
×
859
                self.window_ref = window_reference
×
860
            elif isinstance(window_reference, datetime.datetime):
×
861
                # We assume that a datetime without a timezone means UTC
862
                if window_reference.tzinfo is not None:
×
863
                    window_reference = window_reference.astimezone(tz=datetime.timezone.utc).replace(tzinfo=None)
×
864
                self.window_ref = np.datetime64(window_reference)
×
865
            else:
866
                raise ValueError("`window_reference` must be of type `datetime.datetime` or `numpy.datetime64`")
×
867

868
    def name(self) -> str:
1✔
869
        return "TemporalRasterAggregation"
1✔
870

871
    def to_dict(self) -> dict[str, Any]:
1✔
872
        w_ref = self.window_ref.astype("datetime64[ms]").astype(int) if self.window_ref is not None else None
1✔
873

874
        return {
1✔
875
            "type": self.name(),
876
            "params": {
877
                "aggregation": {
878
                    "type": self.aggregation_type,
879
                    "ignoreNoData": self.ignore_no_data,
880
                    "percentile": self.percentile,
881
                },
882
                "window": {"granularity": self.window_granularity, "step": self.window_size},
883
                "windowReference": w_ref,
884
                "outputType": self.output_type,
885
            },
886
            "sources": {"raster": self.source.to_dict()},
887
        }
888

889
    @classmethod
1✔
890
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> TemporalRasterAggregation:
1✔
891
        if operator_dict["type"] != "TemporalRasterAggregation":
1✔
892
            raise ValueError("Invalid operator type")
×
893

894
        w_ref: datetime.datetime | np.datetime64 | None = None
1✔
895
        if "windowReference" in operator_dict["params"]:
1✔
896
            t_ref = operator_dict["params"]["windowReference"]
1✔
897
            if isinstance(t_ref, str):
1✔
898
                w_ref = datetime.datetime.fromisoformat(t_ref)
×
899
            if isinstance(t_ref, int):
1✔
900
                w_ref = np.datetime64(t_ref, "ms")
×
901

902
        percentile = None
1✔
903
        if "percentile" in operator_dict["params"]["aggregation"]:
1✔
904
            percentile = operator_dict["params"]["aggregation"]["percentile"]
1✔
905

906
        return TemporalRasterAggregation(
1✔
907
            source=RasterOperator.from_operator_dict(operator_dict["sources"]["raster"]),
908
            aggregation_type=operator_dict["params"]["aggregation"]["type"],
909
            ignore_no_data=operator_dict["params"]["aggregation"]["ignoreNoData"],
910
            granularity=operator_dict["params"]["window"]["granularity"],
911
            window_size=operator_dict["params"]["window"]["step"],
912
            output_type=operator_dict["params"]["outputType"],
913
            window_reference=w_ref,
914
            percentile=percentile,
915
        )
916

917

918
class TimeShift(Operator):
1✔
919
    """A RasterTypeConversion operator."""
920

921
    source: RasterOperator | VectorOperator
1✔
922
    shift_type: Literal["relative", "absolute"]
1✔
923
    granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"]
1✔
924
    value: int
1✔
925

926
    def __init__(
1✔
927
        self,
928
        source: RasterOperator | VectorOperator,
929
        shift_type: Literal["relative", "absolute"],
930
        granularity: Literal["days", "months", "years", "hours", "minutes", "seconds", "millis"],
931
        value: int,
932
    ):
933
        """Creates a new RasterTypeConversion operator."""
934
        if shift_type == "absolute":
1✔
935
            raise NotImplementedError("Absolute time shifts are not supported yet")
×
936
        self.source = source
1✔
937
        self.shift_type = shift_type
1✔
938
        self.granularity = granularity
1✔
939
        self.value = value
1✔
940

941
    def name(self) -> str:
1✔
942
        return "TimeShift"
1✔
943

944
    def data_type(self) -> Literal["Vector", "Raster"]:
1✔
945
        return self.source.data_type()
×
946

947
    def as_vector(self) -> VectorOperator:
1✔
948
        """Casts this operator to a VectorOperator."""
949
        if self.data_type() != "Vector":
×
950
            raise TypeError("Cannot cast to VectorOperator")
×
951
        return cast(VectorOperator, self)
×
952

953
    def as_raster(self) -> RasterOperator:
1✔
954
        """Casts this operator to a RasterOperator."""
955
        if self.data_type() != "Raster":
×
956
            raise TypeError("Cannot cast to RasterOperator")
×
957
        return cast(RasterOperator, self)
×
958

959
    def to_dict(self) -> dict[str, Any]:
1✔
960
        return {
1✔
961
            "type": self.name(),
962
            "params": {"type": self.shift_type, "granularity": self.granularity, "value": self.value},
963
            "sources": {"source": self.source.to_dict()},
964
        }
965

966
    @classmethod
1✔
967
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> TimeShift:
1✔
968
        """Constructs the operator from the given dictionary."""
969
        if operator_dict["type"] != "TimeShift":
×
970
            raise ValueError("Invalid operator type")
×
971
        source: RasterOperator | VectorOperator
972
        try:
×
973
            source = VectorOperator.from_operator_dict(operator_dict["sources"]["source"])
×
974
        except ValueError:
×
975
            source = RasterOperator.from_operator_dict(operator_dict["sources"]["source"])
×
976

977
        return TimeShift(
×
978
            source=source,
979
            shift_type=operator_dict["params"]["type"],
980
            granularity=operator_dict["params"]["granularity"],
981
            value=operator_dict["params"]["value"],
982
        )
983

984

985
class RenameBands:
1✔
986
    """Base class for renaming bands of a raster."""
987

988
    @abstractmethod
1✔
989
    def to_dict(self) -> dict[str, Any]:
1✔
990
        pass
×
991

992
    @classmethod
1✔
993
    def from_dict(cls, rename_dict: dict[str, Any]) -> RenameBands:
1✔
994
        """Returns a RenameBands object from a dictionary."""
995
        if rename_dict["type"] == "default":
×
996
            return RenameBandsDefault()
×
997
        if rename_dict["type"] == "suffix":
×
998
            return RenameBandsSuffix(cast(list[str], rename_dict["values"]))
×
999
        if rename_dict["type"] == "rename":
×
1000
            return RenameBandsRename(cast(list[str], rename_dict["values"]))
×
1001
        raise ValueError("Invalid rename type")
×
1002

1003
    @classmethod
1✔
1004
    def default(cls) -> RenameBands:
1✔
1005
        return RenameBandsDefault()
×
1006

1007
    @classmethod
1✔
1008
    def suffix(cls, values: list[str]) -> RenameBands:
1✔
1009
        return RenameBandsSuffix(values)
×
1010

1011
    @classmethod
1✔
1012
    def rename(cls, values: list[str]) -> RenameBands:
1✔
1013
        return RenameBandsRename(values)
×
1014

1015

1016
class RenameBandsDefault(RenameBands):
1✔
1017
    """Rename bands with default suffix."""
1018

1019
    def to_dict(self) -> dict[str, Any]:
1✔
1020
        return {"type": "default"}
1✔
1021

1022

1023
class RenameBandsSuffix(RenameBands):
1✔
1024
    """Rename bands with custom suffixes."""
1025

1026
    suffixes: list[str]
1✔
1027

1028
    def __init__(self, suffixes: list[str]) -> None:
1✔
1029
        self.suffixes = suffixes
×
1030
        super().__init__()
×
1031

1032
    def to_dict(self) -> dict[str, Any]:
1✔
1033
        return {"type": "suffix", "values": self.suffixes}
×
1034

1035

1036
class RenameBandsRename(RenameBands):
1✔
1037
    """Rename bands with new names."""
1038

1039
    new_names: list[str]
1✔
1040

1041
    def __init__(self, new_names: list[str]) -> None:
1✔
1042
        self.new_names = new_names
×
1043
        super().__init__()
×
1044

1045
    def to_dict(self) -> dict[str, Any]:
1✔
1046
        return {"type": "rename", "values": self.new_names}
×
1047

1048

1049
class RasterStacker(RasterOperator):
1✔
1050
    """The RasterStacker operator."""
1051

1052
    sources: list[RasterOperator]
1✔
1053
    rename: RenameBands
1✔
1054

1055
    # pylint: disable=too-many-arguments
1056
    def __init__(self, sources: list[RasterOperator], rename: RenameBands | None = None):
1✔
1057
        """Creates a new RasterStacker operator."""
1058
        if rename is None:
1✔
1059
            rename = RenameBandsDefault()
1✔
1060

1061
        self.sources = sources
1✔
1062
        self.rename = rename
1✔
1063

1064
    def name(self) -> str:
1✔
1065
        return "RasterStacker"
1✔
1066

1067
    def to_dict(self) -> dict[str, Any]:
1✔
1068
        return {
1✔
1069
            "type": self.name(),
1070
            "params": {"renameBands": self.rename.to_dict()},
1071
            "sources": {"rasters": [raster_source.to_dict() for raster_source in self.sources]},
1072
        }
1073

1074
    @classmethod
1✔
1075
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> RasterStacker:
1✔
1076
        if operator_dict["type"] != "RasterStacker":
×
1077
            raise ValueError("Invalid operator type")
×
1078

1079
        sources = [RasterOperator.from_operator_dict(source) for source in operator_dict["sources"]["rasters"]]
×
1080
        rename = RenameBands.from_dict(operator_dict["params"]["renameBands"])
×
1081

1082
        return RasterStacker(sources=sources, rename=rename)
×
1083

1084

1085
class BandNeighborhoodAggregate(RasterOperator):
1✔
1086
    """The BandNeighborhoodAggregate operator."""
1087

1088
    source: RasterOperator
1✔
1089
    aggregate: BandNeighborhoodAggregateParams
1✔
1090

1091
    # pylint: disable=too-many-arguments
1092
    def __init__(self, source: RasterOperator, aggregate: BandNeighborhoodAggregateParams):
1✔
1093
        """Creates a new BandNeighborhoodAggregate operator."""
1094
        self.source = source
×
1095
        self.aggregate = aggregate
×
1096

1097
    def name(self) -> str:
1✔
1098
        return "BandNeighborhoodAggregate"
×
1099

1100
    def to_dict(self) -> dict[str, Any]:
1✔
1101
        return {
×
1102
            "type": self.name(),
1103
            "params": {"aggregate": self.aggregate.to_dict()},
1104
            "sources": {"raster": self.source.to_dict()},
1105
        }
1106

1107
    @classmethod
1✔
1108
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> BandNeighborhoodAggregate:
1✔
1109
        if operator_dict["type"] != "BandNeighborhoodAggregate":
×
1110
            raise ValueError("Invalid operator type")
×
1111

1112
        source = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
×
1113
        aggregate = BandNeighborhoodAggregateParams.from_dict(operator_dict["params"]["aggregate"])
×
1114

1115
        return BandNeighborhoodAggregate(source=source, aggregate=aggregate)
×
1116

1117

1118
class BandNeighborhoodAggregateParams:
1✔
1119
    """Abstract base class for band neighborhood aggregate params."""
1120

1121
    @abstractmethod
1✔
1122
    def to_dict(self) -> dict[str, Any]:
1✔
1123
        pass
×
1124

1125
    @classmethod
1✔
1126
    def from_dict(cls, band_neighborhood_aggregate_dict: dict[str, Any]) -> BandNeighborhoodAggregateParams:
1✔
1127
        """Returns a BandNeighborhoodAggregate object from a dictionary."""
1128
        if band_neighborhood_aggregate_dict["type"] == "firstDerivative":
×
1129
            return BandNeighborhoodAggregateFirstDerivative.from_dict(band_neighborhood_aggregate_dict)
×
1130
        if band_neighborhood_aggregate_dict["type"] == "average":
×
1131
            return BandNeighborhoodAggregateAverage(band_neighborhood_aggregate_dict["windowSize"])
×
1132
        raise ValueError("Invalid neighborhood aggregate type")
×
1133

1134
    @classmethod
1✔
1135
    def first_derivative(cls, equally_spaced_band_distance: float) -> BandNeighborhoodAggregateParams:
1✔
1136
        return BandNeighborhoodAggregateFirstDerivative(equally_spaced_band_distance)
×
1137

1138
    @classmethod
1✔
1139
    def average(cls, window_size: int) -> BandNeighborhoodAggregateParams:
1✔
1140
        return BandNeighborhoodAggregateAverage(window_size)
×
1141

1142

1143
@dataclass
1✔
1144
class BandNeighborhoodAggregateFirstDerivative(BandNeighborhoodAggregateParams):
1✔
1145
    """The first derivative band neighborhood aggregate."""
1146

1147
    equally_spaced_band_distance: float
1✔
1148

1149
    @classmethod
1✔
1150
    def from_dict(cls, band_neighborhood_aggregate_dict: dict[str, Any]) -> BandNeighborhoodAggregateParams:
1✔
1151
        if band_neighborhood_aggregate_dict["type"] != "firstDerivative":
×
1152
            raise ValueError("Invalid neighborhood aggregate type")
×
1153

1154
        return BandNeighborhoodAggregateFirstDerivative(band_neighborhood_aggregate_dict["bandDistance"]["distance"])
×
1155

1156
    def to_dict(self) -> dict[str, Any]:
1✔
1157
        return {
×
1158
            "type": "firstDerivative",
1159
            "bandDistance": {"type": "equallySpaced", "distance": self.equally_spaced_band_distance},
1160
        }
1161

1162

1163
@dataclass
1✔
1164
class BandNeighborhoodAggregateAverage(BandNeighborhoodAggregateParams):
1✔
1165
    """The average band neighborhood aggregate."""
1166

1167
    window_size: int
1✔
1168

1169
    def to_dict(self) -> dict[str, Any]:
1✔
1170
        return {"type": "average", "windowSize": self.window_size}
×
1171

1172

1173
class Onnx(RasterOperator):
1✔
1174
    """Onnx ML operator."""
1175

1176
    source: RasterOperator
1✔
1177
    model: str
1✔
1178

1179
    # pylint: disable=too-many-arguments
1180
    def __init__(self, source: RasterOperator, model: str):
1✔
1181
        """Creates a new Onnx operator."""
1182
        self.source = source
×
1183
        self.model = model
×
1184

1185
    def name(self) -> str:
1✔
1186
        return "Onnx"
×
1187

1188
    def to_dict(self) -> dict[str, Any]:
1✔
1189
        return {"type": self.name(), "params": {"model": self.model}, "sources": {"raster": self.source.to_dict()}}
×
1190

1191
    @classmethod
1✔
1192
    def from_operator_dict(cls, operator_dict: dict[str, Any]) -> Onnx:
1✔
1193
        if operator_dict["type"] != "Onnx":
×
1194
            raise ValueError("Invalid operator type")
×
1195

1196
        source = RasterOperator.from_operator_dict(operator_dict["sources"]["raster"])
×
1197
        model = operator_dict["params"]["model"]
×
1198

1199
        return Onnx(source=source, model=model)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc