Skip to content

Commit d06c7b6

Browse files
Merge pull request #209 from geo-engine/get_or_create_unique_collection
Get or create unique collection
2 parents 46ce218 + 0dddc43 commit d06c7b6

File tree

10 files changed

+949
-749
lines changed

10 files changed

+949
-749
lines changed

geoengine/__init__.py

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,40 @@
33
from pkg_resources import get_distribution
44
from requests import utils
55
from pydantic import ValidationError
6-
76
from geoengine_openapi_client.exceptions import BadRequestException, OpenApiException, ApiTypeError, ApiValueError, \
87
ApiKeyError, ApiAttributeError, ApiException, NotFoundException
98
from geoengine_openapi_client import UsageSummaryGranularity
10-
from .auth import Session, get_session, initialize, reset
11-
from .colorizer import Colorizer, ColorBreakpoint, LinearGradientColorizer, PaletteColorizer, \
12-
LogarithmicGradientColorizer
13-
from .datasets import upload_dataframe, StoredDataset, add_dataset, volumes, AddDatasetProperties, \
14-
delete_dataset, list_datasets, DatasetListOrder, OgrSourceDatasetTimeType, OgrOnError
15-
from .error import GeoEngineException, InputException, UninitializedException, TypeException, \
16-
MethodNotCalledOnPlotException, MethodNotCalledOnRasterException, MethodNotCalledOnVectorException, \
17-
SpatialReferenceMismatchException, check_response_for_error, ModificationNotOnLayerDbException, \
18-
InvalidUrlException, MissingFieldInResponseException, OGCXMLError
19-
from .layers import Layer, LayerCollection, LayerListing, LayerCollectionListing, \
20-
LayerId, LayerCollectionId, LayerProviderId, \
21-
layer_collection, layer
22-
from .ml import register_ml_model, MlModelConfig, MlModelName
23-
from .permissions import add_permission, remove_permission, add_role, remove_role, assign_role, revoke_role, \
24-
ADMIN_ROLE_ID, REGISTERED_USER_ROLE_ID, ANONYMOUS_USER_ROLE_ID, Permission, Resource, UserId, RoleId
25-
from .tasks import Task, TaskId
9+
10+
from . import workflow_builder
11+
from .raster_workflow_rio_writer import RasterWorkflowRioWriter
12+
from .raster import RasterTile2D
13+
from .workflow import WorkflowId, Workflow, workflow_by_id, register_workflow, get_quota, update_quota, data_usage, \
14+
data_usage_summary
15+
from .util import clamp_datetime_ms_ns
16+
from .resource_identifier import LAYER_DB_PROVIDER_ID, LAYER_DB_ROOT_COLLECTION_ID, DatasetName, UploadId, \
17+
LayerId, LayerCollectionId, LayerProviderId, Resource, MlModelName
2618
from .types import QueryRectangle, GeoTransform, \
2719
RasterResultDescriptor, Provenance, UnitlessMeasurement, ContinuousMeasurement, \
2820
ClassificationMeasurement, BoundingBox2D, TimeInterval, SpatialResolution, SpatialPartition2D, \
2921
RasterSymbology, VectorSymbology, VectorDataType, VectorResultDescriptor, VectorColumnInfo, \
3022
FeatureDataType, RasterBandDescriptor, DEFAULT_ISO_TIME_FORMAT, RasterColorizer, SingleBandRasterColorizer, \
3123
MultiBandRasterColorizer
32-
33-
from .util import clamp_datetime_ms_ns
34-
from .workflow import WorkflowId, Workflow, workflow_by_id, register_workflow, get_quota, update_quota, data_usage, \
35-
data_usage_summary
36-
from .raster import RasterTile2D
37-
from .raster_workflow_rio_writer import RasterWorkflowRioWriter
38-
39-
from . import workflow_builder
24+
from .tasks import Task, TaskId
25+
from .permissions import add_permission, remove_permission, add_role, remove_role, assign_role, revoke_role, \
26+
ADMIN_ROLE_ID, REGISTERED_USER_ROLE_ID, ANONYMOUS_USER_ROLE_ID, Permission, UserId, RoleId
27+
from .ml import register_ml_model, MlModelConfig
28+
from .layers import Layer, LayerCollection, LayerListing, LayerCollectionListing, \
29+
layer_collection, layer
30+
from .error import GeoEngineException, InputException, UninitializedException, TypeException, \
31+
MethodNotCalledOnPlotException, MethodNotCalledOnRasterException, MethodNotCalledOnVectorException, \
32+
SpatialReferenceMismatchException, check_response_for_error, ModificationNotOnLayerDbException, \
33+
InvalidUrlException, MissingFieldInResponseException, OGCXMLError
34+
from .auth import Session, get_session, initialize, reset
35+
from .colorizer import Colorizer, ColorBreakpoint, LinearGradientColorizer, PaletteColorizer, \
36+
LogarithmicGradientColorizer
37+
from .datasets import upload_dataframe, StoredDataset, add_dataset, volumes, AddDatasetProperties, \
38+
delete_dataset, list_datasets, DatasetListOrder, OgrSourceDatasetTimeType, OgrOnError, \
39+
add_or_replace_dataset_with_permissions, dataset_info_by_name
4040

