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

geo-engine / geoengine / 37755207466

08 Oct 2026 09:13AM UTC coverage: 87.424% (+0.003%) from 87.421%
37755207466

Pull #1325

github

web-flow
Merge 6216ee71d into c58cee21a
Pull Request #1325: feat: rename workflow to processing graph in API

132223 of 151244 relevant lines covered (87.42%)

549486.87 hits per line

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

81.29
/python/geoengine/layers.py
1
"""
2
A wrapper around the layer and layerDb API
3
"""
4

5
from __future__ import annotations
1 ✔
6

7
import json
1 ✔
8
import os
1 ✔
9
from dataclasses import dataclass
1 ✔
10
from enum import auto
1 ✔
11
from io import StringIO
1 ✔
12
from typing import Any, Generic, Literal, TypeVar, cast
1 ✔
13

14
import geoengine_api_client
1 ✔
15
from strenum import LowercaseStrEnum
1 ✔
16
from typing_extensions import deprecated
1 ✔
17

18
from geoengine._deprecation import renamed_parameter
1 ✔
19
from geoengine.auth import get_session
1 ✔
20
from geoengine.error import InputException, ModificationNotOnLayerDbException
1 ✔
21
from geoengine.permissions import Permission, RoleId, add_permission
1 ✔
22
from geoengine.processing_graph import ProcessingGraph, ProcessingGraphId
1 ✔
23
from geoengine.processing_graph_builder.operators import Operator as ProcessingGraphBuilderOperator
1 ✔
24
from geoengine.resource_identifier import LAYER_DB_PROVIDER_ID, LayerCollectionId, LayerId, LayerProviderId, Resource
1 ✔
25
from geoengine.tasks import Task, TaskId
1 ✔
26
from geoengine.types import Symbology
1 ✔
27

28

29
class LayerCollectionListingType(LowercaseStrEnum):
1 ✔
30
    LAYER = auto()
1 ✔
31
    COLLECTION = auto()
1 ✔
32

33

34
LISTINGID = TypeVar("LISTINGID")
1 ✔
35

36

37
@dataclass(repr=False)
1 ✔
38
class Listing(Generic[LISTINGID]):
1 ✔
39
    """A listing item of a collection"""
40

41
    listing_id: LISTINGID
1 ✔
42
    provider_id: LayerProviderId
1 ✔
43
    name: str
1 ✔
44
    description: str
1 ✔
45

46
    def __repr__(self) -> str:
1 ✔
47
        """String representation of a `Listing`"""
48

49
        buf = StringIO()
1 ✔
50

51
        buf.write(f"{self._type_str()}{os.linesep}")
1 ✔
52
        buf.write(f"name: {self.name}{os.linesep}")
1 ✔
53
        buf.write(f"description: {self.description}{os.linesep}")
1 ✔
54
        buf.write(f"id: {self.listing_id}{os.linesep}")
1 ✔
55
        buf.write(f"provider id: {self.provider_id}{os.linesep}")
1 ✔
56

57
        return buf.getvalue()
1 ✔
58

59
    def html_str(self) -> str:
1 ✔
60
        """HTML representation for Jupyter notebooks"""
61

62
        buf = StringIO()
×
63

64
        buf.write("<table>")
×
65
        buf.write(f'<thead><tr><th colspan="2">{self._type_str()}</th></tr></thead>')
×
66
        buf.write("<tbody>")
×
67
        buf.write(f"<tr><th>name</th><td>{self.name}</td></tr>")
×
68
        buf.write(f"<tr><th>description</th><td>{self.description}</td></tr>")
×
69
        buf.write(f"<tr><th>id</th><td>{self.listing_id}</td></tr>")
×
70
        buf.write(f"<tr><th>provider id</th><td>{self.provider_id}</td></tr>")
×
71
        buf.write("</tbody>")
×
72
        buf.write("</table>")
×
73

74
        return buf.getvalue()
×
75

76
    def _repr_html_(self) -> str:
1 ✔
77
        """HTML representation for Jupyter notebooks"""
78

79
        return self.html_str()
×
80

81
    def _type_str(self) -> str:
1 ✔
82
        """String representation of the listing type"""
83
        raise NotImplementedError("Please Implement this method")
×
84

85
    def load(self, timeout: int = 60) -> LayerCollection | Layer:
1 ✔
86
        """Load the listing item"""
87
        raise NotImplementedError("Please Implement this method")
×
88

89
    def _remove(self, collection_id: LayerCollectionId, provider_id: LayerProviderId, timeout: int = 60) -> None:
1 ✔
90
        """Remove the item behind the listing from the parent collection"""
91
        raise NotImplementedError("Please Implement this method")
×
92

93

94
@dataclass(repr=False)
1 ✔
95
class LayerListing(Listing[LayerId]):
1 ✔
96
    """A layer listing as item of a collection"""
97

98
    def _type_str(self) -> str:
1 ✔
99
        """String representation of the listing type"""
100

101
        return "Layer"
1 ✔
102

103
    def load(self, timeout: int = 60) -> LayerCollection | Layer:
1 ✔
104
        """Load the listing item"""
105

106
        return layer(self.listing_id, self.provider_id, timeout)
