geoservercloud 0.3.1.dev35__tar.gz → 0.3.1.dev37__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/PKG-INFO +1 -1
  2. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/geoservercloud.py +58 -3
  3. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/geoservercloudsync.py +36 -2
  4. geoservercloud-0.3.1.dev37/geoservercloud/models/layergroup.py +116 -0
  5. geoservercloud-0.3.1.dev37/geoservercloud/models/layergroups.py +21 -0
  6. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/services/restservice.py +31 -18
  7. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/templates.py +0 -48
  8. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/pyproject.toml +1 -1
  9. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/LICENSE +0 -0
  10. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/README.md +0 -0
  11. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/__init__.py +0 -0
  12. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/gridsets/2056.xml +0 -0
  13. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/gridsets/21781.xml +0 -0
  14. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/gridsets/3857.xml +0 -0
  15. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/models/__init__.py +0 -0
  16. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/models/common.py +0 -0
  17. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/models/datastore.py +0 -0
  18. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/models/datastores.py +0 -0
  19. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/models/featuretype.py +0 -0
  20. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/models/featuretypes.py +0 -0
  21. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/models/layer.py +0 -0
  22. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/models/resourcedirectory.py +0 -0
  23. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/models/style.py +0 -0
  24. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/models/styles.py +0 -0
  25. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/models/wmssettings.py +0 -0
  26. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/models/workspace.py +0 -0
  27. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/models/workspaces.py +0 -0
  28. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/services/__init__.py +0 -0
  29. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/services/owsservice.py +0 -0
  30. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/services/restclient.py +0 -0
  31. {geoservercloud-0.3.1.dev35 → geoservercloud-0.3.1.dev37}/geoservercloud/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geoservercloud
3
- Version: 0.3.1.dev35
3
+ Version: 0.3.1.dev37
4
4
  Summary: Lightweight Python client to interact with GeoServer Cloud REST API, GeoServer ACL and OGC services
5
5
  License: BSD-2-Clause
6
6
  Author: Camptocamp
@@ -10,6 +10,7 @@ from geoservercloud import utils
10
10
  from geoservercloud.models.datastore import PostGisDataStore
11
11
  from geoservercloud.models.featuretype import FeatureType
12
12
  from geoservercloud.models.layer import Layer
13
+ from geoservercloud.models.layergroup import LayerGroup
13
14
  from geoservercloud.models.style import Style
14
15
  from geoservercloud.models.wmssettings import WmsSettings
15
16
  from geoservercloud.models.workspace import Workspace
@@ -402,6 +403,30 @@ class GeoServerCloud:
402
403
  workspace_name, datastore_name, layer_name
403
404
  )
404
405
 
406
+ def get_layer_groups(
407
+ self, workspace_name: str
408
+ ) -> tuple[list[dict[str, str]] | str, int]:
409
+ """
410
+ Get all layer groups for a given workspace
411
+ """
412
+ layer_groups, status_code = self.rest_service.get_layer_groups(workspace_name)
413
+ if isinstance(layer_groups, str):
414
+ return layer_groups, status_code
415
+ return layer_groups.aslist(), status_code
416
+
417
+ def get_layer_group(
418
+ self, workspace_name: str, layer_group_name: str
419
+ ) -> tuple[dict[str, Any] | str, int]:
420
+ """
421
+ Get a layer group by name
422
+ """
423
+ layer_group, status_code = self.rest_service.get_layer_group(
424
+ workspace_name, layer_group_name
425
+ )
426
+ if isinstance(layer_group, str):
427
+ return layer_group, status_code
428
+ return layer_group.asdict(), status_code
429
+
405
430
  def create_layer_group(
406
431
  self,
407
432
  group: str,
@@ -411,16 +436,46 @@ class GeoServerCloud:
411
436
  abstract: str | dict,
412
437
  epsg: int = 4326,
413
438
  mode: str = "SINGLE",
439
+ enabled: bool = True,
440
+ advertised: bool = True,
414
441
  ) -> tuple[str, int]:
415
442
  """
416
- Create a layer group if it does not already exist.
443
+ Create a layer group or update it if it already exists.
417
444
  """
418
445
  workspace_name = workspace_name or self.default_workspace
419
446
  if not workspace_name:
420
447
  raise ValueError("Workspace not provided")
421
- return self.rest_service.create_layer_group(
422
- group, workspace_name, layers, title, abstract, epsg, mode
448
+ if not mode in LayerGroup.modes:
449
+ raise ValueError(
450
+ f"Invalid mode: {mode}, possible values are: {LayerGroup.modes}"
451
+ )
452
+ bounds = {
453
+ "minx": utils.EPSG_BBOX[epsg]["nativeBoundingBox"]["minx"],
454
+ "maxx": utils.EPSG_BBOX[epsg]["nativeBoundingBox"]["maxx"],
455
+ "miny": utils.EPSG_BBOX[epsg]["nativeBoundingBox"]["miny"],
456
+ "maxy": utils.EPSG_BBOX[epsg]["nativeBoundingBox"]["maxy"],
457
+ "crs": f"EPSG:{epsg}",
458
+ }
459
+ layer_group = LayerGroup(
460
+ name=group,
461
+ mode=mode,
462
+ workspace_name=workspace_name,
463
+ title=title,
464
+ abstract=abstract,
465
+ publishables=[f"{workspace_name}:{layer}" for layer in layers],
466
+ bounds=bounds,
467
+ enabled=enabled,
468
+ advertised=advertised,
423
469
  )
470
+ return self.rest_service.create_layer_group(group, workspace_name, layer_group)
471
+
472
+ def delete_layer_group(
473
+ self, workspace_name: str, layer_group_name: str
474
+ ) -> tuple[str, int]:
475
+ """
476
+ Delete a layer group
477
+ """
478
+ return self.rest_service.delete_layer_group(workspace_name, layer_group_name)
424
479
 
425
480
  def create_wmts_layer(
426
481
  self,
@@ -1,6 +1,5 @@
1
1
  from argparse import ArgumentParser
2
2
 
3
- from geoservercloud.models.resourcedirectory import ResourceDirectory
4
3
  from geoservercloud.services import RestService
5
4
 
6
5
 
@@ -50,7 +49,7 @@ class GeoServerCloudSync:
50
49
  """
51
50
  Copy a workspace from the source to the destination GeoServer instance.
52
51
  If deep_copy is True, the copy includes the PostGIS datastores, the feature types in the datastores,
53
- the corresponding layers and the styles in the workspace (including images).
52
+ the corresponding layers, the layer groups and the styles in the workspace (including images).
54
53
  """
55
54
  workspace, status_code = self.src_instance.get_workspace(workspace_name)
56
55
  if isinstance(workspace, str):
@@ -69,6 +68,9 @@ class GeoServerCloudSync:
69
68
  )
70
69
  if self.not_ok(status_code):
71
70
  return content, status_code
71
+ content, status_code = self.copy_layer_groups(workspace_name)
72
+ if self.not_ok(status_code):
73
+ return content, status_code
72
74
  return new_workspace, new_ws_status_code
73
75
 
74
76
  def copy_pg_datastores(
@@ -165,6 +167,38 @@ class GeoServerCloudSync:
165
167
  return layer, status_code
166
168
  return self.dst_instance.update_layer(layer, workspace_name)
167
169
 
170
+ def copy_layer_groups(self, workspace_name: str) -> tuple[str, int]:
171
+ """
172
+ Copy all layer groups in a workspace from source to destination GeoServer instance
173
+ """
174
+ layer_groups, status_code = self.src_instance.get_layer_groups(workspace_name)
175
+ if isinstance(layer_groups, str):
176
+ return layer_groups, status_code
177
+ elif layer_groups.aslist() == []:
178
+ return "", status_code
179
+ for layer_group in layer_groups.aslist():
180
+ content, status_code = self.copy_layer_group(
181
+ workspace_name, layer_group["name"]
182
+ )
183
+ if self.not_ok(status_code):
184
+ return content, status_code
185
+ return content, status_code
186
+
187
+ def copy_layer_group(
188
+ self, workspace_name: str, layer_group_name: str
189
+ ) -> tuple[str, int]:
190
+ """
191
+ Copy a layer group from source to destination GeoServer instance
192
+ """
193
+ layer_group, status_code = self.src_instance.get_layer_group(
194
+ workspace_name, layer_group_name
195
+ )
196
+ if isinstance(layer_group, str):
197
+ return layer_group, status_code
198
+ return self.dst_instance.create_layer_group(
199
+ layer_group_name, workspace_name, layer_group
200
+ )
201
+
168
202
  def copy_styles(
169
203
  self, workspace_name: str | None = None, include_images: bool = True
170
204
  ) -> tuple[str, int]:
@@ -0,0 +1,116 @@
1
+ from typing import Any
2
+
3
+ from geoservercloud.models.common import I18N, EntityModel, ReferencedObjectModel
4
+
5
+
6
+ class LayerGroup(EntityModel):
7
+ modes = ["SINGLE", "OPAQUE_CONTAINER", "NAMED", "CONTAINER", "EO"]
8
+
9
+ def __init__(
10
+ self,
11
+ name: str | None = None,
12
+ mode: str | None = None,
13
+ enabled: bool | None = None,
14
+ advertised: bool | None = None,
15
+ workspace_name: str | None = None,
16
+ title: dict[str, str] | str | None = None,
17
+ abstract: dict[str, str] | str | None = None,
18
+ publishables: list[str] | None = None,
19
+ styles: list[str] | None = None,
20
+ bounds: dict[str, Any] | None = None,
21
+ ):
22
+ self.name: str | None = name
23
+ self.mode: str | None = mode
24
+ self.enabled: bool | None = enabled
25
+ self.advertised: bool | None = advertised
26
+ self.workspace: ReferencedObjectModel | None = (
27
+ ReferencedObjectModel(workspace_name) if workspace_name else None
28
+ )
29
+ self.title: I18N | None = (
30
+ I18N(("title", "internationalTitle"), title) if title else None
31
+ )
32
+ self.abstract: I18N | None = (
33
+ I18N(("abstract", "internationalAbstract"), abstract) if abstract else None
34
+ )
35
+ self.publishables: list[ReferencedObjectModel] | None = (
36
+ [ReferencedObjectModel(publishable) for publishable in publishables]
37
+ if publishables
38
+ else None
39
+ )
40
+ self.styles: list[ReferencedObjectModel] | None = (
41
+ [ReferencedObjectModel(style) for style in styles] if styles else None
42
+ )
43
+ self.bounds: dict[str, int | str] | None = bounds
44
+
45
+ @property
46
+ def workspace_name(self) -> str | None:
47
+ return self.workspace.name if self.workspace else None
48
+
49
+ @classmethod
50
+ def from_get_response_payload(cls, content):
51
+ layer_group: dict[str, Any] = content["layerGroup"]
52
+ # publishables: list of dict or dict (if only one layer)
53
+ publishables: list[dict[str, str]] | dict[str, str] = layer_group[
54
+ "publishables"
55
+ ]["published"]
56
+ if isinstance(publishables, dict):
57
+ publishables = [publishables]
58
+ # style: list of dict, dict (if only one layer) or list of empty strings (if using default layer styles)
59
+ styles: list[dict] | dict | list[str] = layer_group["styles"]["style"]
60
+ if isinstance(styles, dict):
61
+ styles = [styles["name"]]
62
+ if isinstance(styles, list):
63
+ styles = [s["name"] if isinstance(s, dict) else s for s in styles]
64
+ return cls(
65
+ name=layer_group["name"],
66
+ mode=layer_group["mode"],
67
+ enabled=layer_group.get("enabled"),
68
+ advertised=layer_group.get("advertised"),
69
+ workspace_name=layer_group["workspace"]["name"],
70
+ title=layer_group.get("internationalTitle", layer_group.get("title")),
71
+ abstract=layer_group.get(
72
+ "internationalAbstract", layer_group.get("abstract")
73
+ ),
74
+ publishables=[p["name"] for p in publishables],
75
+ styles=styles,
76
+ bounds=layer_group.get("bounds"),
77
+ )
78
+
79
+ def asdict(self) -> dict[str, Any]:
80
+ optional_items = {
81
+ "name": self.name,
82
+ "mode": self.mode,
83
+ "enabled": self.enabled,
84
+ "advertised": self.advertised,
85
+ "bounds": self.bounds,
86
+ }
87
+ content = EntityModel.add_items_to_dict({}, optional_items)
88
+ if self.workspace:
89
+ content["workspace"] = self.workspace.asdict()
90
+ if self.publishables:
91
+ content["publishables"] = {
92
+ "published": [
93
+ {"@type": "layer", "name": p.name} for p in self.publishables
94
+ ]
95
+ }
96
+ if self.styles:
97
+ content["styles"] = {"style": [s.asdict() for s in self.styles]}
98
+ elif self.publishables:
99
+ content["styles"] = {"style": [{"name": ""}] * len(self.publishables)}
100
+ if self.title:
101
+ content.update(self.title.asdict())
102
+ if self.abstract:
103
+ content.update(self.abstract.asdict())
104
+ return content
105
+
106
+ def post_payload(self) -> dict[str, Any]:
107
+ return {"layerGroup": self.asdict()}
108
+
109
+ def put_payload(self) -> dict[str, Any]:
110
+ content = self.post_payload()
111
+ # Force a null value on non-i18ned attributes, otherwise GeoServer sets it to the first i18n value
112
+ if content["layerGroup"].get("internationalTitle"):
113
+ content["layerGroup"]["title"] = None
114
+ if content["layerGroup"].get("internationalAbstract"):
115
+ content["layerGroup"]["abstract"] = None
116
+ return content
@@ -0,0 +1,21 @@
1
+ import json
2
+
3
+ from geoservercloud.models.common import ListModel
4
+
5
+
6
+ class LayerGroups(ListModel):
7
+ def __init__(self, layergroups: list = []) -> None:
8
+ self._layergroups = layergroups
9
+
10
+ @classmethod
11
+ def from_get_response_payload(cls, content: dict):
12
+ feature_types: str | dict = content["layerGroups"]
13
+ if not feature_types:
14
+ return cls()
15
+ return cls(feature_types["layerGroup"]) # type: ignore
16
+
17
+ def aslist(self) -> list[dict[str, str]]:
18
+ return self._layergroups
19
+
20
+ def __repr__(self):
21
+ return json.dumps(self._layergroups, indent=4)
@@ -11,6 +11,8 @@ from geoservercloud.models.datastores import DataStores
11
11
  from geoservercloud.models.featuretype import FeatureType
12
12
  from geoservercloud.models.featuretypes import FeatureTypes
13
13
  from geoservercloud.models.layer import Layer
14
+ from geoservercloud.models.layergroup import LayerGroup
15
+ from geoservercloud.models.layergroups import LayerGroups
14
16
  from geoservercloud.models.resourcedirectory import ResourceDirectory
15
17
  from geoservercloud.models.style import Style
16
18
  from geoservercloud.models.styles import Styles
@@ -297,37 +299,48 @@ class RestService:
297
299
  )
298
300
  return response.content.decode(), response.status_code
299
301
 
302
+ def get_layer_groups(self, workspace_name: str) -> tuple[LayerGroups | str, int]:
303
+ response: Response = self.rest_client.get(
304
+ self.rest_endpoints.layergroups(workspace_name)
305
+ )
306
+ return self.deserialize_response(response, LayerGroups)
307
+
308
+ def get_layer_group(
309
+ self, workspace_name: str, layer_group_name: str
310
+ ) -> tuple[LayerGroup | str, int]:
311
+ response: Response = self.rest_client.get(
312
+ self.rest_endpoints.layergroup(workspace_name, layer_group_name)
313
+ )
314
+ return self.deserialize_response(response, LayerGroup)
315
+
300
316
  def create_layer_group(
301
317
  self,
302
- group: str,
318
+ layer_group_name: str,
303
319
  workspace_name: str,
304
- layers: list[str],
305
- title: str | dict,
306
- abstract: str | dict,
307
- epsg: int = 4326,
308
- mode: str = "SINGLE",
320
+ layer_group: LayerGroup,
309
321
  ) -> tuple[str, int]:
310
- payload: dict[str, dict[str, Any]] = Templates.layer_group(
311
- group=group,
312
- layers=layers,
313
- workspace=workspace_name,
314
- title=title,
315
- abstract=abstract,
316
- epsg=epsg,
317
- mode=mode,
318
- )
319
322
  if not self.resource_exists(
320
- self.rest_endpoints.layergroup(workspace_name, group)
323
+ self.rest_endpoints.layergroup(workspace_name, layer_group_name)
321
324
  ):
322
325
  response: Response = self.rest_client.post(
323
- self.rest_endpoints.layergroups(workspace_name), json=payload
326
+ self.rest_endpoints.layergroups(workspace_name),
327
+ json=layer_group.post_payload(),
324
328
  )
325
329
  else:
326
330
  response = self.rest_client.put(
327
- self.rest_endpoints.layergroup(workspace_name, group), json=payload
331
+ self.rest_endpoints.layergroup(workspace_name, layer_group_name),
332
+ json=layer_group.put_payload(),
328
333
  )
329
334
  return response.content.decode(), response.status_code
330
335
 
336
+ def delete_layer_group(
337
+ self, workspace_name: str, layer_group_name: str
338
+ ) -> tuple[str, int]:
339
+ response: Response = self.rest_client.delete(
340
+ self.rest_endpoints.layergroup(workspace_name, layer_group_name)
341
+ )
342
+ return response.content.decode(), response.status_code
343
+
331
344
  def get_styles(self, workspace_name: str | None = None) -> tuple[Styles | str, int]:
332
345
  path = self.rest_endpoints.styles(workspace_name=workspace_name)
333
346
  response: Response = self.rest_client.get(path)
@@ -28,54 +28,6 @@ class Templates:
28
28
  }
29
29
  }
30
30
 
31
- @staticmethod
32
- def layer_group(
33
- group: str,
34
- layers: list[str],
35
- workspace: str,
36
- title: str | dict[str, Any],
37
- abstract: str | dict[str, Any],
38
- epsg: int = 4326,
39
- mode: str = "SINGLE",
40
- ) -> dict[str, dict[str, Any]]:
41
- modes = ["SINGLE", "OPAQUE_CONTAINER", "NAMED", "CONTAINER", "EO"]
42
- if not mode in modes:
43
- raise ValueError(f"Invalid mode: {mode}, possible values are: {modes}")
44
- template = {
45
- "layerGroup": {
46
- "name": group,
47
- "workspace": {"name": workspace},
48
- "mode": mode,
49
- "publishables": {
50
- "published": [
51
- {"@type": "layer", "name": f"{workspace}:{layer}"}
52
- for layer in layers
53
- ]
54
- },
55
- "styles": {"style": [{"name": ""}] * len(layers)},
56
- "bounds": {
57
- "minx": EPSG_BBOX[epsg]["nativeBoundingBox"]["minx"],
58
- "maxx": EPSG_BBOX[epsg]["nativeBoundingBox"]["maxx"],
59
- "miny": EPSG_BBOX[epsg]["nativeBoundingBox"]["miny"],
60
- "maxy": EPSG_BBOX[epsg]["nativeBoundingBox"]["maxy"],
61
- "crs": f"EPSG:{epsg}",
62
- },
63
- "enabled": True,
64
- "advertised": True,
65
- }
66
- }
67
- if title:
68
- if type(title) is dict:
69
- template["layerGroup"]["internationalTitle"] = title
70
- else:
71
- template["layerGroup"]["title"] = title
72
- if abstract:
73
- if type(abstract) is dict:
74
- template["layerGroup"]["internationalAbstract"] = abstract
75
- else:
76
- template["layerGroup"]["abstract"] = abstract
77
- return template
78
-
79
31
  @staticmethod
80
32
  def wmts_layer(
81
33
  name: str,
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "geoservercloud"
3
- version = "0.3.1.dev35"
3
+ version = "0.3.1.dev37"
4
4
  description = "Lightweight Python client to interact with GeoServer Cloud REST API, GeoServer ACL and OGC services"
5
5
  authors = ["Camptocamp <info@camptocamp.com>"]
6
6
  license = "BSD-2-Clause"