Skip to content

Commit

Permalink
Copy layer group
Browse files Browse the repository at this point in the history
  • Loading branch information
vuilleumierc committed Nov 26, 2024
1 parent 9bfbf4f commit 5f38158
Show file tree
Hide file tree
Showing 8 changed files with 629 additions and 71 deletions.
61 changes: 58 additions & 3 deletions geoservercloud/geoservercloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from geoservercloud.models.datastore import PostGisDataStore
from geoservercloud.models.featuretype import FeatureType
from geoservercloud.models.layer import Layer
from geoservercloud.models.layergroup import LayerGroup
from geoservercloud.models.style import Style
from geoservercloud.models.wmssettings import WmsSettings
from geoservercloud.models.workspace import Workspace
Expand Down Expand Up @@ -402,6 +403,30 @@ def delete_feature_type(
workspace_name, datastore_name, layer_name
)

def get_layer_groups(
self, workspace_name: str
) -> tuple[list[dict[str, str]] | str, int]:
"""
Get all layer groups for a given workspace
"""
layer_groups, status_code = self.rest_service.get_layer_groups(workspace_name)
if isinstance(layer_groups, str):
return layer_groups, status_code
return layer_groups.aslist(), status_code

def get_layer_group(
self, workspace_name: str, layer_group_name: str
) -> tuple[dict[str, Any] | str, int]:
"""
Get a layer group by name
"""
layer_group, status_code = self.rest_service.get_layer_group(
workspace_name, layer_group_name
)
if isinstance(layer_group, str):
return layer_group, status_code
return layer_group.asdict(), status_code

def create_layer_group(
self,
group: str,
Expand All @@ -411,16 +436,46 @@ def create_layer_group(
abstract: str | dict,
epsg: int = 4326,
mode: str = "SINGLE",
enabled: bool = True,
advertised: bool = True,
) -> tuple[str, int]:
"""
Create a layer group if it does not already exist.
Create a layer group or update it if it already exists.
"""
workspace_name = workspace_name or self.default_workspace
if not workspace_name:
raise ValueError("Workspace not provided")
return self.rest_service.create_layer_group(
group, workspace_name, layers, title, abstract, epsg, mode
if not mode in LayerGroup.modes:
raise ValueError(
f"Invalid mode: {mode}, possible values are: {LayerGroup.modes}"
)
bounds = {
"minx": utils.EPSG_BBOX[epsg]["nativeBoundingBox"]["minx"],
"maxx": utils.EPSG_BBOX[epsg]["nativeBoundingBox"]["maxx"],
"miny": utils.EPSG_BBOX[epsg]["nativeBoundingBox"]["miny"],
"maxy": utils.EPSG_BBOX[epsg]["nativeBoundingBox"]["maxy"],
"crs": f"EPSG:{epsg}",
}
layer_group = LayerGroup(
name=group,
mode=mode,
workspace_name=workspace_name,
title=title,
abstract=abstract,
publishables=[f"{workspace_name}:{layer}" for layer in layers],
bounds=bounds,
enabled=enabled,
advertised=advertised,
)
return self.rest_service.create_layer_group(group, workspace_name, layer_group)

def delete_layer_group(
self, workspace_name: str, layer_group_name: str
) -> tuple[str, int]:
"""
Delete a layer group
"""
return self.rest_service.delete_layer_group(workspace_name, layer_group_name)

def create_wmts_layer(
self,
Expand Down
38 changes: 36 additions & 2 deletions geoservercloud/geoservercloudsync.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from argparse import ArgumentParser

from geoservercloud.models.resourcedirectory import ResourceDirectory
from geoservercloud.services import RestService


Expand Down Expand Up @@ -50,7 +49,7 @@ def copy_workspace(
"""
Copy a workspace from the source to the destination GeoServer instance.
If deep_copy is True, the copy includes the PostGIS datastores, the feature types in the datastores,
the corresponding layers and the styles in the workspace (including images).
the corresponding layers, the layer groups and the styles in the workspace (including images).
"""
workspace, status_code = self.src_instance.get_workspace(workspace_name)
if isinstance(workspace, str):
Expand All @@ -69,6 +68,9 @@ def copy_workspace(
)
if self.not_ok(status_code):
return content, status_code
content, status_code = self.copy_layer_groups(workspace_name)
if self.not_ok(status_code):
return content, status_code
return new_workspace, new_ws_status_code

def copy_pg_datastores(
Expand Down Expand Up @@ -165,6 +167,38 @@ def copy_layer(
return layer, status_code
return self.dst_instance.update_layer(layer, workspace_name)

def copy_layer_groups(self, workspace_name: str) -> tuple[str, int]:
"""
Copy all layer groups in a workspace from source to destination GeoServer instance
"""
layer_groups, status_code = self.src_instance.get_layer_groups(workspace_name)
if isinstance(layer_groups, str):
return layer_groups, status_code
elif layer_groups.aslist() == []:
return "", status_code
for layer_group in layer_groups.aslist():
content, status_code = self.copy_layer_group(
workspace_name, layer_group["name"]
)
if self.not_ok(status_code):
return content, status_code
return content, status_code

def copy_layer_group(
self, workspace_name: str, layer_group_name: str
) -> tuple[str, int]:
"""
Copy a layer group from source to destination GeoServer instance
"""
layer_group, status_code = self.src_instance.get_layer_group(
workspace_name, layer_group_name
)
if isinstance(layer_group, str):
return layer_group, status_code
return self.dst_instance.create_layer_group(
layer_group_name, workspace_name, layer_group
)

def copy_styles(
self, workspace_name: str | None = None, include_images: bool = True
) -> tuple[str, int]:
Expand Down
116 changes: 116 additions & 0 deletions geoservercloud/models/layergroup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from typing import Any

from geoservercloud.models.common import I18N, EntityModel, ReferencedObjectModel


class LayerGroup(EntityModel):
modes = ["SINGLE", "OPAQUE_CONTAINER", "NAMED", "CONTAINER", "EO"]

def __init__(
self,
name: str | None = None,
mode: str | None = None,
enabled: bool | None = None,
advertised: bool | None = None,
workspace_name: str | None = None,
title: dict[str, str] | str | None = None,
abstract: dict[str, str] | str | None = None,
publishables: list[str] | None = None,
styles: list[str] | None = None,
bounds: dict[str, Any] | None = None,
):
self.name: str | None = name
self.mode: str | None = mode
self.enabled: bool | None = enabled
self.advertised: bool | None = advertised
self.workspace: ReferencedObjectModel | None = (
ReferencedObjectModel(workspace_name) if workspace_name else None
)
self.title: I18N | None = (
I18N(("title", "internationalTitle"), title) if title else None
)
self.abstract: I18N | None = (
I18N(("abstract", "internationalAbstract"), abstract) if abstract else None
)
self.publishables: list[ReferencedObjectModel] | None = (
[ReferencedObjectModel(publishable) for publishable in publishables]
if publishables
else None
)
self.styles: list[ReferencedObjectModel] | None = (
[ReferencedObjectModel(style) for style in styles] if styles else None
)
self.bounds: dict[str, int | str] | None = bounds

@property
def workspace_name(self) -> str | None:
return self.workspace.name if self.workspace else None

@classmethod
def from_get_response_payload(cls, content):
layer_group: dict[str, Any] = content["layerGroup"]
# publishables: list of dict or dict (if only one layer)
publishables: list[dict[str, str]] | dict[str, str] = layer_group[
"publishables"
]["published"]
if isinstance(publishables, dict):
publishables = [publishables]
# style: list of dict, dict (if only one layer) or list of empty strings (if using default layer styles)
styles: list[dict] | dict | list[str] = layer_group["styles"]["style"]
if isinstance(styles, dict):
styles = [styles["name"]]
if isinstance(styles, list):
styles = [s["name"] if isinstance(s, dict) else s for s in styles]
return cls(
name=layer_group["name"],
mode=layer_group["mode"],
enabled=layer_group.get("enabled"),
advertised=layer_group.get("advertised"),
workspace_name=layer_group["workspace"]["name"],
title=layer_group.get("internationalTitle", layer_group.get("title")),
abstract=layer_group.get(
"internationalAbstract", layer_group.get("abstract")
),
publishables=[p["name"] for p in publishables],
styles=styles,
bounds=layer_group.get("bounds"),
)

def asdict(self) -> dict[str, Any]:
optional_items = {
"name": self.name,
"mode": self.mode,
"enabled": self.enabled,
"advertised": self.advertised,
"bounds": self.bounds,
}
content = EntityModel.add_items_to_dict({}, optional_items)
if self.workspace:
content["workspace"] = self.workspace.asdict()
if self.publishables:
content["publishables"] = {
"published": [
{"@type": "layer", "name": p.name} for p in self.publishables
]
}
if self.styles:
content["styles"] = {"style": [s.asdict() for s in self.styles]}
elif self.publishables:
content["styles"] = {"style": [{"name": ""}] * len(self.publishables)}
if self.title:
content.update(self.title.asdict())
if self.abstract:
content.update(self.abstract.asdict())
return content

def post_payload(self) -> dict[str, Any]:
return {"layerGroup": self.asdict()}

def put_payload(self) -> dict[str, Any]:
content = self.post_payload()
# Force a null value on non-i18ned attributes, otherwise GeoServer sets it to the first i18n value
if content["layerGroup"].get("internationalTitle"):
content["layerGroup"]["title"] = None
if content["layerGroup"].get("internationalAbstract"):
content["layerGroup"]["abstract"] = None
return content
21 changes: 21 additions & 0 deletions geoservercloud/models/layergroups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import json

from geoservercloud.models.common import ListModel


class LayerGroups(ListModel):
def __init__(self, layergroups: list = []) -> None:
self._layergroups = layergroups

@classmethod
def from_get_response_payload(cls, content: dict):
feature_types: str | dict = content["layerGroups"]
if not feature_types:
return cls()
return cls(feature_types["layerGroup"]) # type: ignore

def aslist(self) -> list[dict[str, str]]:
return self._layergroups

def __repr__(self):
return json.dumps(self._layergroups, indent=4)
49 changes: 31 additions & 18 deletions geoservercloud/services/restservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from geoservercloud.models.featuretype import FeatureType
from geoservercloud.models.featuretypes import FeatureTypes
from geoservercloud.models.layer import Layer
from geoservercloud.models.layergroup import LayerGroup
from geoservercloud.models.layergroups import LayerGroups
from geoservercloud.models.resourcedirectory import ResourceDirectory
from geoservercloud.models.style import Style
from geoservercloud.models.styles import Styles
Expand Down Expand Up @@ -297,37 +299,48 @@ def delete_feature_type(
)
return response.content.decode(), response.status_code

def get_layer_groups(self, workspace_name: str) -> tuple[LayerGroups | str, int]:
response: Response = self.rest_client.get(
self.rest_endpoints.layergroups(workspace_name)
)
return self.deserialize_response(response, LayerGroups)

def get_layer_group(
self, workspace_name: str, layer_group_name: str
) -> tuple[LayerGroup | str, int]:
response: Response = self.rest_client.get(
self.rest_endpoints.layergroup(workspace_name, layer_group_name)
)
return self.deserialize_response(response, LayerGroup)

def create_layer_group(
self,
group: str,
layer_group_name: str,
workspace_name: str,
layers: list[str],
title: str | dict,
abstract: str | dict,
epsg: int = 4326,
mode: str = "SINGLE",
layer_group: LayerGroup,
) -> tuple[str, int]:
payload: dict[str, dict[str, Any]] = Templates.layer_group(
group=group,
layers=layers,
workspace=workspace_name,
title=title,
abstract=abstract,
epsg=epsg,
mode=mode,
)
if not self.resource_exists(
self.rest_endpoints.layergroup(workspace_name, group)
self.rest_endpoints.layergroup(workspace_name, layer_group_name)
):
response: Response = self.rest_client.post(
self.rest_endpoints.layergroups(workspace_name), json=payload
self.rest_endpoints.layergroups(workspace_name),
json=layer_group.post_payload(),
)
else:
response = self.rest_client.put(
self.rest_endpoints.layergroup(workspace_name, group), json=payload
self.rest_endpoints.layergroup(workspace_name, layer_group_name),
json=layer_group.put_payload(),
)
return response.content.decode(), response.status_code

def delete_layer_group(
self, workspace_name: str, layer_group_name: str
) -> tuple[str, int]:
response: Response = self.rest_client.delete(
self.rest_endpoints.layergroup(workspace_name, layer_group_name)
)
return response.content.decode(), response.status_code

def get_styles(self, workspace_name: str | None = None) -> tuple[Styles | str, int]:
path = self.rest_endpoints.styles(workspace_name=workspace_name)
response: Response = self.rest_client.get(path)
Expand Down
Loading

0 comments on commit 5f38158

Please sign in to comment.