1 ✔
107

108
    def _remove(self, collection_id: LayerCollectionId, provider_id: LayerProviderId, timeout: int = 60) -> None:
1 ✔
109
        if provider_id != LAYER_DB_PROVIDER_ID:
1 ✔
110
            raise ModificationNotOnLayerDbException("Layer collection is not stored in the layer database")
×
111

112
        _delete_layer_from_collection(
1 ✔
113
            collection_id,
114
            self.listing_id,
115
            timeout,
116
        )
117

118

119
@dataclass(repr=False)
1 ✔
120
class LayerCollectionListing(Listing[LayerCollectionId]):
1 ✔
121
    """A layer listing as item of a collection"""
122

123
    def _type_str(self) -> str:
1 ✔
124
        """String representation of the listing type"""
125

126
        return "Layer Collection"
1 ✔
127

128
    def load(self, timeout: int = 60) -> LayerCollection | Layer:
1 ✔
129
        """Load the listing item"""
130

131
        return layer_collection(self.listing_id, self.provider_id, timeout)
1 ✔
132

133
    def _remove(self, collection_id: LayerCollectionId, provider_id: LayerProviderId, timeout: int = 60) -> None:
1 ✔
134
        if provider_id != LAYER_DB_PROVIDER_ID:
1 ✔
135
            raise ModificationNotOnLayerDbException("Layer collection is not stored in the layer database")
×
136

137
        _delete_layer_collection_from_collection(
1 ✔
138
            collection_id,
139
            self.listing_id,
140
            timeout,
141
        )
142

143

144
class LayerCollection:
1 ✔
145
    """A layer collection"""
146

147
    name: str
1 ✔
148
    description: str
1 ✔
149
    collection_id: LayerCollectionId
1 ✔
150
    provider_id: LayerProviderId
1 ✔
151
    items: list[Listing]
1 ✔
152

153
    # pylint: disable=too-many-arguments,too-many-positional-arguments
154
    def __init__(
1 ✔
155
        self,
156
        name: str,
157
        description: str,
158
        collection_id: LayerCollectionId,
159
        provider_id: LayerProviderId,
160
        items: list[Listing],
161
    ) -> None:
162
        """Create a new `LayerCollection`"""
163

164
        self.name = name
1 ✔
165
        self.description = description
1 ✔
166
        self.collection_id = collection_id
1 ✔
167
        self.provider_id = provider_id
1 ✔
168
        self.items = items
1 ✔
169

170
    @classmethod
1 ✔
171
    def from_response(cls, response_pages: list[geoengine_api_client.LayerCollection]) -> LayerCollection:
1 ✔
172
        """Parse an HTTP JSON response to an `LayerCollection`"""
173

174
        assert len(response_pages) > 0, "No response pages"
1 ✔
175

176
        def parse_listing(response: geoengine_api_client.CollectionItem) -> Listing:
1 ✔
177
            inner = response.actual_instance
1 ✔
178
            if inner is None:
1 ✔
179
                raise AssertionError("Invalid listing type")
×
180

181
            item_type = LayerCollectionListingType(inner.type)
1 ✔
182

183
            if item_type is LayerCollectionListingType.LAYER:
1 ✔
184
                layer_id_response = cast(geoengine_api_client.ProviderLayerId, inner.id)
1 ✔
185
                return LayerListing(
1 ✔
186
                    listing_id=LayerId(layer_id_response.layer_id),
187
                    provider_id=LayerProviderId(layer_id_response.provider_id),
188
                    name=inner.name,
189
                    description=inner.description,
190
                )
191

192
            if item_type is LayerCollectionListingType.COLLECTION:
1 ✔
193
                collection_id_response = cast(geoengine_api_client.ProviderLayerCollectionId, inner.id)
1 ✔
194
                return LayerCollectionListing(
1 ✔
195
                    listing_id=LayerCollectionId(collection_id_response.collection_id),
196
                    provider_id=LayerProviderId(collection_id_response.provider_id),
197
                    name=inner.name,
198
                    description=inner.description,
199
                )
200

201
            raise AssertionError("Invalid listing type")
×
202

203
        items = []
1 ✔
204
        for response in response_pages:
1 ✔
205
            for item_response in response.items:
1 ✔
206
                items.append(parse_listing(item_response))
1 ✔
207

208
        response = response_pages[0]
1 ✔
209

210
        return LayerCollection(
1 ✔
211
            name=response.name,
212
            description=response.description,
213
            collection_id=LayerCollectionId(response.id.collection_id),
214
            provider_id=LayerProviderId(response.id.provider_id),
215
            items=items,
216
        )
217

218
    def reload(self) -> LayerCollection:
1 ✔
219
        """Reload the layer collection"""
220

221
        return layer_collection(self.collection_id, self.provider_id)
1 ✔
222

223
    def __repr__(self) -> str:
1 ✔
224
        """String representation of a `LayerCollection`"""
225

226
        buf = StringIO()
1 ✔
227

228
        buf.write(f"Layer Collection{os.linesep}")
1 ✔
229
        buf.write(f"name: {self.name}{os.linesep}")
1 ✔
230
        buf.write(f"description: {self.description}{os.linesep}")
