Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get or create unique collection #209

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions geoengine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,19 @@
SpatialReferenceMismatchException, check_response_for_error, ModificationNotOnLayerDbException, \
InvalidUrlException, MissingFieldInResponseException
from .layers import Layer, LayerCollection, LayerListing, LayerCollectionListing, \
LayerId, LayerCollectionId, LayerProviderId, \
layer_collection, layer
from .ml import register_ml_model, MlModelConfig
from .permissions import add_permission, remove_permission, add_role, remove_role, assign_role, revoke_role, \
ADMIN_ROLE_ID, REGISTERED_USER_ROLE_ID, ANONYMOUS_USER_ROLE_ID, Permission, Resource, UserId, RoleId
ADMIN_ROLE_ID, REGISTERED_USER_ROLE_ID, ANONYMOUS_USER_ROLE_ID, Permission, UserId, RoleId
from .tasks import Task, TaskId
from .types import QueryRectangle, GeoTransform, \
RasterResultDescriptor, Provenance, UnitlessMeasurement, ContinuousMeasurement, \
ClassificationMeasurement, BoundingBox2D, TimeInterval, SpatialResolution, SpatialPartition2D, \
RasterSymbology, VectorSymbology, VectorDataType, VectorResultDescriptor, VectorColumnInfo, \
FeatureDataType, RasterBandDescriptor, DEFAULT_ISO_TIME_FORMAT, RasterColorizer, SingleBandRasterColorizer, \
MultiBandRasterColorizer
from .resource_identifier import LAYER_DB_PROVIDER_ID, LAYER_DB_ROOT_COLLECTION_ID, DatasetName, UploadId, \
LayerId, LayerCollectionId, LayerProviderId, Resource

from .util import clamp_datetime_ms_ns
from .workflow import WorkflowId, Workflow, workflow_by_id, register_workflow, get_quota, update_quota
Expand Down
79 changes: 13 additions & 66 deletions geoengine/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@
from uuid import UUID
import tempfile
from attr import dataclass
import geoengine_openapi_client
import geoengine_openapi_client.models
import numpy as np
import geopandas as gpd
import geoengine_openapi_client
from geoengine import api
from geoengine.error import InputException, MissingFieldInResponseException
from geoengine.auth import get_session
from geoengine.types import Provenance, RasterSymbology, TimeStep, \
TimeStepGranularity, VectorDataType, VectorResultDescriptor, VectorColumnInfo, \
UnitlessMeasurement, FeatureDataType
from geoengine.resource_identifier import UploadId, DatasetName


class UnixTimeStampType(Enum):
Expand Down Expand Up @@ -256,71 +258,6 @@ def to_api_enum(self) -> geoengine_openapi_client.OgrSourceErrorSpec:
return geoengine_openapi_client.OgrSourceErrorSpec(self.value)


class DatasetName:
'''A wrapper for a dataset id'''

__dataset_name: str

def __init__(self, dataset_name: str) -> None:
self.__dataset_name = dataset_name

@classmethod
def from_response(cls, response: geoengine_openapi_client.CreateDatasetHandler200Response) -> DatasetName:
'''Parse a http response to an `DatasetId`'''
return DatasetName(response.dataset_name)

def __str__(self) -> str:
return self.__dataset_name

def __repr__(self) -> str:
return str(self)

def __eq__(self, other) -> bool:
'''Checks if two dataset ids are equal'''
if not isinstance(other, self.__class__):
return False

return self.__dataset_name == other.__dataset_name # pylint: disable=protected-access

def to_api_dict(self) -> geoengine_openapi_client.CreateDatasetHandler200Response:
return geoengine_openapi_client.CreateDatasetHandler200Response(
dataset_name=str(self.__dataset_name)
)


class UploadId:
'''A wrapper for an upload id'''

__upload_id: UUID

def __init__(self, upload_id: UUID) -> None:
self.__upload_id = upload_id

@classmethod
def from_response(cls, response: geoengine_openapi_client.AddCollection200Response) -> UploadId:
'''Parse a http response to an `UploadId`'''
return UploadId(UUID(response.id))

def __str__(self) -> str:
return str(self.__upload_id)

def __repr__(self) -> str:
return str(self)

def __eq__(self, other) -> bool:
'''Checks if two upload ids are equal'''
if not isinstance(other, self.__class__):
return False

return self.__upload_id == other.__upload_id # pylint: disable=protected-access

def to_api_dict(self) -> geoengine_openapi_client.AddCollection200Response:
'''Converts the upload id to a dict for the api'''
return geoengine_openapi_client.AddCollection200Response(
id=str(self.__upload_id)
)


class AddDatasetProperties():
'''The properties for adding a dataset'''
name: Optional[str]
Expand Down Expand Up @@ -630,3 +567,13 @@ def list_datasets(offset: int = 0,
)

return response


def dataset_info_by_name(dataset_name: DatasetName, timeout: int = 60) -> geoengine_openapi_client.models.Dataset:
'''Delete a dataset. The dataset must be owned by the caller.'''

session = get_session()

with geoengine_openapi_client.ApiClient(session.configuration) as api_client:
datasets_api = geoengine_openapi_client.DatasetsApi(api_client)
return datasets_api.get_dataset_handler(str(dataset_name), _request_timeout=timeout)
60 changes: 52 additions & 8 deletions geoengine/layers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from enum import auto
from io import StringIO
import os
from typing import Any, Dict, Generic, List, Literal, NewType, Optional, TypeVar, Union, cast
from typing import Any, Dict, Generic, List, Literal, Optional, TypeVar, Union, cast, Tuple
from uuid import UUID
import json
from strenum import LowercaseStrEnum
Expand All @@ -16,15 +16,11 @@
from geoengine.error import ModificationNotOnLayerDbException, InputException
from geoengine.tasks import Task, TaskId
from geoengine.types import Symbology
from geoengine.permissions import RoleId, Permission, add_permission
from geoengine.workflow import Workflow, WorkflowId
from geoengine.workflow_builder.operators import Operator as WorkflowBuilderOperator

LayerId = NewType('LayerId', str)
LayerCollectionId = NewType('LayerCollectionId', str)
LayerProviderId = NewType('LayerProviderId', UUID)

LAYER_DB_PROVIDER_ID = LayerProviderId(UUID('ce5e84db-cbf9-48a2-9a32-d4b7cc56ea74'))
LAYER_DB_ROOT_COLLECTION_ID = LayerCollectionId('05102bb3-a855-4a37-8a8a-30026a91fef1')
from geoengine.resource_identifier import LayerCollectionId, LayerId, LayerProviderId, \
LAYER_DB_PROVIDER_ID, Resource


class LayerCollectionListingType(LowercaseStrEnum):
Expand Down Expand Up @@ -456,6 +452,54 @@ def search(self, search_string: str, *,

return listings

def get_or_create_unique_collection(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is quite complex. Could you add a test that verifies that it works?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i really don't want to build the mock thingy for that...

self,
collection_name: str,
create_collection_description: Optional[str] = None,
delete_existing_with_same_name: bool = False,
create_permissions_tuples: Optional[List[Tuple[RoleId, Permission]]] = None
) -> LayerCollection:
'''
Get a unique child by name OR if it does not exist create it.
Removes existing collections with same name if forced!
Sets permissions if the collection is created from a list of tuples
'''
parent_collection = self.reload() # reload just to be safe since self's state change on the server
existing_collections = parent_collection.get_items_by_name(collection_name)

if delete_existing_with_same_name and len(existing_collections) > 0:
for c in existing_collections:
actual = c.load()
if isinstance(actual, LayerCollection):
actual.remove()
parent_collection = parent_collection.reload()
existing_collections = parent_collection.get_items_by_name(collection_name)

if len(existing_collections) == 0:
new_desc = create_collection_description if create_collection_description is not None else collection_name
new_collection = parent_collection.add_collection(collection_name, new_desc)
new_ressource = Resource.from_layer_collection_id(new_collection)

if create_permissions_tuples is not None:
for (role, perm) in create_permissions_tuples:
add_permission(role, new_ressource, perm)
parent_collection = parent_collection.reload()
existing_collections = parent_collection.get_items_by_name(collection_name)

if len(existing_collections) == 0:
raise KeyError(
f"No collection with name {collection_name} exists in {parent_collection.name} and none was created!"
)

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

res = existing_collections[0].load()
if isinstance(res, Layer):
raise TypeError(f"Found a Layer not a Layer collection for {collection_name}")

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


@dataclass(repr=False)
class Layer:
Expand Down
45 changes: 2 additions & 43 deletions geoengine/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@
from enum import Enum

import ast
from typing import Dict, Literal, Any
from typing import Dict
from uuid import UUID

import geoengine_openapi_client

from geoengine.auth import get_session
from geoengine.datasets import DatasetName
from geoengine.resource_identifier import Resource
from geoengine.error import GeoEngineException
from geoengine.layers import LayerCollectionId, LayerId


class RoleId:
Expand Down Expand Up @@ -79,46 +78,6 @@ def __repr__(self) -> str:
return repr(self.__user_id)


class Resource:
'''A wrapper for a resource id'''

def __init__(self, resource_type: Literal['dataset', 'layer', 'layerCollection'],
resource_id: str) -> None:
'''Create a resource id'''
self.__type = resource_type
self.__id = resource_id

@classmethod
def from_layer_id(cls, layer_id: LayerId) -> Resource:
'''Create a resource id from a layer id'''
return Resource('layer', str(layer_id))

@classmethod
def from_layer_collection_id(cls, layer_collection_id: LayerCollectionId) -> Resource:
'''Create a resource id from a layer collection id'''
return Resource('layerCollection', str(layer_collection_id))

@classmethod
def from_dataset_name(cls, dataset_name: DatasetName) -> Resource:
'''Create a resource id from a dataset id'''
return Resource('dataset', str(dataset_name))

def to_api_dict(self) -> geoengine_openapi_client.Resource:
'''Convert to a dict for the API'''
inner: Any = None

if self.__type == "layer":
inner = geoengine_openapi_client.LayerResource(type="layer", id=self.__id)
elif self.__type == "layerCollection":
inner = geoengine_openapi_client.LayerCollectionResource(type="layerCollection", id=self.__id)
elif self.__type == "project":
inner = geoengine_openapi_client.ProjectResource(type="project", id=self.__id)
elif self.__type == "dataset":
inner = geoengine_openapi_client.DatasetResource(type="dataset", id=self.__id)

return geoengine_openapi_client.Resource(inner)


class Permission(str, Enum):
'''A permission'''
READ = 'Read'
Expand Down
118 changes: 118 additions & 0 deletions geoengine/resource_identifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
''' Types that identify a ressource in the Geo Engine'''

from __future__ import annotations
from typing import Any, Literal, NewType
from uuid import UUID
import geoengine_openapi_client

LayerId = NewType('LayerId', str)
LayerCollectionId = NewType('LayerCollectionId', str)
LayerProviderId = NewType('LayerProviderId', UUID)

LAYER_DB_PROVIDER_ID = LayerProviderId(UUID('ce5e84db-cbf9-48a2-9a32-d4b7cc56ea74'))
LAYER_DB_ROOT_COLLECTION_ID = LayerCollectionId('05102bb3-a855-4a37-8a8a-30026a91fef1')


class DatasetName:
'''A wrapper for a dataset id'''

__dataset_name: str

def __init__(self, dataset_name: str) -> None:
self.__dataset_name = dataset_name

@classmethod
def from_response(cls, response: geoengine_openapi_client.CreateDatasetHandler200Response) -> DatasetName:
'''Parse a http response to an `DatasetId`'''
return DatasetName(response.dataset_name)

def __str__(self) -> str:
return self.__dataset_name

def __repr__(self) -> str:
return str(self)

def __eq__(self, other) -> bool:
'''Checks if two dataset ids are equal'''
if not isinstance(other, self.__class__):
return False

return self.__dataset_name == other.__dataset_name # pylint: disable=protected-access

def to_api_dict(self) -> geoengine_openapi_client.CreateDatasetHandler200Response:
return geoengine_openapi_client.CreateDatasetHandler200Response(
dataset_name=str(self.__dataset_name)
)


class UploadId:
'''A wrapper for an upload id'''

__upload_id: UUID

def __init__(self, upload_id: UUID) -> None:
self.__upload_id = upload_id

@classmethod
def from_response(cls, response: geoengine_openapi_client.AddCollection200Response) -> UploadId:
'''Parse a http response to an `UploadId`'''
return UploadId(UUID(response.id))

def __str__(self) -> str:
return str(self.__upload_id)

def __repr__(self) -> str:
return str(self)

def __eq__(self, other) -> bool:
'''Checks if two upload ids are equal'''
if not isinstance(other, self.__class__):
return False

return self.__upload_id == other.__upload_id # pylint: disable=protected-access

def to_api_dict(self) -> geoengine_openapi_client.AddCollection200Response:
'''Converts the upload id to a dict for the api'''
return geoengine_openapi_client.AddCollection200Response(
id=str(self.__upload_id)
)


class Resource:
'''A wrapper for a resource id'''

def __init__(self, resource_type: Literal['dataset', 'layer', 'layerCollection'],
resource_id: str) -> None:
'''Create a resource id'''
self.__type = resource_type
self.__id = resource_id

@classmethod
def from_layer_id(cls, layer_id: LayerId) -> Resource:
'''Create a resource id from a layer id'''
return Resource('layer', str(layer_id))

@classmethod
def from_layer_collection_id(cls, layer_collection_id: LayerCollectionId) -> Resource:
'''Create a resource id from a layer collection id'''
return Resource('layerCollection', str(layer_collection_id))

@classmethod
def from_dataset_name(cls, dataset_name: DatasetName) -> Resource:
'''Create a resource id from a dataset id'''
return Resource('dataset', str(dataset_name))

def to_api_dict(self) -> geoengine_openapi_client.Resource:
'''Convert to a dict for the API'''
inner: Any = None

if self.__type == "layer":
inner = geoengine_openapi_client.LayerResource(type="layer", id=self.__id)
elif self.__type == "layerCollection":
inner = geoengine_openapi_client.LayerCollectionResource(type="layerCollection", id=self.__id)
elif self.__type == "project":
inner = geoengine_openapi_client.ProjectResource(type="project", id=self.__id)
elif self.__type == "dataset":
inner = geoengine_openapi_client.DatasetResource(type="dataset", id=self.__id)

return geoengine_openapi_client.Resource(inner)