4141

4242
DEFAULT_USER_AGENT = f'geoengine-python/{get_distribution("geoengine").version}'

geoengine/datasets.py

Lines changed: 64 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,24 @@
55
from __future__ import annotations
66
from abc import abstractmethod
77
from pathlib import Path
8-
from typing import List, NamedTuple, Optional, Union, Literal
8+
from typing import List, NamedTuple, Optional, Union, Literal, Tuple
99
from enum import Enum
1010
from uuid import UUID
1111
import tempfile
1212
from attr import dataclass
13+
import geoengine_openapi_client
14+
import geoengine_openapi_client.exceptions
15+
import geoengine_openapi_client.models
1316
import numpy as np
1417
import geopandas as gpd
15-
import geoengine_openapi_client
1618
from geoengine import api
1719
from geoengine.error import InputException, MissingFieldInResponseException
1820
from geoengine.auth import get_session
1921
from geoengine.types import Provenance, RasterSymbology, TimeStep, \
2022
TimeStepGranularity, VectorDataType, VectorResultDescriptor, VectorColumnInfo, \
2123
UnitlessMeasurement, FeatureDataType
24+
from geoengine.resource_identifier import Resource, UploadId, DatasetName
25+
from geoengine.permissions import RoleId, Permission, add_permission
2226

2327

2428
class UnixTimeStampType(Enum):
@@ -256,71 +260,6 @@ def to_api_enum(self) -> geoengine_openapi_client.OgrSourceErrorSpec:
256260
return geoengine_openapi_client.OgrSourceErrorSpec(self.value)
257261

258262

259-
class DatasetName:
260-
'''A wrapper for a dataset name'''
261-
262-
__dataset_name: str
263-
264-
def __init__(self, dataset_name: str) -> None:
265-
self.__dataset_name = dataset_name
266-
267-
@classmethod
268-
def from_response(cls, response: geoengine_openapi_client.CreateDatasetHandler200Response) -> DatasetName:
269-
'''Parse a http response to an `DatasetName`'''
270-
return DatasetName(response.dataset_name)
271-
272-
def __str__(self) -> str:
273-
return self.__dataset_name
274-
275-
def __repr__(self) -> str:
276-
return str(self)
277-
278-
def __eq__(self, other) -> bool:
279-
'''Checks if two dataset names are equal'''
280-
if not isinstance(other, self.__class__):
281-
return False
282-
283-
return self.__dataset_name == other.__dataset_name # pylint: disable=protected-access
284-
285-
def to_api_dict(self) -> geoengine_openapi_client.CreateDatasetHandler200Response:
286-
return geoengine_openapi_client.CreateDatasetHandler200Response(
287-
dataset_name=str(self.__dataset_name)
288-
)
289-
290-
291-
class UploadId:
292-
'''A wrapper for an upload id'''
293-
294-
__upload_id: UUID
295-
296-
def __init__(self, upload_id: UUID) -> None:
297-
self.__upload_id = upload_id
298-
299-
@classmethod
300-
def from_response(cls, response: geoengine_openapi_client.AddCollection200Response) -> UploadId:
301-
'''Parse a http response to an `UploadId`'''
302-
return UploadId(UUID(response.id))
303-
304-
def __str__(self) -> str:
305-
return str(self.__upload_id)
306-
307-
def __repr__(self) -> str:
308-
return str(self)
309-
310-
def __eq__(self, other) -> bool:
311-
'''Checks if two upload ids are equal'''
312-
if not isinstance(other, self.__class__):
313-
return False
314-
315-
return self.__upload_id == other.__upload_id # pylint: disable=protected-access
316-
317-
def to_api_dict(self) -> geoengine_openapi_client.AddCollection200Response:
318-
'''Converts the upload id to a dict for the api'''
319-
return geoengine_openapi_client.AddCollection200Response(
320-
id=str(self.__upload_id)
321-
)
322-
323-
324263
class AddDatasetProperties():
325264
'''The properties for adding a dataset'''
326265
name: Optional[str]
@@ -601,6 +540,42 @@ def add_dataset(data_store: Union[Volume, UploadId],
601540
return DatasetName.from_response(response)
602541

603542

543+
def add_or_replace_dataset_with_permissions(data_store: Union[Volume, UploadId],
544+
properties: AddDatasetProperties,
545+
meta_data: geoengine_openapi_client.MetaDataDefinition,
546+
permission_tuples: Optional[List[Tuple[RoleId, Permission]]] = None,
547+
replace_existing=False,
548+
timeout: int = 60) -> DatasetName:
549+
'''
550+
Add a dataset to the Geo Engine and set permissions.
551+
Replaces existing datasets if forced!
552+
'''
553+
# pylint: disable=too-many-arguments,too-many-positional-arguments
554+
555+
def add_dataset_and_permissions() -> DatasetName:
556+
dataset_name = add_dataset(data_store=data_store, properties=properties, meta_data=meta_data, timeout=timeout)
557+
if permission_tuples is not None:
558+
dataset_res = Resource.from_dataset_name(dataset_name)
559+
for (role, perm) in permission_tuples:
560+
add_permission(role, dataset_res, perm, timeout=timeout)
561+
return dataset_name
562+
563+
if properties.name is None:
564+
dataset_name = add_dataset_and_permissions()
565+
566+
else:
567+
dataset_name = DatasetName(properties.name)
568+
dataset_info = dataset_info_by_name(dataset_name)
569+
if dataset_info is None: # dataset is not existing
570+
dataset_name = add_dataset_and_permissions()
571+
else:
572+
if replace_existing: # dataset exists and we overwrite it
573+
delete_dataset(dataset_name)
574+
dataset_name = add_dataset_and_permissions()
575+
576+
return dataset_name
577+
578+
604579
def delete_dataset(dataset_name: DatasetName, timeout: int = 60) -> None:
605580
'''Delete a dataset. The dataset must be owned by the caller.'''
606581

@@ -636,3 +611,25 @@ def list_datasets(offset: int = 0,
636611
)
637612

638613
return response
614+
615+
616+
def dataset_info_by_name(
617+
dataset_name: Union[DatasetName, str], timeout: int = 60
618+
) -> geoengine_openapi_client.models.Dataset | None:
619+
'''Get dataset information.'''
620+
621+
if not isinstance(dataset_name, DatasetName):
622+
dataset_name = DatasetName(dataset_name)
623+
624+
session = get_session()
625+
626+
with geoengine_openapi_client.ApiClient(session.configuration) as api_client:
627+
datasets_api = geoengine_openapi_client.DatasetsApi(api_client)
628+
res = None
629+
try:
630+
res = datasets_api.get_dataset_handler(str(dataset_name), _request_timeout=timeout)
631+
except geoengine_openapi_client.exceptions.BadRequestException as e:
632+
e_body = e.body
633+
if isinstance(e_body, str) and 'CannotLoadDataset' not in e_body:
634+
raise e
635+
return res

geoengine/layers.py

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from enum import auto
88
from io import StringIO
99
import os
10-
from typing import Any, Dict, Generic, List, Literal, NewType, Optional, TypeVar, Union, cast
10+
from typing import Any, Dict, Generic, List, Literal, Optional, TypeVar, Union, cast, Tuple
1111
from uuid import UUID
1212
import json
1313
from strenum import LowercaseStrEnum
@@ -16,15 +16,11 @@
1616
from geoengine.error import ModificationNotOnLayerDbException, InputException
1717
from geoengine.tasks import Task, TaskId
1818
from geoengine.types import Symbology
19+
from geoengine.permissions import RoleId, Permission, add_permission
1920
from geoengine.workflow import Workflow, WorkflowId
2021
from geoengine.workflow_builder.operators import Operator as WorkflowBuilderOperator
21-
22-
LayerId = NewType('LayerId', str)
23-
LayerCollectionId = NewType('LayerCollectionId', str)
24-
LayerProviderId = NewType('LayerProviderId', UUID)
25-
26-
LAYER_DB_PROVIDER_ID = LayerProviderId(UUID('ce5e84db-cbf9-48a2-9a32-d4b7cc56ea74'))
27-
LAYER_DB_ROOT_COLLECTION_ID = LayerCollectionId('05102bb3-a855-4a37-8a8a-30026a91fef1')
22+
from geoengine.resource_identifier import LayerCollectionId, LayerId, LayerProviderId, \
23+
LAYER_DB_PROVIDER_ID, Resource
2824

2925

3026
class LayerCollectionListingType(LowercaseStrEnum):
@@ -319,6 +315,26 @@ def add_layer(self,
319315

320316
return layer_id
321317

318+
def add_layer_with_permissions(self,
319+
name: str,
320+
description: str,
321+
workflow: Union[Dict[str, Any], WorkflowBuilderOperator], # TODO: improve type
322+
symbology: Optional[Symbology],
323+
permission_tuples: Optional[List[Tuple[RoleId, Permission]]] = None,
324+
timeout: int = 60) -> LayerId:
325+
'''
326+
Add a layer to this collection and set permissions.
327+
'''
328+
329+
layer_id = self.add_layer(name, description, workflow, symbology, timeout)
330+
331+
if permission_tuples is not None:
332+
res = Resource.from_layer_id(layer_id)
333+
for (role, perm) in permission_tuples:
334+
add_permission(role, res, perm)
335+
336+
return layer_id
337+
322338
def add_existing_layer(self,
323339
existing_layer: Union[LayerListing, Layer, LayerId],
324340
timeout: int = 60):
@@ -456,6 +472,63 @@ def search(self, search_string: str, *,
456472

457473
return listings
458474

475+
def get_or_create_unique_collection(
476+
self,
477+
collection_name: str,
478+
create_collection_description: Optional[str] = None,
479+
delete_existing_with_same_name: bool = False,
480+
create_permissions_tuples: Optional[List[Tuple[RoleId, Permission]]] = None
481+
) -> LayerCollection:
482+
'''
483+
Get a unique child by name OR if it does not exist create it.
484+
Removes existing collections with same name if forced!
485+
Sets permissions if the collection is created from a list of tuples
486+
'''
487+
parent_collection = self.reload() # reload just to be safe since self's state change on the server
488+
existing_collections = parent_collection.get_items_by_name(collection_name)
489+
490+
if delete_existing_with_same_name and len(existing_collections) > 0:
491+
for c in existing_collections:
492+
actual = c.load()
493+
if isinstance(actual, LayerCollection):
494+
actual.remove()
495+
parent_collection = parent_collection.reload()
496+
existing_collections = parent_collection.get_items_by_name(collection_name)
497+
498+
if len(existing_collections) == 0:
499+
new_desc = create_collection_description if create_collection_description is not None else collection_name
500+
new_collection = parent_collection.add_collection(collection_name, new_desc)
501+
new_ressource = Resource.from_layer_collection_id(new_collection)
502+
503+
if create_permissions_tuples is not None:
504+
for (role, perm) in create_permissions_tuples:
505+
add_permission(role, new_ressource, perm)
506+
parent_collection = parent_collection.reload()
507+
existing_collections = parent_collection.get_items_by_name(collection_name)
508+
509+
if len(existing_collections) == 0:
510+
raise KeyError(
511+
f"No collection with name {collection_name} exists in {parent_collection.name} and none was created!"
512+
)
513+
514+
if len(existing_collections) > 1:
515+
raise KeyError(f"Multiple collections with name {collection_name} exist in {parent_collection.name}")
516+
517+
res = existing_collections[0].load()
518+
if isinstance(res, Layer):
519+
raise TypeError(f"Found a Layer not a Layer collection for {collection_name}")
520+
521+
return cast(LayerCollection, existing_collections[0].load()) # we know that it is a collection since check that
522+
523+
def __eq__(self, other):
524+
''' Tests if two layer listings are identical '''
525+
if not isinstance(other, self.__class__):
526+
return False
527+
528+
return self.name == other.name and self.description == other.description \
529+
and self.provider_id == other.provider_id \
530+
and self.collection_id == other.collection_id and self.items == other.items
531+
459532

460533
@dataclass(repr=False)
461534
class Layer:
@@ -663,7 +736,7 @@ def layer(layer_id: LayerId,
663736

664737
with geoengine_openapi_client.ApiClient(session.configuration) as api_client:
665738
layers_api = geoengine_openapi_client.LayersApi(api_client)
666-
response = layers_api.layer_handler(str(layer_provider_id), layer_id, _request_timeout=timeout)
739+
response = layers_api.layer_handler(str(layer_provider_id), str(layer_id), _request_timeout=timeout)
667740

668741
return Layer.from_response(response)
669742

0 commit comments

Comments
 (0)