1 ✔
231
        buf.write(f"id: {self.collection_id}{os.linesep}")
1 ✔
232
        buf.write(f"provider id: {self.provider_id}{os.linesep}")
1 ✔
233

234
        for i, item in enumerate(self.items):
1 ✔
235
            items_str = "items: "
1 ✔
236
            buf.write(items_str if i == 0 else " " * len(items_str))
1 ✔
237
            buf.write(f"{item}{os.linesep}")
1 ✔
238

239
        return buf.getvalue()
1 ✔
240

241
    def _repr_html_(self) -> str | None:
1 ✔
242
        """HTML representation for Jupyter notebooks"""
243

244
        buf = StringIO()
×
245

246
        buf.write("<table>")
×
247
        buf.write('<thead><tr><th colspan="2">Layer Collection</th></tr></thead>')
×
248
        buf.write("<tbody>")
×
249
        buf.write(f"<tr><th>name</th><td>{self.name}</td></tr>")
×
250
        buf.write(f"<tr><th>description</th><td>{self.description}</td></tr>")
×
251
        buf.write(f"<tr><th>id</th><td>{self.collection_id}</td></tr>")
×
252
        buf.write(f"<tr><th>provider id</th><td>{self.provider_id}</td></tr>")
×
253

254
        num_items = len(self.items)
×
255
        for i, item in enumerate(self.items):
×
256
            buf.write("<tr>")
×
257
            if i == 0:
×
258
                buf.write(f'<th rowspan="{num_items}">items</th>')
×
259
            buf.write(f"<td>{item.html_str()}</td>")
×
260
            buf.write("</tr>")
×
261

262
        buf.write("</tbody>")
×
263
        buf.write("</table>")
×
264

265
        return buf.getvalue()
×
266

267
    def remove(self, timeout: int = 60) -> None:
1 ✔
268
        """Remove the layer collection itself"""
269

270
        if self.provider_id != LAYER_DB_PROVIDER_ID:
1 ✔
271
            raise ModificationNotOnLayerDbException("Layer collection is not stored in the layer database")
×
272

273
        _delete_layer_collection(self.collection_id, timeout)
1 ✔
274

275
    def remove_item(self, index: int, timeout: int = 60):
1 ✔
276
        """Remove a layer or collection from this collection"""
277

278
        if index < 0 or index >= len(self.items):
1 ✔
279
            raise IndexError(f"index {index} out of range")
×
280

281
        item = self.items[index]
1 ✔
282

283
        if self.provider_id != LAYER_DB_PROVIDER_ID:
1 ✔
284
            raise ModificationNotOnLayerDbException("Layer collection is not stored in the layer database")
×
285

286
        # pylint: disable=protected-access
287
        item._remove(self.collection_id, self.provider_id, timeout)
1 ✔
288

289
        self.items.pop(index)
1 ✔
290

291
    @renamed_parameter("workflow", "processing_graph")
1 ✔
292
    def add_layer(
1 ✔
293
        self,
294
        name: str,
295
        description: str,
296
        # TODO: improve type
297
        processing_graph: dict[str, Any] | ProcessingGraphBuilderOperator,
298
        symbology: Symbology | None,
299
        replace_existing: bool = False,
300
        timeout: int = 60,
301
    ) -> LayerId:
302
        """Add a layer to this collection. Removes existing layers with the same name if forced."""
303
        # pylint: disable=too-many-arguments,too-many-positional-arguments
304

305
        if self.provider_id != LAYER_DB_PROVIDER_ID:
1 ✔
306
            raise ModificationNotOnLayerDbException("Layer collection is not stored in the layer database")
×
307

308
        if replace_existing:
1 ✔
309
            for item in self.get_items_by_name(name):
1 ✔
310
                if isinstance(item, LayerListing):
1 ✔
311
                    # pylint: disable=protected-access
312
                    item._remove(self.collection_id, self.provider_id, timeout)
1 ✔
313
            self.items = [item for item in self.items if not (isinstance(item, LayerListing) and item.name == name)]
1 ✔
314

315
        layer_id = _add_layer_to_collection(name, description, processing_graph, symbology, self.collection_id, timeout)
1 ✔
316

317
        self.items.append(
1 ✔
318
            LayerListing(
319
                listing_id=layer_id,
320
                provider_id=self.provider_id,
321
                name=name,
322
                description=description,
323
            )
324
        )
325

326
        return layer_id
1 ✔
327

328
    @renamed_parameter("workflow", "processing_graph")
1 ✔
329
    def add_layer_with_permissions(
1 ✔
330
        self,
331
        name: str,
332
        description: str,
333
        # TODO: improve type
334
        processing_graph: dict[str, Any] | ProcessingGraphBuilderOperator,
335
        symbology: Symbology | None,
336
        permission_tuples: list[tuple[RoleId, Permission]] | None = None,
337
        replace_existing: bool = False,
338
        timeout: int = 60,
339
    ) -> LayerId:
340
        """
341
        Add a layer to this collection and set permissions.
342
        Removes existing layers with the same name if forced.
343
        """
344

345
        layer_id = self.add_layer(
1 ✔
346
            name,
347
            description,
348
            processing_graph,
349
            symbology,
350
            replace_existing=replace_existing,
351
            timeout=timeout,
352
        )
353

354
        if permission_tuples is not None:
1 ✔
355
            res = Resource.from_layer_id(layer_id)
1 ✔
356
            for role, perm in permission_tuples:
1 ✔
357
                add_permission(role, res, perm)
1 ✔
358

359
        return layer_id
1 ✔
360

361
    def add_existing_layer(self, existing_layer: LayerListing | Layer | LayerId, timeout: int = 60):
1 ✔
362
        """Add an existing layer to this collection"""
363

364
        if self.provider_id != LAYER_DB_PROVIDER_ID:
1 ✔
365
            raise ModificationNotOnLayerDbException("Layer collection is not stored in the layer database")
×
366

367
        if isinstance(existing_layer, LayerListing):
1 ✔
368
            layer_id = existing_layer.listing_id
1 ✔
369
        elif isinstance(existing_layer, Layer):
×
370
            layer_id = existing_layer.layer_id
×
371
        elif isinstance(existing_layer, str):  # TODO: check for LayerId in Python 3.11+
×
372
            layer_id = existing_layer
×
373
        else:
374
            raise InputException("Invalid layer type")
×
375

376
        _add_existing_layer_to_collection(layer_id, self.collection_id, timeout)
1 ✔
377

378
        child_layer = layer(layer_id, self.provider_id)
1 ✔
379

380
        self.items.append(
1 ✔
381
            LayerListing(
382
                listing_id=layer_id,
383
                provider_id=self.provider_id,
384
                name=child_layer.name,
385
                description=child_layer.description,
386
            )
387
        )
388

389
        return layer_id
1 ✔
390

391
    def add_collection(self, name: str, description: str, timeout: int = 60) -> LayerCollectionId:
1 ✔
392
        """Add a collection to this collection"""
393

394
        if self.provider_id != LAYER_DB_PROVIDER_ID:
1 ✔
395
            raise ModificationNotOnLayerDbException("Layer collection is not stored in the layer database")
×
396

397
        collection_id = _add_layer_collection_to_collection(name, description, self.collection_id, timeout)
1 ✔
398

399
        self.items.append(
1 ✔
400
            LayerCollectionListing(
401
                listing_id=collection_id,
402
                provider_id=self.provider_id,
403
                name=name,
404
                description=description,
405
            )
406
        )
407

408
        return collection_id
1 ✔
409

410
    def add_existing_collection(
1 ✔
411
        self, existing_collection: LayerCollectionListing | LayerCollection | LayerCollectionId, timeout: int = 60
412
    ) -> LayerCollectionId:
413
        """Add an existing collection to this collection"""
414

415
        if self.provider_id != LAYER_DB_PROVIDER_ID:
1 ✔
416
            raise ModificationNotOnLayerDbException("Layer collection is not stored in the layer database")
×
417

418
        if isinstance(existing_collection, LayerCollectionListing):
1 ✔
419
            collection_id = existing_collection.listing_id
1 ✔
420
        elif isinstance(existing_collection, LayerCollection):
1 ✔
421
            collection_id = existing_collection.collection_id
×
422
        # TODO: check for LayerId in Python 3.11+
423
        elif isinstance(existing_collection, str):
1 ✔
424
            collection_id = existing_collection
1 ✔
425
        else:
426
            raise InputException("Invalid collection type")
×
427

428
        _add_existing_layer_collection_to_collection(
1 ✔
429
            collection_id=collection_id, parent_collection_id=self.collection_id, timeout=timeout
430
        )
431

432
        child_collection = layer_collection(collection_id, self.provider_id)
1 ✔
433

434
        self.items.append(
1 ✔
435
            LayerCollectionListing(
436
                listing_id=collection_id,
437
                provider_id=self.provider_id,
438
                name=child_collection.name,
439
                description=child_collection.description,
440
            )
441
        )
442

443
        return collection_id
1 ✔
444

445
    def get_items_by_name(self, name: str) -> list[Listing]:
1 ✔
446
        """Get all children with the given name"""
447

448
        return [item for item in self.items if item.name == name]
1 ✔
449

450
    def get_items_by_name_unique(self, name: str) -> Listing | None:
1 ✔
451
        """Get all children with the given name"""
452

453
        items = self.get_items_by_name(name=name)
1 ✔
454
        if len(items) == 0:
1 ✔
455
            return None
×
456
        if len(items) > 1:
1 ✔
457
            raise KeyError("{name} is not unique")
×
458
        return items[0]
1 ✔
459

460
    def search(
1 ✔
461
        self,
462
        search_string: str,
463
        *,
464
        search_type: Literal["fulltext", "prefix"] = "fulltext",
465
        offset: int = 0,
466
        limit: int = 20,
467
        timeout: int = 60,
468
    ) -> list[Listing]:
469
        """Search for a string in the layer collection"""
470

471
        session = get_session()
1 ✔
472

473
        if search_type not in [
1 ✔
474
            geoengine_api_client.SearchType.FULLTEXT,
475
            geoengine_api_client.SearchType.PREFIX,
476
        ]:
477
            raise ValueError(f"Invalid search type {search_type}")
×
478

479
        with geoengine_api_client.ApiClient(session.configuration) as api_client:
1 ✔
480
            layers_api = geoengine_api_client.LayersApi(api_client)
1 ✔
481
            layer_collection_response = layers_api.search_handler(
1 ✔
482
                provider=self.provider_id,
483
                collection=str(self.collection_id),
484
                search_string=search_string,
485
                search_type=geoengine_api_client.SearchType(search_type),
486
                offset=offset,
487
                limit=limit,
488
                _request_timeout=timeout,
489
            )
490

491
        listings: list[Listing] = []
1 ✔
492
        for item in layer_collection_response.items:
1 ✔
493
            inner = item.actual_instance
1 ✔
494
            if inner is None:
1 ✔
495
                continue
×
496
            if isinstance(inner, geoengine_api_client.LayerCollectionListing):
1 ✔
497
                listings.append(
×
498
                    LayerCollectionListing(
499
                        listing_id=LayerCollectionId(inner.id.collection_id),
500
                        provider_id=LayerProviderId(inner.id.provider_id),
501
                        name=inner.name,
502
                        description=inner.description,
503
                    )
504
                )
505
            elif isinstance(inner, geoengine_api_client.LayerListing):
1 ✔
506
                listings.append(
1 ✔
507
                    LayerListing(
508
                        listing_id=LayerId(inner.id.layer_id),
509
                        provider_id=LayerProviderId(inner.id.provider_id),
510
                        name=inner.name,
511
                        description=inner.description,
512
                    )
513
                )
514
            else:
515
                pass  # ignore, should not happen
×
516

517
        return listings
1 ✔
518

519
    def get_or_create_unique_collection(
1 ✔
520
        self,
521
        collection_name: str,
522
        create_collection_description: str | None = None,
523
        delete_existing_with_same_name: bool = False,
524
        create_permissions_tuples: list[tuple[RoleId, Permission]] | None = None,
525
    ) -> LayerCollection:
526
        """
527
        Get a unique child by name OR if it does not exist create it.
528
        Removes existing collections with same name if forced!
529
        Sets permissions if the collection is created from a list of tuples
530
        """
531
        parent_collection = self.reload()  # reload just to be safe since self's state change on the server
1 ✔
532
        existing_collections = parent_collection.get_items_by_name(collection_name)
1 ✔
533

534
        if delete_existing_with_same_name and len(existing_collections) > 0:
1 ✔
535
            for c in existing_collections:
1 ✔
536
                actual = c.load()
1 ✔
537
                if isinstance(actual, LayerCollection):
1 ✔
538
                    actual.remove()
1 ✔
539
            parent_collection = parent_collection.reload()
1 ✔
540
            existing_collections = parent_collection.get_items_by_name(collection_name)
1 ✔
541

542
        if len(existing_collections) == 0:
1 ✔
543
            new_desc = create_collection_description if create_collection_description is not None else collection_name
1 ✔
544
            new_collection = parent_collection.add_collection(collection_name, new_desc)
1 ✔
545
            new_ressource = Resource.from_layer_collection_id(new_collection)
1 ✔
546

547
            if create_permissions_tuples is not None:
1 ✔
548
                for role, perm in create_permissions_tuples:
1 ✔
549
                    add_permission(role, new_ressource, perm)
1 ✔
550
            parent_collection = parent_collection.reload()
1 ✔
551
            existing_collections = parent_collection.get_items_by_name(collection_name)
1 ✔
552

553
        if len(existing_collections) == 0:
1 ✔
554
            raise KeyError(
×
555
                f"No collection with name {collection_name} exists in {parent_collection.name} and none was created!"
556
            )
557

558
        if len(existing_collections) > 1:
1 ✔
559
            raise KeyError(f"Multiple collections with name {collection_name} exist in {parent_collection.name}")
×
560

561
        res = existing_collections[0].load()
1 ✔
562
        if isinstance(res, Layer):
1 ✔
563
            raise TypeError(f"Found a Layer not a Layer collection for {collection_name}")
×
564

565
        # we know that it is a collection since check that
566
        return cast(LayerCollection, existing_collections[0].load())
1 ✔
567

568
    def __eq__(self, other):
1 ✔
569
        """Tests if two layer listings are identical"""
570
        if not isinstance(other, self.__class__):
1 ✔
571
            return False
×
572

573
        return (
1 ✔
574
            self.name == other.name
575
            and self.description == other.description
576
            and self.provider_id == other.provider_id
577
            and self.collection_id == other.collection_id
578
            and self.items == other.items
579
        )
580

581

582
@dataclass(repr=False)
1 ✔
583
class Layer:
1 ✔
584
    """A layer"""
585

586
    # pylint: disable=too-many-instance-attributes
587

588
    name: str
1 ✔
589
    description: str
1 ✔
590
    layer_id: LayerId
1 ✔
591
    provider_id: LayerProviderId
1 ✔
592
    processing_graph: dict[str, Any]  # TODO: specify in more detail
1 ✔
593
    symbology: Symbology | None
1 ✔
594
    properties: list[Any]  # TODO: specify in more detail
