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

geo-engine / geoengine-python / 19573839061

21 Nov 2025 02:37PM UTC coverage: 78.668% (-1.1%) from 79.74%
19573839061

Pull #221

github

web-flow
Merge 4012cbccd into e06d48b64
Pull Request #221: Pixel_based_queries_rewrite

3083 of 3919 relevant lines covered (78.67%)

0.79 hits per line

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

80.56
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_openapi_client
1✔
15
from strenum import LowercaseStrEnum
1✔
16

17
from geoengine.auth import get_session
1✔
18
from geoengine.error import InputException, ModificationNotOnLayerDbException
1✔
19
from geoengine.permissions import Permission, RoleId, add_permission
1✔
20
from geoengine.resource_identifier import LAYER_DB_PROVIDER_ID, LayerCollectionId, LayerId, LayerProviderId, Resource
1✔
21
from geoengine.tasks import Task, TaskId
1✔
22
from geoengine.types import Symbology
1✔
23
from geoengine.workflow import Workflow, WorkflowId
1✔
24
from geoengine.workflow_builder.operators import Operator as WorkflowBuilderOperator
1✔
25

26

27
class LayerCollectionListingType(LowercaseStrEnum):
1✔
28
    LAYER = auto()
1✔
29
    COLLECTION = auto()
1✔
30

31

32
LISTINGID = TypeVar("LISTINGID")
1✔
33

34

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

39
    listing_id: LISTINGID
1✔
40
    provider_id: LayerProviderId
1✔
41
    name: str
1✔
42
    description: str
1✔
43

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

47
        buf = StringIO()
1✔
48

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

55
        return buf.getvalue()
1✔
56

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

60
        buf = StringIO()
×
61

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

72
        return buf.getvalue()
×
73

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

77
        return self.html_str()
×
78

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

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

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

91

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

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

99
        return "Layer"
1✔
100

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

104
        return layer(self.listing_id, self.provider_id, timeout)
1✔
105

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

110
        _delete_layer_from_collection(
1✔
111
            collection_id,
112
            self.listing_id,
113
            timeout,
114
        )
115

116

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

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

124
        return "Layer Collection"
1✔
125

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

129
        return layer_collection(self.listing_id, self.provider_id, timeout)
1✔
130

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

135
        _delete_layer_collection_from_collection(
1✔
136
            collection_id,
137
            self.listing_id,
138
            timeout,
139
        )
140

141

142
class LayerCollection:
1✔
143
    """A layer collection"""
144

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

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

162
        self.name = name
1✔
163
        self.description = description
1✔
164
        self.collection_id = collection_id
1✔
165
        self.provider_id = provider_id
1✔
166
        self.items = items
1✔
167

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

172
        assert len(response_pages) > 0, "No response pages"
1✔
173

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

179
            item_type = LayerCollectionListingType(inner.type)
1✔
180

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

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

199
            raise AssertionError("Invalid listing type")
×
200

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

206
        response = response_pages[0]
1✔
207

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

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

219
        return layer_collection(self.collection_id, self.provider_id)
1✔
220

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

224
        buf = StringIO()
1✔
225

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

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

237
        return buf.getvalue()
1✔
238

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

242
        buf = StringIO()
×
243

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

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

260
        buf.write("</tbody>")
×
261
        buf.write("</table>")
×
262

263
        return buf.getvalue()
×
264

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

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

271
        _delete_layer_collection(self.collection_id, timeout)
1✔
272

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

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

279
        item = self.items[index]
1✔
280

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

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

287
        self.items.pop(index)
1✔
288

289
    def add_layer(
1✔
290
        self,
291
        name: str,
292
        description: str,
293
        # TODO: improve type
294
        workflow: dict[str, Any] | WorkflowBuilderOperator,
295
        symbology: Symbology | None,
296
        timeout: int = 60,
297
    ) -> LayerId:
298
        """Add a layer to this collection"""
299
        # pylint: disable=too-many-arguments,too-many-positional-arguments
300

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

304
        layer_id = _add_layer_to_collection(name, description, workflow, symbology, self.collection_id, timeout)
1✔
305

306
        self.items.append(
1✔
307
            LayerListing(
308
                listing_id=layer_id,
309
                provider_id=self.provider_id,
310
                name=name,
311
                description=description,
312
            )
313
        )
314

315
        return layer_id
1✔
316

317
    def add_layer_with_permissions(
1✔
318
        self,
319
        name: str,
320
        description: str,
321
        # TODO: improve type
322
        workflow: dict[str, Any] | WorkflowBuilderOperator,
323
        symbology: Symbology | None,
324
        permission_tuples: list[tuple[RoleId, Permission]] | None = None,
325
        timeout: int = 60,
326
    ) -> LayerId:
327
        """
328
        Add a layer to this collection and set permissions.
329
        """
330

331
        layer_id = self.add_layer(name, description, workflow, symbology, timeout)
1✔
332

333
        if permission_tuples is not None:
1✔
334
            res = Resource.from_layer_id(layer_id)
1✔
335
            for role, perm in permission_tuples:
1✔
336
                add_permission(role, res, perm)
1✔
337

338
        return layer_id
1✔
339

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

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

346
        if isinstance(existing_layer, LayerListing):
1✔
347
            layer_id = existing_layer.listing_id
1✔
348
        elif isinstance(existing_layer, Layer):
×
349
            layer_id = existing_layer.layer_id
×
350
        elif isinstance(existing_layer, str):  # TODO: check for LayerId in Python 3.11+
×
351
            layer_id = existing_layer
×
352
        else:
353
            raise InputException("Invalid layer type")
×
354

355
        _add_existing_layer_to_collection(layer_id, self.collection_id, timeout)
1✔
356

357
        child_layer = layer(layer_id, self.provider_id)
1✔
358

359
        self.items.append(
1✔
360
            LayerListing(
361
                listing_id=layer_id,
362
                provider_id=self.provider_id,
363
                name=child_layer.name,
364
                description=child_layer.description,
365
            )
366
        )
367

368
        return layer_id
1✔
369

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

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

376
        collection_id = _add_layer_collection_to_collection(name, description, self.collection_id, timeout)
1✔
377

378
        self.items.append(
1✔
379
            LayerCollectionListing(
380
                listing_id=collection_id,
381
                provider_id=self.provider_id,
382
                name=name,
383
                description=description,
384
            )
385
        )
386

387
        return collection_id
1✔
388

389
    def add_existing_collection(
1✔
390
        self, existing_collection: LayerCollectionListing | LayerCollection | LayerCollectionId, timeout: int = 60
391
    ) -> LayerCollectionId:
392
        """Add an existing 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
        if isinstance(existing_collection, LayerCollectionListing):
1✔
398
            collection_id = existing_collection.listing_id
1✔
399
        elif isinstance(existing_collection, LayerCollection):
1✔
400
            collection_id = existing_collection.collection_id
×
401
        # TODO: check for LayerId in Python 3.11+
402
        elif isinstance(existing_collection, str):
1✔
403
            collection_id = existing_collection
1✔
404
        else:
405
            raise InputException("Invalid collection type")
×
406

407
        _add_existing_layer_collection_to_collection(
1✔
408
            collection_id=collection_id, parent_collection_id=self.collection_id, timeout=timeout
409
        )
410

411
        child_collection = layer_collection(collection_id, self.provider_id)
1✔
412

413
        self.items.append(
1✔
414
            LayerCollectionListing(
415
                listing_id=collection_id,
416
                provider_id=self.provider_id,
417
                name=child_collection.name,
418
                description=child_collection.description,
419
            )
420
        )
421

422
        return collection_id
1✔
423

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

427
        return [item for item in self.items if item.name == name]
1✔
428

429
    def get_items_by_name_unique(self, name: str) -> Listing | None:
1✔
430
        """Get all children with the given name"""
431

432
        items = self.get_items_by_name(name=name)
1✔
433
        if len(items) == 0:
1✔
434
            return None
×
435
        if len(items) > 1:
1✔
436
            raise KeyError("{name} is not unique")
×
437
        return items[0]
1✔
438

439
    def search(
1✔
440
        self,
441
        search_string: str,
442
        *,
443
        search_type: Literal["fulltext", "prefix"] = "fulltext",
444
        offset: int = 0,
445
        limit: int = 20,
446
        timeout: int = 60,
447
    ) -> list[Listing]:
448
        """Search for a string in the layer collection"""
449

450
        session = get_session()
1✔
451

452
        if search_type not in [
1✔
453
            geoengine_openapi_client.SearchType.FULLTEXT,
454
            geoengine_openapi_client.SearchType.PREFIX,
455
        ]:
456
            raise ValueError(f"Invalid search type {search_type}")
×
457

458
        with geoengine_openapi_client.ApiClient(session.configuration) as api_client:
1✔
459
            layers_api = geoengine_openapi_client.LayersApi(api_client)
1✔
460
            layer_collection_response = layers_api.search_handler(
1✔
461
                provider=self.provider_id,
462
                collection=str(self.collection_id),
463
                search_string=search_string,
464
                search_type=geoengine_openapi_client.SearchType(search_type),
465
                offset=offset,
466
                limit=limit,
467
                _request_timeout=timeout,
468
            )
469

470
        listings: list[Listing] = []
1✔
471
        for item in layer_collection_response.items:
1✔
472
            inner = item.actual_instance
1✔
473
            if inner is None:
1✔
474
                continue
×
475
            if isinstance(inner, geoengine_openapi_client.LayerCollectionListing):
1✔
476
                listings.append(
×
477
                    LayerCollectionListing(
478
                        listing_id=LayerCollectionId(inner.id.collection_id),
479
                        provider_id=LayerProviderId(inner.id.provider_id),
480
                        name=inner.name,
481
                        description=inner.description,
482
                    )
483
                )
484
            elif isinstance(inner, geoengine_openapi_client.LayerListing):
1✔
485
                listings.append(
1✔
486
                    LayerListing(
487
                        listing_id=LayerId(inner.id.layer_id),
488
                        provider_id=LayerProviderId(inner.id.provider_id),
489
                        name=inner.name,
490
                        description=inner.description,
491
                    )
492
                )
493
            else:
494
                pass  # ignore, should not happen
×
495

496
        return listings
1✔
497

498
    def get_or_create_unique_collection(
1✔
499
        self,
500
        collection_name: str,
501
        create_collection_description: str | None = None,
502
        delete_existing_with_same_name: bool = False,
503
        create_permissions_tuples: list[tuple[RoleId, Permission]] | None = None,
504
    ) -> LayerCollection:
505
        """
506
        Get a unique child by name OR if it does not exist create it.
507
        Removes existing collections with same name if forced!
508
        Sets permissions if the collection is created from a list of tuples
509
        """
510
        parent_collection = self.reload()  # reload just to be safe since self's state change on the server
1✔
511
        existing_collections = parent_collection.get_items_by_name(collection_name)
1✔
512

513
        if delete_existing_with_same_name and len(existing_collections) > 0:
1✔
514
            for c in existing_collections:
1✔
515
                actual = c.load()
1✔
516
                if isinstance(actual, LayerCollection):
1✔
517
                    actual.remove()
1✔
518
            parent_collection = parent_collection.reload()
1✔
519
            existing_collections = parent_collection.get_items_by_name(collection_name)
1✔
520

521
        if len(existing_collections) == 0:
1✔
522
            new_desc = create_collection_description if create_collection_description is not None else collection_name
1✔
523
            new_collection = parent_collection.add_collection(collection_name, new_desc)
1✔
524
            new_ressource = Resource.from_layer_collection_id(new_collection)
1✔
525

526
            if create_permissions_tuples is not None:
1✔
527
                for role, perm in create_permissions_tuples:
1✔
528
                    add_permission(role, new_ressource, perm)
1✔
529
            parent_collection = parent_collection.reload()
1✔
530
            existing_collections = parent_collection.get_items_by_name(collection_name)
1✔
531

532
        if len(existing_collections) == 0:
1✔
533
            raise KeyError(
×
534
                f"No collection with name {collection_name} exists in {parent_collection.name} and none was created!"
535
            )
536

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

540
        res = existing_collections[0].load()
1✔
541
        if isinstance(res, Layer):
1✔
542
            raise TypeError(f"Found a Layer not a Layer collection for {collection_name}")
×
543

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

547
    def __eq__(self, other):
1✔
548
        """Tests if two layer listings are identical"""
549
        if not isinstance(other, self.__class__):
1✔
550
            return False
×
551

552
        return (
1✔
553
            self.name == other.name
554
            and self.description == other.description
555
            and self.provider_id == other.provider_id
556
            and self.collection_id == other.collection_id
557
            and self.items == other.items
558
        )
559

560

561
@dataclass(repr=False)
1✔
562
class Layer:
1✔
563
    """A layer"""
564

565
    # pylint: disable=too-many-instance-attributes
566

567
    name: str
1✔
568
    description: str
1✔
569
    layer_id: LayerId
1✔
570
    provider_id: LayerProviderId
1✔
571
    workflow: dict[str, Any]  # TODO: specify in more detail
1✔
572
    symbology: Symbology | None
1✔
573
    properties: list[Any]  # TODO: specify in more detail
1✔
574
    metadata: dict[str, Any]  # TODO: specify in more detail
1✔
575

576
    def __init__(
1✔
577
        self,
578
        name: str,
579
        description: str,
580
        layer_id: LayerId,
581
        provider_id: LayerProviderId,
582
        workflow: dict[str, Any],
583
        symbology: Symbology | None,
584
        properties: list[Any],
585
        metadata: dict[Any, Any],
586
    ) -> None:
587
        """Create a new `Layer`"""
588
        # pylint: disable=too-many-arguments,too-many-positional-arguments
589

590
        self.name = name
1✔
591
        self.description = description
1✔
592
        self.layer_id = layer_id
1✔
593
        self.provider_id = provider_id
1✔
594
        self.workflow = workflow
1✔
595
        self.symbology = symbology
1✔
596
        self.properties = properties
1✔
597
        self.metadata = metadata
1✔
598

599
    @classmethod
1✔
600
    def from_response(cls, response: geoengine_openapi_client.Layer) -> Layer:
1✔
601
        """Parse an HTTP JSON response to an `Layer`"""
602
        symbology = None
1✔
603
        if response.symbology is not None:
1✔
604
            symbology = Symbology.from_response(response.symbology)
1✔
605

606
        return Layer(
1✔
607
            name=response.name,
608
            description=response.description,
609
            layer_id=LayerId(response.id.layer_id),
610
            provider_id=LayerProviderId(response.id.provider_id),
611
            workflow=response.workflow.to_dict(),
612
            symbology=symbology,
613
            properties=cast(list[Any], response.properties),
614
            metadata=cast(dict[Any, Any], response.metadata),
615
        )
616

617
    def __repr__(self) -> str:
1✔
618
        """String representation of a `Layer`"""
619

620
        buf = StringIO()
×
621

622
        buf.write(f"Layer{os.linesep}")
×
623
        buf.write(f"name: {self.name}{os.linesep}")
×
624
        buf.write(f"description: {self.description}{os.linesep}")
×
625
        buf.write(f"id: {self.layer_id}{os.linesep}")
×
626
        buf.write(f"provider id: {self.provider_id}{os.linesep}")
×
627
        # TODO: better representation of workflow, symbology, properties, metadata
628
        buf.write(f"workflow: {self.workflow}{os.linesep}")
×
629
        buf.write(f"symbology: {self.symbology}{os.linesep}")
×
630
        buf.write(f"properties: {self.properties}{os.linesep}")
×
631
        buf.write(f"metadata: {self.metadata}{os.linesep}")
×
632

633
        return buf.getvalue()
×
634

635
    def _repr_html_(self) -> str | None:
1✔
636
        """HTML representation for Jupyter notebooks"""
637

638
        buf = StringIO()
1✔
639

640
        buf.write("<table>")
1✔
641
        buf.write('<thead><tr><th colspan="2">Layer</th></tr></thead>')
1✔
642
        buf.write("<tbody>")
1✔
643
        buf.write(f"<tr><th>name</th><td>{self.name}</td></tr>")
1✔
644
        buf.write(f"<tr><th>description</th><td>{self.description}</td></tr>")
1✔
645
        buf.write(f"<tr><th>id</th><td>{self.layer_id}</td></tr>")
1✔
646
        buf.write(f"<tr><th>provider id</th><td>{self.provider_id}</td></tr>")
1✔
647

648
        # TODO: better representation of workflow, symbology, properties, metadata
649
        buf.write('<tr><th>workflow</th><td align="left">')
1✔
650
        buf.write(f"<pre>{json.dumps(self.workflow, indent=4)}{os.linesep}</pre></td></tr>")
1✔
651
        buf.write("<tr><th>symbology</th>")
1✔
652
        if self.symbology is None:
1✔
653
            buf.write('<td align="left">None</td></tr>')
×
654
        else:
655
            buf.write(f'<td align="left"><pre>{self.symbology.to_api_dict().to_json()}{os.linesep}</pre></td></tr>')
1✔
656
        buf.write(f"<tr><th>properties</th><td>{self.properties}{os.linesep}</td></tr>")
1✔
657
        buf.write(f"<tr><th>metadata</th><td>{self.metadata}{os.linesep}</td></tr>")
1✔
658

659
        buf.write("</tbody>")
1✔
660
        buf.write("</table>")
1✔
661

662
        return buf.getvalue()
1✔
663

664
    def save_as_dataset(self, timeout: int = 60) -> Task:
1✔
665
        """
666
        Save a layer as a new dataset.
667
        """
668
        session = get_session()
1✔
669

670
        with geoengine_openapi_client.ApiClient(session.configuration) as api_client:
1✔
671
            layers_api = geoengine_openapi_client.LayersApi(api_client)
1✔
672
            response = layers_api.layer_to_dataset(self.provider_id, str(self.layer_id), _request_timeout=timeout)
1✔
673

674
        return Task(TaskId.from_response(response))
1✔
675

676
    def to_api_dict(self) -> geoengine_openapi_client.Layer:
1✔
677
        """Convert to a dictionary that can be serialized to JSON"""
678
        return geoengine_openapi_client.Layer(
×
679
            name=self.name,
680
            description=self.description,
681
            id=geoengine_openapi_client.ProviderLayerId(
682
                layer_id=str(self.layer_id),
683
                provider_id=str(self.provider_id),
684
            ),
685
            workflow=self.workflow,
686
            symbology=self.symbology.to_api_dict() if self.symbology is not None else None,
687
            properties=self.properties,
688
            metadata=self.metadata,
689
        )
690

691
    def as_workflow_id(self, timeout: int = 60) -> WorkflowId:
1✔
692
        """
693
        Register a layer as a workflow and returns its workflowId
694
        """
695
        session = get_session()
1✔
696

697
        with geoengine_openapi_client.ApiClient(session.configuration) as api_client:
1✔
698
            layers_api = geoengine_openapi_client.LayersApi(api_client)
1✔
699
            response = layers_api.layer_to_workflow_id_handler(
1✔
700
                self.provider_id, self.layer_id, _request_timeout=timeout
701
            )
702

703
        return WorkflowId.from_response(response)
1✔
704

705
    def as_workflow(self, timeout: int = 60) -> Workflow:
1✔
706
        """
707
        Register a layer as a workflow and returns the workflow
708
        """
709
        workflow_id = self.as_workflow_id(timeout=timeout)
1✔
710

711
        return Workflow(workflow_id)
1✔
712

713

714
def layer_collection(
1✔
715
    layer_collection_id: LayerCollectionId | None = None,
716
    layer_provider_id: LayerProviderId = LAYER_DB_PROVIDER_ID,
717
    timeout: int = 60,
718
) -> LayerCollection:
719
    """
720
    Retrieve a layer collection that contains layers and layer collections.
721
    """
722

723
    session = get_session()
1✔
724

725
    page_limit = 20
1✔
726
    pages: list[geoengine_openapi_client.LayerCollection] = []
1✔
727

728
    offset = 0
1✔
729
    while True:
1✔
730
        with geoengine_openapi_client.ApiClient(session.configuration) as api_client:
1✔
731
            layers_api = geoengine_openapi_client.LayersApi(api_client)
1✔
732

733
            if layer_collection_id is None:
1✔
734
                page = layers_api.list_root_collections_handler(offset, page_limit, _request_timeout=timeout)
1✔
735
            else:
736
                page = layers_api.list_collection_handler(
1✔
737
                    layer_provider_id, layer_collection_id, offset, page_limit, _request_timeout=timeout
738
                )
739

740
        if len(page.items) < page_limit:
1✔
741
            # we need at least one page before breaking
742
            if len(pages) == 0 or len(page.items) > 0:
1✔
743
                pages.append(page)
1✔
744
            break
1✔
745

746
        pages.append(page)
×
747
        offset += page_limit
×
748

749
    return LayerCollection.from_response(pages)
1✔
750

751

752
def layer(layer_id: LayerId, layer_provider_id: LayerProviderId = LAYER_DB_PROVIDER_ID, timeout: int = 60) -> Layer:
1✔
753
    """
754
    Retrieve a layer from the server.
755
    """
756

757
    session = get_session()
1✔
758

759
    with geoengine_openapi_client.ApiClient(session.configuration) as api_client:
1✔
760
        layers_api = geoengine_openapi_client.LayersApi(api_client)
1✔
761
        response = layers_api.layer_handler(layer_provider_id, str(layer_id), _request_timeout=timeout)
1✔
762

763
    return Layer.from_response(response)
1✔
764

765

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

769
    session = get_session()
1✔
770

771
    with geoengine_openapi_client.ApiClient(session.configuration) as api_client:
1✔
772
        layers_api = geoengine_openapi_client.LayersApi(api_client)
1✔
773
        layers_api.remove_layer_from_collection(collection_id, layer_id, _request_timeout=timeout)
1✔
774

775

776
def _delete_layer_collection_from_collection(
1✔
777
    parent_id: LayerCollectionId, collection_id: LayerCollectionId, timeout: int = 60
778
) -> None:
779
    """Delete a layer collection from a collection"""
780

781
    session = get_session()
1✔
782

783
    with geoengine_openapi_client.ApiClient(session.configuration) as api_client:
1✔
784
        layers_api = geoengine_openapi_client.LayersApi(api_client)
1✔
785
        layers_api.remove_collection_from_collection(parent_id, collection_id, _request_timeout=timeout)
1✔
786

787

788
def _delete_layer_collection(collection_id: LayerCollectionId, timeout: int = 60) -> None:
1✔
789
    """Delete a layer collection"""
790

791
    session = get_session()
1✔
792

793
    with geoengine_openapi_client.ApiClient(session.configuration) as api_client:
1✔
794
        layers_api = geoengine_openapi_client.LayersApi(api_client)
1✔
795
        layers_api.remove_collection(collection_id, _request_timeout=timeout)
1✔
796

797

798
def _add_layer_collection_to_collection(
1✔
799
    name: str, description: str, parent_collection_id: LayerCollectionId, timeout: int = 60
800
) -> LayerCollectionId:
801
    """Add a new layer collection"""
802

803
    session = get_session()
1✔
804

805
    with geoengine_openapi_client.ApiClient(session.configuration) as api_client:
1✔
806
        layers_api = geoengine_openapi_client.LayersApi(api_client)
1✔
807
        response = layers_api.add_collection(
1✔
808
            parent_collection_id,
809
            geoengine_openapi_client.AddLayerCollection(
810
                name=name,
811
                description=description,
812
            ),
813
            _request_timeout=timeout,
814
        )
815

816
    return LayerCollectionId(str(response.id))
1✔
817

818

819
def _add_existing_layer_collection_to_collection(
1✔
820
    collection_id: LayerCollectionId, parent_collection_id: LayerCollectionId, timeout: int = 60
821
) -> None:
822
    """Add an existing layer collection to a collection"""
823

824
    session = get_session()
1✔
825

826
    with geoengine_openapi_client.ApiClient(session.configuration) as api_client:
1✔
827
        layers_api = geoengine_openapi_client.LayersApi(api_client)
1✔
828
        layers_api.add_existing_collection_to_collection(parent_collection_id, collection_id, _request_timeout=timeout)
1✔
829

830

831
def _add_layer_to_collection(
1✔
832
    name: str,
833
    description: str,
834
    workflow: dict[str, Any] | WorkflowBuilderOperator,  # TODO: improve type
835
    symbology: Symbology | None,
836
    collection_id: LayerCollectionId,
837
    timeout: int = 60,
838
) -> LayerId:
839
    """Add a new layer"""
840
    # pylint: disable=too-many-arguments,too-many-positional-arguments
841

842
    # convert workflow to dict if necessary
843
    if isinstance(workflow, WorkflowBuilderOperator):
1✔
844
        workflow = workflow.to_workflow_dict()
×
845

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

848
    session = get_session()
1✔
849

850
    with geoengine_openapi_client.ApiClient(session.configuration) as api_client:
1✔
851
        layers_api = geoengine_openapi_client.LayersApi(api_client)
1✔
852
        response = layers_api.add_layer(
1✔
853
            collection_id,
854
            geoengine_openapi_client.AddLayer(
855
                name=name, description=description, workflow=workflow, symbology=symbology_dict
856
            ),
857
            _request_timeout=timeout,
858
        )
859

860
    return LayerId(str(response.id))
1✔
861

862

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

866
    session = get_session()
1✔
867

868
    with geoengine_openapi_client.ApiClient(session.configuration) as api_client:
1✔
869
        layers_api = geoengine_openapi_client.LayersApi(api_client)
1✔
870
        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 · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc