diff --git a/geoservercloud/geoservercloud.py b/geoservercloud/geoservercloud.py index bda038f..622dc65 100644 --- a/geoservercloud/geoservercloud.py +++ b/geoservercloud/geoservercloud.py @@ -11,6 +11,7 @@ PostGisDataStore, Workspace, ) +from geoservercloud.models.layer import Layer from geoservercloud.services import OwsService, RestService from geoservercloud.templates import Templates @@ -403,9 +404,11 @@ def create_style_from_file( return self.rest_service.create_style_from_file(style, file, workspace_name) def set_default_layer_style( - self, layer: str, workspace_name: str, style: str + self, layer_name: str, workspace_name: str, style: str ) -> tuple[str, int]: - return self.rest_service.set_default_layer_style(layer, workspace_name, style) + """Set the default style for a layer""" + layer = Layer(layer_name, default_style_name=style) + return self.rest_service.update_layer(layer, workspace_name) def get_wms_layers( self, workspace_name: str, accept_languages: str | None = None diff --git a/geoservercloud/models/common.py b/geoservercloud/models/common.py index 360c7e2..018d653 100644 --- a/geoservercloud/models/common.py +++ b/geoservercloud/models/common.py @@ -18,6 +18,18 @@ def post_payload(self) -> dict[str, Any]: def put_payload(self) -> dict[str, Any]: raise NotImplementedError + @staticmethod + def add_items_to_dict(content: dict, items: dict[str, Any]) -> dict[Any, Any]: + for key, value in items.items(): + content = EntityModel.add_item_to_dict(content, key, value) + return content + + @staticmethod + def add_item_to_dict(content: dict, key: str, value: Any) -> dict[Any, Any]: + if value is not None: + content[key] = value + return content + class ListModel(BaseModel): def aslist(self) -> list: @@ -26,8 +38,8 @@ def aslist(self) -> list: class ReferencedObjectModel(BaseModel): def __init__(self, name: str, href: str | None = None): - self.name = name - self.href = href + self.name: str = name + self.href: str | None = href @classmethod def from_get_response_payload(cls, content: dict): @@ -35,10 +47,7 @@ def from_get_response_payload(cls, content: dict): cls.href = content["href"] def asdict(self) -> dict[str, str]: - content = {"name": self.name} - if self.href: - content["href"] = self.href - return content + return EntityModel.add_item_to_dict({"name": self.name}, "href", self.href) class KeyDollarListDict(dict): diff --git a/geoservercloud/models/datastore.py b/geoservercloud/models/datastore.py index 277936e..5efe034 100644 --- a/geoservercloud/models/datastore.py +++ b/geoservercloud/models/datastore.py @@ -40,15 +40,13 @@ def asdict(self) -> dict[str, Any]: "connectionParameters": {"entry": dict(self.connection_parameters)}, "workspace": self.workspace_name, } - if self.description: - content["description"] = self.description - if self.enabled: - content["enabled"] = self.enabled - if self._default is not None: - content["_default"] = self._default - if self.disable_on_conn_failure is not None: - content["disableOnConnFailure"] = self.disable_on_conn_failure - return content + optional_items = { + "description": self.description, + "enabled": self.enabled, + "_default": self._default, + "disableOnConnFailure": self.disable_on_conn_failure, + } + return EntityModel.add_items_to_dict(content, optional_items) def post_payload(self) -> dict[str, Any]: content = self.asdict() diff --git a/geoservercloud/models/featuretype.py b/geoservercloud/models/featuretype.py index c789bc0..a80dfac 100644 --- a/geoservercloud/models/featuretype.py +++ b/geoservercloud/models/featuretype.py @@ -158,14 +158,10 @@ def asdict(self) -> dict[str, Any]: } if self.namespace is not None: content["namespace"] = self.namespace.asdict() - if self.srs: - content["srs"] = self.srs if self.title: content.update(self.title.asdict()) if self.abstract: content.update(self.abstract.asdict()) - if self.keywords: - content["keywords"] = self.keywords if self.native_bounding_box: content["nativeBoundingBox"] = self.native_bounding_box elif self.epsg_code: @@ -178,35 +174,25 @@ def asdict(self) -> dict[str, Any]: content["latLonBoundingBox"] = EPSG_BBOX[self.epsg_code][ "latLonBoundingBox" ] - if self.attributes: - content["attributes"] = self.attributes - if self.projection_policy is not None: - content["projectionPolicy"] = self.projection_policy - if self.enabled is not None: - content["enabled"] = self.enabled - if self.advertised is not None: - content["advertised"] = self.advertised - if self.service_configuration is not None: - content["serviceConfiguration"] = self.service_configuration - if self.simple_conversion_enabled is not None: - content["simpleConversionEnabled"] = self.simple_conversion_enabled - if self.max_features is not None: - content["maxFeatures"] = self.max_features - if self.num_decimals is not None: - content["numDecimals"] = self.num_decimals - if self.pad_with_zeros is not None: - content["padWithZeros"] = self.pad_with_zeros - if self.forced_decimals is not None: - content["forcedDecimals"] = self.forced_decimals - if self.overriding_service_srs is not None: - content["overridingServiceSRS"] = self.overriding_service_srs - if self.skip_number_match is not None: - content["skipNumberMatch"] = self.skip_number_match - if self.circular_arc_present is not None: - content["circularArcPresent"] = self.circular_arc_present - if self.encode_measures is not None: - content["encodeMeasures"] = self.encode_measures - return content + optional_items = { + "srs": self.srs, + "keywords": self.keywords, + "attributes": self.attributes, + "projectionPolicy": self.projection_policy, + "enabled": self.enabled, + "advertised": self.advertised, + "serviceConfiguration": self.service_configuration, + "simpleConversionEnabled": self.simple_conversion_enabled, + "maxFeatures": self.max_features, + "numDecimals": self.num_decimals, + "padWithZeros": self.pad_with_zeros, + "forcedDecimals": self.forced_decimals, + "overridingServiceSRS": self.overriding_service_srs, + "skipNumberMatch": self.skip_number_match, + "circularArcPresent": self.circular_arc_present, + "encodeMeasures": self.encode_measures, + } + return EntityModel.add_items_to_dict(content, optional_items) def post_payload(self) -> dict[str, Any]: content = self.asdict() diff --git a/geoservercloud/models/layer.py b/geoservercloud/models/layer.py new file mode 100644 index 0000000..2d97097 --- /dev/null +++ b/geoservercloud/models/layer.py @@ -0,0 +1,78 @@ +from typing import Any + +from geoservercloud.models import EntityModel, ReferencedObjectModel +from geoservercloud.models.styles import Styles + + +class Layer(EntityModel): + def __init__( + self, + name: str, + resource_name: str | None = None, + type: str | None = None, + default_style_name: str | None = None, + styles: list | None = None, + queryable: bool | None = None, + attribution: dict[str, int] | None = None, + ) -> None: + self.name: str = name + self.type: str | None = type + self.resource: ReferencedObjectModel | None = None + if resource_name: + self.resource = ReferencedObjectModel(resource_name) + self.default_style: ReferencedObjectModel | None = None + if default_style_name: + self.default_style = ReferencedObjectModel(default_style_name) + self.styles: list | None = styles + self.queryable: bool | None = queryable + self.attribution: dict[str, Any] | None = attribution + + @property + def resource_name(self) -> str | None: + return self.resource.name if self.resource else None + + @property + def default_style_name(self) -> str | None: + return self.default_style.name if self.default_style else None + + @classmethod + def from_get_response_payload(cls, content: dict): + layer = content["layer"] + if layer.get("styles"): + styles = Styles.from_get_response_payload(layer).aslist() + else: + styles = None + return cls( + name=layer["name"], + resource_name=layer["resource"]["name"], + type=layer["type"], + default_style_name=layer["defaultStyle"]["name"], + styles=styles, + attribution=layer["attribution"], + queryable=layer.get("queryable"), + ) + + def asdict(self) -> dict[str, Any]: + content: dict[str, Any] = {"name": self.name} + if self.styles is not None: + content.update(Styles(self.styles).post_payload()) + optional_items = { + "name": self.name, + "type": self.type, + "resource": self.resource_name, + "defaultStyle": self.default_style_name, + "attribution": self.attribution, + "queryable": self.queryable, + } + return EntityModel.add_items_to_dict(content, optional_items) + + def post_payload(self) -> dict[str, dict[str, Any]]: + content = self.asdict() + if self.resource: + content["resource"] = self.resource.asdict() + if self.default_style: + content["defaultStyle"] = self.default_style.asdict() + return {"layer": content} + + def put_payload(self) -> dict[str, dict[str, Any]]: + return self.post_payload() diff --git a/geoservercloud/models/style.py b/geoservercloud/models/style.py index d535b90..138d67a 100644 --- a/geoservercloud/models/style.py +++ b/geoservercloud/models/style.py @@ -93,17 +93,14 @@ def asdict(self) -> dict[str, Any]: "format": self.format, "languageVersion": self.language_version, } - if self.workspace_name: - content["workspace"] = self.workspace_name - if self.filename: - content["filename"] = self.filename - if self.date_created: - content["dateCreated"] = self.date_created - if self.date_modified: - content["dateModified"] = self.date_modified - if self.legend: - content["legend"] = self.legend - return content + optional_items = { + "workspace": self.workspace_name, + "filename": self.filename, + "dateCreated": self.date_created, + "dateModified": self.date_modified, + "legend": self.legend, + } + return EntityModel.add_items_to_dict(content, optional_items) def post_payload(self) -> dict[str, dict[str, Any]]: content = self.asdict() diff --git a/geoservercloud/models/styles.py b/geoservercloud/models/styles.py index 06bd17f..b8dfe46 100644 --- a/geoservercloud/models/styles.py +++ b/geoservercloud/models/styles.py @@ -19,3 +19,6 @@ def from_get_response_payload(cls, content: dict): if not styles: return cls([]) return cls([style["name"] for style in styles["style"]]) # type: ignore + + def post_payload(self) -> dict[str, dict[str, list[dict[str, str]]]]: + return {"styles": {"style": [{"name": style} for style in self._styles]}} diff --git a/geoservercloud/services/restservice.py b/geoservercloud/services/restservice.py index bf2f7a6..6d08239 100644 --- a/geoservercloud/services/restservice.py +++ b/geoservercloud/services/restservice.py @@ -16,6 +16,7 @@ ) from geoservercloud.models.featuretype import FeatureType from geoservercloud.models.featuretypes import FeatureTypes +from geoservercloud.models.layer import Layer from geoservercloud.services.restclient import RestClient from geoservercloud.templates import Templates @@ -394,12 +395,10 @@ def create_style_from_file( response = self.rest_client.put(resource_path, data=data, headers=headers) return response.content.decode(), response.status_code - def set_default_layer_style( - self, layer: str, workspace_name: str, style: str - ) -> tuple[str, int]: - data = {"layer": {"defaultStyle": {"name": style}}} + def update_layer(self, layer: Layer, workspace_name: str) -> tuple[str, int]: response: Response = self.rest_client.put( - self.rest_endpoints.workspace_layer(workspace_name, layer), json=data + self.rest_endpoints.workspace_layer(workspace_name, layer.name), + json=layer.put_payload(), ) return response.content.decode(), response.status_code diff --git a/tests/models/test_layer.py b/tests/models/test_layer.py new file mode 100644 index 0000000..8b05bd8 --- /dev/null +++ b/tests/models/test_layer.py @@ -0,0 +1,78 @@ +from geoservercloud.models.layer import Layer + + +def test_layer_post_payload(): + layer = Layer( + name="test_point", + resource_name="test_workspace:test_point", + type="VECTOR", + default_style_name="point", + styles=["burg", "capitals"], + queryable=True, + attribution={"logoWidth": 0, "logoHeight": 0}, + ) + + content = layer.post_payload() + + assert content == { + "layer": { + "name": "test_point", + "type": "VECTOR", + "defaultStyle": {"name": "point"}, + "styles": { + "style": [ + {"name": "burg"}, + {"name": "capitals"}, + ], + }, + "resource": { + "name": "test_workspace:test_point", + }, + "queryable": True, + "attribution": {"logoWidth": 0, "logoHeight": 0}, + } + } + + +def test_from_get_response_payload(): + content = { + "layer": { + "name": "test_point", + "type": "VECTOR", + "defaultStyle": { + "name": "point", + "href": "http://localhost:9099/geoserver/rest/styles/point.json", + }, + "styles": { + "@class": "linked-hash-set", + "style": [ + { + "name": "burg", + "href": "http://localhost:9099/geoserver/rest/styles/burg.json", + }, + { + "name": "capitals", + "href": "http://localhost:9099/geoserver/rest/styles/capitals.json", + }, + ], + }, + "resource": { + "@class": "featureType", + "name": "test_workspace:test_point", + "href": "http://localhost:9099/geoserver/rest/workspaces/elden/datastores/elden/featuretypes/test_point.json", + }, + "attribution": {"logoWidth": 0, "logoHeight": 0}, + "dateCreated": "2024-11-06 10:16:07.328 UTC", + "dateModified": "2024-11-06 14:48:09.460 UTC", + } + } + + layer = Layer.from_get_response_payload(content) + + assert layer.name == "test_point" + assert layer.resource_name == "test_workspace:test_point" + assert layer.type == "VECTOR" + assert layer.default_style_name == "point" + assert layer.styles == ["burg", "capitals"] + assert layer.attribution == {"logoWidth": 0, "logoHeight": 0} + assert layer.queryable is None diff --git a/tests/models/test_styles.py b/tests/models/test_styles.py index 77a961a..55357ea 100644 --- a/tests/models/test_styles.py +++ b/tests/models/test_styles.py @@ -49,3 +49,18 @@ def test_styles_from_get_response_empty(empty_styles_get_response_payload): ) assert styles_instance.aslist() == [] + + +def test_styles_post_payload(): + styles = ["style1", "style2"] + + styles_instance = Styles(styles) + + assert styles_instance.post_payload() == { + "styles": { + "style": [ + {"name": "style1"}, + {"name": "style2"}, + ] + } + } diff --git a/tests/test_layer.py b/tests/test_layer.py new file mode 100644 index 0000000..8776c5f --- /dev/null +++ b/tests/test_layer.py @@ -0,0 +1,25 @@ +import responses + +from geoservercloud import GeoServerCloud + + +def test_set_default_layer_style(geoserver: GeoServerCloud) -> None: + workspace = "test_workspace" + layer = "test_layer" + style = "test_style" + with responses.RequestsMock() as rsps: + rsps.put( + url=f"{geoserver.url}/rest/layers/{workspace}:{layer}.json", + status=200, + body=b"", + match=[ + responses.matchers.json_params_matcher( + {"layer": {"name": layer, "defaultStyle": {"name": style}}} + ) + ], + ) + + content, code = geoserver.set_default_layer_style(layer, workspace, style) + + assert content == "" + assert code == 200 diff --git a/tests/test_style.py b/tests/test_style.py index 0051afa..a0ea46b 100644 --- a/tests/test_style.py +++ b/tests/test_style.py @@ -192,25 +192,3 @@ def test_create_style_unsupported_format(geoserver: GeoServerCloud) -> None: file="resources/style.txt", ) assert "Unsupported file extension" in str(excinfo.value) - - -def test_set_default_layer_style(geoserver: GeoServerCloud) -> None: - workspace = "test_workspace" - layer = "test_layer" - style = "test_style" - with responses.RequestsMock() as rsps: - rsps.put( - url=f"{GEOSERVER_URL}/rest/layers/{workspace}:{layer}.json", - status=200, - body=b"", - match=[ - responses.matchers.json_params_matcher( - {"layer": {"defaultStyle": {"name": style}}} - ) - ], - ) - - content, code = geoserver.set_default_layer_style(layer, workspace, style) - - assert content == "" - assert code == 200