1 ✔
595
    metadata: dict[str, Any]  # TODO: specify in more detail
1 ✔
596

597
    @renamed_parameter("workflow", "processing_graph")
1 ✔
598
    def __init__(
1 ✔
599
        self,
600
        name: str,
601
        description: str,
602
        layer_id: LayerId,
603
        provider_id: LayerProviderId,
604
        processing_graph: dict[str, Any],
605
        symbology: Symbology | None,
606
        properties: list[Any],
607
        metadata: dict[Any, Any],
608
    ) -> None:
609
        """Create a new `Layer`"""
610
        # pylint: disable=too-many-arguments,too-many-positional-arguments
611

612
        self.name = name
1 ✔
613
        self.description = description
1 ✔
614
        self.layer_id = layer_id
1 ✔
615
        self.provider_id = provider_id
1 ✔
616
        self.processing_graph = processing_graph
1 ✔
617
        self.symbology = symbology
1 ✔
618
        self.properties = properties
1 ✔
619
        self.metadata = metadata
1 ✔
620

621
    @property
1 ✔
622
    @deprecated("Use `Layer.processing_graph` instead.", category=DeprecationWarning)
1 ✔
623
    def workflow(self) -> dict[str, Any]:
1 ✔
624
        """Deprecated: use `Layer.processing_graph` instead."""
625
        return self.processing_graph
1 ✔
626

627
    @classmethod
1 ✔
628
    def from_response(cls, response: geoengine_api_client.Layer) -> Layer:
1 ✔
629
        """Parse an HTTP JSON response to an `Layer`"""
630
        symbology = None
1 ✔
631
        if response.symbology is not None:
1 ✔
632
            symbology = Symbology.from_response(response.symbology)
1 ✔
633

634
        processing_graph_dict = cast(dict[str, Any], response.processing_graph.to_dict())  # silence mypy here
1 ✔
635

636
        return Layer(
1 ✔
637
            name=response.name,
638
            description=response.description,
639
            layer_id=LayerId(response.id.layer_id),
640
            provider_id=LayerProviderId(response.id.provider_id),
641
            processing_graph=processing_graph_dict,
642
            symbology=symbology,
643
            properties=cast(list[Any], response.properties),
644
            metadata=cast(dict[Any, Any], response.metadata),
645
        )
646

647
    def __repr__(self) -> str:
1 ✔
648
        """String representation of a `Layer`"""
649

650
        buf = StringIO()
×
651

652
        buf.write(f"Layer{os.linesep}")
×
653
        buf.write(f"name: {self.name}{os.linesep}")
×
654
        buf.write(f"description: {self.description}{os.linesep}")
×
655
        buf.write(f"id: {self.layer_id}{os.linesep}")
×
656
        buf.write(f"provider id: {self.provider_id}{os.linesep}")
×
657
        # TODO: better representation of processing graph, symbology, properties, metadata
658
        buf.write(f"processing graph: {self.processing_graph}{os.linesep}")
×
659
        buf.write(f"symbology: {self.symbology}{os.linesep}")
×
660
        buf.write(f"properties: {self.properties}{os.linesep}")
×
661
        buf.write(f"metadata: {self.metadata}{os.linesep}")
×
662

663
        return buf.getvalue()
×
664

665
    def _repr_html_(self) -> str | None:
1 ✔
666
        """HTML representation for Jupyter notebooks"""
667

668
        buf = StringIO()
1 ✔
669

670
        buf.write("<table>")
1 ✔
671
        buf.write('<thead><tr><th colspan="2">Layer</th></tr></thead>')
1 ✔
672
        buf.write("<tbody>")
1 ✔
673
        buf.write(f"<tr><th>name</th><td>{self.name}</td></tr>")
1 ✔
674
        buf.write(f"<tr><th>description</th><td>{self.description}</td></tr>")
1 ✔
675
        buf.write(f"<tr><th>id</th><td>{self.layer_id}</td></tr>")
1 ✔
676
        buf.write(f"<tr><th>provider id</th><td>{self.provider_id}</td></tr>")
1 ✔
677

678
        # TODO: better representation of processing graph, symbology, properties, metadata
679
        buf.write('<tr><th>processing graph</th><td align="left">')
1 ✔
680
        buf.write(f"<pre>{json.dumps(self.processing_graph, indent=4)}{os.linesep}</pre></td></tr>")
1 ✔
681
        buf.write("<tr><th>symbology</th>")
1 ✔
682
        if self.symbology is None:
1 ✔
683
            buf.write('<td align="left">None</td></tr>')
×
684
        else:
685
            buf.write(f'<td align="left"><pre>{self.symbology.to_api_dict().to_json()}{os.linesep}</pre></td></tr>')
1 ✔
686
        buf.write(f"<tr><th>properties</th><td>{self.properties}{os.linesep}</td></tr>")
1 ✔
687
        buf.write(f"<tr><th>metadata</th><td>{self.metadata}{os.linesep}</td></tr>")
1 ✔
688

689
        buf.write("</tbody>")
1 ✔
690
        buf.write("</table>")
1 ✔
691

692
        return buf.getvalue()
1 ✔
693

694
    def save_as_dataset(self, timeout: int = 60) -> Task:
1 ✔
695
        """
696
        Save a layer as a new dataset.
697
        """
698
        session = get_session()
1 ✔
699

700
        with geoengine_api_client.ApiClient(session.configuration) as api_client:
1 ✔
701
            layers_api = geoengine_api_client.LayersApi(api_client)
1 ✔
702
            response = layers_api.layer_to_dataset(self.provider_id, str(self.layer_id), _request_timeout=timeout)
1 ✔
703

704
        return Task(TaskId.from_response(response))
1 ✔
705

706
    def to_api_dict(self) -> geoengine_api_client.Layer:
1 ✔
707
        """Convert to a dictionary that can be serialized to JSON"""
708
        return geoengine_api_client.Layer(
×
709
            name=self.name,
710
            description=self.description,
711
            id=geoengine_api_client.ProviderLayerId(
712
                layer_id=str(self.layer_id),
713
                provider_id=str(self.provider_id),
714
            ),
715
            processing_graph=geoengine_api_client.ProcessingGraph.from_dict(self.processing_graph),
716
            symbology=self.symbology.to_api_dict() if self.symbology is not None else None,
717
            properties=self.properties,
718
            metadata=self.metadata,
719
        )
720

721
    def as_processing_graph_id(self, timeout: int = 60) -> ProcessingGraphId:
1 ✔
722
        """
723
        Register a layer as a processing graph and returns its `ProcessingGraphId`
724
        """
725
        session = get_session()
1 ✔
726

727
        with geoengine_api_client.ApiClient(session.configuration) as api_client:
1 ✔
728
            layers_api = geoengine_api_client.LayersApi(api_client)
1 ✔
729
            response = layers_api.layer_to_processing_graph_id_handler(
1 ✔
730
                self.provider_id, self.layer_id, _request_timeout=timeout
731
            )
732

733
        return ProcessingGraphId.from_response(response)
1 ✔
734

735
    def as_processing_graph(self, timeout: int = 60) -> ProcessingGraph:
1 ✔
736
        """
737
        Register a layer as a processing graph and returns the processing graph
738
        """
739
        processing_graph_id = self.as_processing_graph_id(timeout=timeout)
1 ✔
740

741
        return ProcessingGraph(processing_graph_id)
1 ✔
742

743
    @deprecated("Use `Layer.as_processing_graph_id` instead.", category=DeprecationWarning)
1 ✔
744
    def as_workflow_id(self, timeout: int = 60) -> ProcessingGraphId:
1 ✔
745
        """Deprecated: use `Layer.as_processing_graph_id` instead."""
746
        return self.as_processing_graph_id(timeout=timeout)
×
747

748
    @deprecated("Use `Layer.as_processing_graph` instead.", category=DeprecationWarning)
1 ✔
749
    def as_workflow(self, timeout: int = 60) -> ProcessingGraph:
1 ✔
750
        """Deprecated: use `Layer.as_processing_graph` instead."""
751
        return self.as_processing_graph(timeout=timeout)
×
752

753

754
def layer_collection(
1 ✔
755
    layer_collection_id: LayerCollectionId | None = None,
756
    layer_provider_id: LayerProviderId = LAYER_DB_PROVIDER_ID,
757
    timeout: int = 60,
758
) -> LayerCollection:
759
    """
760
    Retrieve a layer collection that contains layers and layer collections.
761
    """
762

763
    session = get_session()
1 ✔
764

765
    page_limit = 20
1 ✔
766
    pages: list[geoengine_api_client.LayerCollection] = []
1 ✔
767

768
    offset = 0
1 ✔
769
    while True:
1 ✔
770
        with geoengine_api_client.ApiClient(session.configuration) as api_client:
1 ✔
771
            layers_api = geoengine_api_client.LayersApi(api_client)
1 ✔
772

773
            if layer_collection_id is None:
1 ✔
774
                page = layers_api.list_root_collections_handler(offset, page_limit, _request_timeout=timeout)
1 ✔
775
            else:
776
                page = layers_api.list_collection_handler(
1 ✔
777
                    layer_provider_id, layer_collection_id, offset, page_limit, _request_timeout=timeout
778
                )
779

780
        if len(page.items) < page_limit:
1 ✔
781
            # we need at least one page before breaking
782
            if len(pages) == 0 or len(page.items) > 0:
1 ✔
783
                pages.append(page)
1 ✔
784
            break
1 ✔
785

786
        pages.append(page)
×
787
        offset += page_limit
×
788

789
    return LayerCollection.from_response(pages)
1 ✔
790

791

792
def layer(layer_id: LayerId, layer_provider_id: LayerProviderId = LAYER_DB_PROVIDER_ID, timeout: int = 60) -> Layer:
1 ✔
793
    """
794
    Retrieve a layer from the server.
795
    """
796

797
    session = get_session()
1 ✔
798

799
    with geoengine_api_client.ApiClient(session.configuration) as api_client:
1 ✔
800
        layers_api = geoengine_api_client.LayersApi(api_client)
1 ✔
801
        response = layers_api.layer_handler(layer_provider_id, str(layer_id), _request_timeout=timeout)
1 ✔
802

803
    return Layer.from_response(response)
1 ✔
804

805

806
def _delete_layer_from_collection(collection_id: LayerCollectionId, layer_id: LayerId, timeout: int = 60) -> None:
1 ✔
807
    """Delete a layer from a collection"""
808

809
    session = get_session()
1 ✔
810

811
    with geoengine_api_client.ApiClient(session.configuration) as api_client:
1 ✔
812
        layers_api = geoengine_api_client.LayersApi(api_client)
1 ✔
813
        layers_api.remove_layer_from_collection(collection_id, layer_id, _request_timeout=timeout)
1 ✔
814

815

816
def _delete_layer_collection_from_collection(
1 ✔
817
    parent_id: LayerCollectionId, collection_id: LayerCollectionId, timeout: int = 60
818
) -> None:
819
    """Delete a layer collection from a collection"""
820

821
    session = get_session()
1 ✔
822

823
    with geoengine_api_client.ApiClient(session.configuration) as api_client:
1 ✔
824
        layers_api = geoengine_api_client.LayersApi(api_client)
1 ✔
825
        layers_api.remove_collection_from_collection(parent_id, collection_id, _request_timeout=timeout)
1 ✔
826

827

828
def _delete_layer_collection(collection_id: LayerCollectionId, timeout: int = 60) -> None:
1 ✔
829
    """Delete a layer collection"""
830

831
    session = get_session()
1 ✔
832

833
    with geoengine_api_client.ApiClient(session.configuration) as api_client:
1 ✔
834
        layers_api = geoengine_api_client.LayersApi(api_client)
1 ✔
835
        layers_api.remove_collection(collection_id, _request_timeout=timeout)
1 ✔
836

837

838
def _add_layer_collection_to_collection(
1 ✔
839
    name: str, description: str, parent_collection_id: LayerCollectionId, timeout: int = 60
840
) -> LayerCollectionId:
841
    """Add a new layer collection"""
842

843
    session = get_session()
1 ✔
844

845
    with geoengine_api_client.ApiClient(session.configuration) as api_client:
1 ✔
846
        layers_api = geoengine_api_client.LayersApi(api_client)
1 ✔
847
        response = layers_api.add_collection(
1 ✔
848
            parent_collection_id,
849
            geoengine_api_client.AddLayerCollection(
850
                name=name,
851
                description=description,
852
            ),
853
            _request_timeout=timeout,
854
        )
855

856
    return LayerCollectionId(str(response.id))
1 ✔
857

858

859
def _add_existing_layer_collection_to_collection(
1 ✔
860
    collection_id: LayerCollectionId, parent_collection_id: LayerCollectionId, timeout: int = 60
861
) -> None:
862
    """Add an existing layer collection to a collection"""
863

864
    session = get_session()
1 ✔
865

866
    with geoengine_api_client.ApiClient(session.configuration) as api_client:
1 ✔
867
        layers_api = geoengine_api_client.LayersApi(api_client)
1 ✔
868
        layers_api.add_existing_collection_to_collection(parent_collection_id, collection_id, _request_timeout=timeout)
1 ✔
869

870

871
def _add_layer_to_collection(
1 ✔
872
    name: str,
873
    description: str,
874
    processing_graph: dict[str, Any] | ProcessingGraphBuilderOperator,  # TODO: improve type
875
    symbology: Symbology | None,
876
    collection_id: LayerCollectionId,
877
    timeout: int = 60,
878
) -> LayerId:
879
    """Add a new layer"""
880
    # pylint: disable=too-many-arguments,too-many-positional-arguments
881

882
    # convert processing graph to dict if necessary
883
    if isinstance(processing_graph, ProcessingGraphBuilderOperator):
1 ✔
884
        processing_graph = processing_graph.to_processing_graph_dict()
1 ✔
885

886
    symbology_dict = symbology.to_api_dict() if symbology is not None and isinstance(symbology, Symbology) else None
1 ✔
887

888
    session = get_session()
1 ✔
889

890
    with geoengine_api_client.ApiClient(session.configuration) as api_client:
1 ✔
891
        layers_api = geoengine_api_client.LayersApi(api_client)
1 ✔
892
        response = layers_api.add_layer(
1 ✔
893
            collection_id,
894
            geoengine_api_client.AddLayer(
895
                name=name,
896
                description=description,
897
                processing_graph=geoengine_api_client.ProcessingGraph.from_dict(processing_graph),
898
                symbology=symbology_dict,
899
            ),
900
            _request_timeout=timeout,
901
        )
902

903
    return LayerId(str(response.id))
1 ✔
904

905

906
def _add_existing_layer_to_collection(layer_id: LayerId, collection_id: LayerCollectionId, timeout: int = 60) -> None:
1 ✔
907
    """Add an existing layer to a collection"""
908

909
    session = get_session()
1 ✔
910

911
    with geoengine_api_client.ApiClient(session.configuration) as api_client:
1 ✔
912
        layers_api = geoengine_api_client.LayersApi(api_client)
1 ✔
913
        layers_api.add_existing_layer_to_collection(collection_id, layer_id, _request_timeout=timeout)
1 ✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc