geoservercloud 0.1.0.dev1__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.
- geoservercloud-0.1.0.dev1/LICENSE +22 -0
- geoservercloud-0.1.0.dev1/PKG-INFO +21 -0
- geoservercloud-0.1.0.dev1/README.md +1 -0
- geoservercloud-0.1.0.dev1/geoservercloud/__init__.py +3 -0
- geoservercloud-0.1.0.dev1/geoservercloud/geoservercloud.py +657 -0
- geoservercloud-0.1.0.dev1/geoservercloud/gridsets/3857.xml +92 -0
- geoservercloud-0.1.0.dev1/geoservercloud/restservice.py +81 -0
- geoservercloud-0.1.0.dev1/geoservercloud/templates.py +304 -0
- geoservercloud-0.1.0.dev1/geoservercloud/utils.py +36 -0
- geoservercloud-0.1.0.dev1/pyproject.toml +42 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Copyright (c) 2024, Camptocamp SA
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
|
11
|
+
and/or other materials provided with the distribution.
|
|
12
|
+
|
|
13
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
14
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
15
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
16
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
|
17
|
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
18
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
19
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
20
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
21
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
22
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: geoservercloud
|
|
3
|
+
Version: 0.1.0.dev1
|
|
4
|
+
Summary: Lightweight Python client to interact with GeoServer Cloud REST API, GeoServer ACL and OGC services
|
|
5
|
+
License: BSD-2-Clause
|
|
6
|
+
Author: Camptocamp
|
|
7
|
+
Author-email: info@camptocamp.com
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Dist: OWSLib
|
|
16
|
+
Requires-Dist: requests
|
|
17
|
+
Requires-Dist: xmltodict
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# python-geoservercloud
|
|
21
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# python-geoservercloud
|
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import xmltodict
|
|
6
|
+
from owslib.map.wms130 import WebMapService_1_3_0
|
|
7
|
+
from owslib.util import ResponseWrapper
|
|
8
|
+
from owslib.wmts import WebMapTileService
|
|
9
|
+
from requests import Response
|
|
10
|
+
|
|
11
|
+
from geoservercloud import utils
|
|
12
|
+
from geoservercloud.restservice import RestService
|
|
13
|
+
from geoservercloud.templates import Templates
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GeoServerCloud:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
url: str = "http://localhost:9090/geoserver/cloud/",
|
|
20
|
+
user: str = "admin",
|
|
21
|
+
password: str = "geoserver",
|
|
22
|
+
) -> None:
|
|
23
|
+
|
|
24
|
+
self.url: str = url
|
|
25
|
+
self.user: str = user
|
|
26
|
+
self.password: str = password
|
|
27
|
+
self.auth: tuple[str, str] = (user, password)
|
|
28
|
+
self.rest_service: RestService = RestService(url, self.auth)
|
|
29
|
+
self.wms: WebMapService_1_3_0 | None = None
|
|
30
|
+
self.wmts: WebMapTileService | None = None
|
|
31
|
+
self.default_workspace: str | None = None
|
|
32
|
+
self.default_datastore: str | None = None
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def workspace_wms_settings_path(workspace: str) -> str:
|
|
36
|
+
return f"/rest/services/wms/workspaces/{workspace}/settings.json"
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def get_wmts_layer_bbox(
|
|
40
|
+
url: str, layer_name: str
|
|
41
|
+
) -> tuple[float, float, float, float] | None:
|
|
42
|
+
wmts = WebMapTileService(url)
|
|
43
|
+
try:
|
|
44
|
+
return wmts[layer_name].boundingBoxWGS84
|
|
45
|
+
except (KeyError, AttributeError):
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def create_wms(self) -> None:
|
|
49
|
+
if self.default_workspace:
|
|
50
|
+
path: str = f"/{self.default_workspace}/wms"
|
|
51
|
+
else:
|
|
52
|
+
path = "/wms"
|
|
53
|
+
self.wms = WebMapService_1_3_0(
|
|
54
|
+
f"{self.url}{path}",
|
|
55
|
+
username=self.user,
|
|
56
|
+
password=self.password,
|
|
57
|
+
timeout=240,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def create_wmts(self) -> None:
|
|
61
|
+
path = "/gwc/service/wmts"
|
|
62
|
+
self.wmts = WebMapTileService(
|
|
63
|
+
f"{self.url}{path}",
|
|
64
|
+
version="1.0.0",
|
|
65
|
+
username=self.user,
|
|
66
|
+
password=self.password,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def create_workspace(
|
|
70
|
+
self,
|
|
71
|
+
workspace: str,
|
|
72
|
+
isolated: bool = False,
|
|
73
|
+
set_default_workspace: bool = False,
|
|
74
|
+
) -> Response:
|
|
75
|
+
"""
|
|
76
|
+
Create a workspace in GeoServer, if it does not already exist.
|
|
77
|
+
It if exists, update it
|
|
78
|
+
"""
|
|
79
|
+
payload: dict[str, dict[str, Any]] = {
|
|
80
|
+
"workspace": {
|
|
81
|
+
"name": workspace,
|
|
82
|
+
"isolated": isolated,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
response: Response = self.post_request("/rest/workspaces.json", json=payload)
|
|
86
|
+
if response.status_code == 409:
|
|
87
|
+
response = self.put_request(
|
|
88
|
+
f"/rest/workspaces/{workspace}.json", json=payload
|
|
89
|
+
)
|
|
90
|
+
if set_default_workspace:
|
|
91
|
+
self.default_workspace = workspace
|
|
92
|
+
return response
|
|
93
|
+
|
|
94
|
+
def delete_workspace(self, workspace: str) -> Response:
|
|
95
|
+
path: str = f"/rest/workspaces/{workspace}.json?recurse=true"
|
|
96
|
+
response: Response = self.delete_request(path)
|
|
97
|
+
if self.default_workspace == workspace:
|
|
98
|
+
self.default_workspace = None
|
|
99
|
+
self.wms = None
|
|
100
|
+
self.wmts = None
|
|
101
|
+
return response
|
|
102
|
+
|
|
103
|
+
def recreate_workspace(
|
|
104
|
+
self, workspace: str, set_default_workspace: bool = False
|
|
105
|
+
) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Create a workspace in GeoServer, and first delete it if it already exists.
|
|
108
|
+
"""
|
|
109
|
+
self.delete_workspace(workspace)
|
|
110
|
+
self.create_workspace(workspace, set_default_workspace=set_default_workspace)
|
|
111
|
+
|
|
112
|
+
def publish_workspace(self, workspace) -> Response:
|
|
113
|
+
path: str = f"{self.workspace_wms_settings_path(workspace)}"
|
|
114
|
+
|
|
115
|
+
data: dict[str, dict[str, Any]] = Templates.workspace_wms(workspace)
|
|
116
|
+
return self.put_request(path, json=data)
|
|
117
|
+
|
|
118
|
+
def set_default_locale_for_service(
|
|
119
|
+
self, workspace: str, locale: str | None
|
|
120
|
+
) -> None:
|
|
121
|
+
path: str = self.workspace_wms_settings_path(workspace)
|
|
122
|
+
data: dict[str, dict[str, Any]] = {
|
|
123
|
+
"wms": {
|
|
124
|
+
"defaultLocale": locale,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
self.put_request(path, json=data)
|
|
128
|
+
|
|
129
|
+
def unset_default_locale_for_service(self, workspace) -> None:
|
|
130
|
+
self.set_default_locale_for_service(workspace, None)
|
|
131
|
+
|
|
132
|
+
def create_pg_datastore(
|
|
133
|
+
self,
|
|
134
|
+
workspace: str,
|
|
135
|
+
datastore: str,
|
|
136
|
+
pg_host: str,
|
|
137
|
+
pg_port: int,
|
|
138
|
+
pg_db: str,
|
|
139
|
+
pg_user: str,
|
|
140
|
+
pg_password: str,
|
|
141
|
+
pg_schema: str = "public",
|
|
142
|
+
set_default_datastore: bool = False,
|
|
143
|
+
) -> Response | None:
|
|
144
|
+
"""
|
|
145
|
+
Create a PostGIS datastore from the DB connection parameters, or update it if it already exist.
|
|
146
|
+
"""
|
|
147
|
+
response: None | Response = None
|
|
148
|
+
path = f"/rest/workspaces/{workspace}/datastores.json"
|
|
149
|
+
resource_path = f"/rest/workspaces/{workspace}/datastores/{datastore}.json"
|
|
150
|
+
payload: dict[str, dict[str, Any]] = Templates.postgis_data_store(
|
|
151
|
+
datastore=datastore,
|
|
152
|
+
pg_host=pg_host,
|
|
153
|
+
pg_port=pg_port,
|
|
154
|
+
pg_db=pg_db,
|
|
155
|
+
pg_user=pg_user,
|
|
156
|
+
pg_password=pg_password,
|
|
157
|
+
namespace=f"http://{workspace}",
|
|
158
|
+
pg_schema=pg_schema,
|
|
159
|
+
)
|
|
160
|
+
if not self.resource_exists(resource_path):
|
|
161
|
+
response = self.post_request(path, json=payload)
|
|
162
|
+
else:
|
|
163
|
+
response = self.put_request(resource_path, json=payload)
|
|
164
|
+
|
|
165
|
+
if set_default_datastore:
|
|
166
|
+
self.default_datastore = datastore
|
|
167
|
+
|
|
168
|
+
return response
|
|
169
|
+
|
|
170
|
+
def create_jndi_datastore(
|
|
171
|
+
self,
|
|
172
|
+
workspace: str,
|
|
173
|
+
datastore: str,
|
|
174
|
+
jndi_reference: str,
|
|
175
|
+
pg_schema: str = "public",
|
|
176
|
+
description: str | None = None,
|
|
177
|
+
set_default_datastore: bool = False,
|
|
178
|
+
) -> Response | None:
|
|
179
|
+
"""
|
|
180
|
+
Create a PostGIS datastore from JNDI resource, or update it if it already exist.
|
|
181
|
+
"""
|
|
182
|
+
response: None | Response = None
|
|
183
|
+
path = f"/rest/workspaces/{workspace}/datastores.json"
|
|
184
|
+
resource_path = f"/rest/workspaces/{workspace}/datastores/{datastore}.json"
|
|
185
|
+
payload: dict[str, dict[str, Any]] = Templates.postgis_jndi_data_store(
|
|
186
|
+
datastore=datastore,
|
|
187
|
+
jndi_reference=jndi_reference,
|
|
188
|
+
namespace=f"http://{workspace}",
|
|
189
|
+
pg_schema=pg_schema,
|
|
190
|
+
description=description,
|
|
191
|
+
)
|
|
192
|
+
if not self.resource_exists(resource_path):
|
|
193
|
+
response = self.post_request(path, json=payload)
|
|
194
|
+
else:
|
|
195
|
+
response = self.put_request(resource_path, json=payload)
|
|
196
|
+
|
|
197
|
+
if set_default_datastore:
|
|
198
|
+
self.default_datastore = datastore
|
|
199
|
+
|
|
200
|
+
return response
|
|
201
|
+
|
|
202
|
+
def create_wmts_store(
|
|
203
|
+
self,
|
|
204
|
+
workspace: str,
|
|
205
|
+
name: str,
|
|
206
|
+
capabilities: str,
|
|
207
|
+
) -> Response | None:
|
|
208
|
+
"""
|
|
209
|
+
Create a cascaded WMTS store, or update it if it already exist.
|
|
210
|
+
"""
|
|
211
|
+
response: None | Response = None
|
|
212
|
+
path = f"/rest/workspaces/{workspace}/wmtsstores.json"
|
|
213
|
+
resource_path = f"/rest/workspaces/{workspace}/wmtsstores/{name}.json"
|
|
214
|
+
payload = Templates.wmts_store(workspace, name, capabilities)
|
|
215
|
+
if not self.resource_exists(resource_path):
|
|
216
|
+
return self.post_request(path, json=payload)
|
|
217
|
+
else:
|
|
218
|
+
return self.put_request(resource_path, json=payload)
|
|
219
|
+
|
|
220
|
+
def create_feature_type(
|
|
221
|
+
self,
|
|
222
|
+
layer: str,
|
|
223
|
+
workspace: str | None = None,
|
|
224
|
+
datastore: str | None = None,
|
|
225
|
+
title: str | dict = "Default title",
|
|
226
|
+
abstract: str | dict = "Default abstract",
|
|
227
|
+
attributes: dict = Templates.geom_point_attribute(),
|
|
228
|
+
epsg: int = 4326,
|
|
229
|
+
) -> Response | None:
|
|
230
|
+
"""
|
|
231
|
+
Create a feature type or update it if it already exist.
|
|
232
|
+
"""
|
|
233
|
+
workspace = workspace or self.default_workspace
|
|
234
|
+
if not workspace:
|
|
235
|
+
raise ValueError("Workspace not provided")
|
|
236
|
+
datastore = datastore or self.default_datastore
|
|
237
|
+
if not datastore:
|
|
238
|
+
raise ValueError("Datastore not provided")
|
|
239
|
+
path: str = (
|
|
240
|
+
f"/rest/workspaces/{workspace}/datastores/{datastore}/featuretypes.json"
|
|
241
|
+
)
|
|
242
|
+
resource_path: str = (
|
|
243
|
+
f"/rest/workspaces/{workspace}/datastores/{datastore}/featuretypes/{layer}.json"
|
|
244
|
+
)
|
|
245
|
+
payload: dict[str, dict[str, Any]] = Templates.feature_type(
|
|
246
|
+
layer=layer,
|
|
247
|
+
workspace=workspace,
|
|
248
|
+
datastore=datastore,
|
|
249
|
+
attributes=utils.convert_attributes(attributes),
|
|
250
|
+
epsg=epsg,
|
|
251
|
+
)
|
|
252
|
+
if type(title) is dict:
|
|
253
|
+
payload["featureType"]["internationalTitle"] = title
|
|
254
|
+
else:
|
|
255
|
+
payload["featureType"]["title"] = title
|
|
256
|
+
if type(abstract) is dict:
|
|
257
|
+
payload["featureType"]["internationalAbstract"] = abstract
|
|
258
|
+
else:
|
|
259
|
+
payload["featureType"]["abstract"] = abstract
|
|
260
|
+
|
|
261
|
+
if not self.resource_exists(resource_path):
|
|
262
|
+
return self.post_request(path, json=payload)
|
|
263
|
+
else:
|
|
264
|
+
return self.put_request(resource_path, json=payload)
|
|
265
|
+
|
|
266
|
+
def create_layer_group(
|
|
267
|
+
self,
|
|
268
|
+
group: str,
|
|
269
|
+
workspace: str | None,
|
|
270
|
+
layers: list[str],
|
|
271
|
+
title: str | dict,
|
|
272
|
+
abstract: str | dict,
|
|
273
|
+
epsg: int = 4326,
|
|
274
|
+
) -> Response | None:
|
|
275
|
+
"""
|
|
276
|
+
Create a layer group if it does not already exist.
|
|
277
|
+
"""
|
|
278
|
+
workspace = workspace or self.default_workspace
|
|
279
|
+
if not workspace:
|
|
280
|
+
raise ValueError("Workspace not provided")
|
|
281
|
+
path: str = f"/rest/workspaces/{workspace}/layergroups.json"
|
|
282
|
+
resource_path: str = f"/rest/workspaces/{workspace}/layergroups/{group}.json"
|
|
283
|
+
payload: dict[str, dict[str, Any]] = Templates.layer_group(
|
|
284
|
+
group=group,
|
|
285
|
+
layers=layers,
|
|
286
|
+
workspace=workspace,
|
|
287
|
+
title=title,
|
|
288
|
+
abstract=abstract,
|
|
289
|
+
epsg=epsg,
|
|
290
|
+
)
|
|
291
|
+
if not self.resource_exists(resource_path):
|
|
292
|
+
return self.post_request(path, json=payload)
|
|
293
|
+
else:
|
|
294
|
+
return self.put_request(resource_path, json=payload)
|
|
295
|
+
|
|
296
|
+
def create_wmts_layer(
|
|
297
|
+
self,
|
|
298
|
+
workspace: str,
|
|
299
|
+
wmts_store: str,
|
|
300
|
+
native_layer: str,
|
|
301
|
+
published_layer: str | None = None,
|
|
302
|
+
epsg: int = 4326,
|
|
303
|
+
) -> Response | None:
|
|
304
|
+
"""
|
|
305
|
+
Publish a remote WMTS layer if it does not already exist.
|
|
306
|
+
"""
|
|
307
|
+
if not published_layer:
|
|
308
|
+
published_layer = native_layer
|
|
309
|
+
if self.resource_exists(
|
|
310
|
+
f"/rest/workspaces/{workspace}/wmtsstores/{wmts_store}/layers/{published_layer}.json"
|
|
311
|
+
):
|
|
312
|
+
return None
|
|
313
|
+
wmts_store_path = f"/rest/workspaces/{workspace}/wmtsstores/{wmts_store}.json"
|
|
314
|
+
capabilities_url = (
|
|
315
|
+
self.get_request(wmts_store_path)
|
|
316
|
+
.json()
|
|
317
|
+
.get("wmtsStore")
|
|
318
|
+
.get("capabilitiesURL")
|
|
319
|
+
)
|
|
320
|
+
wgs84_bbox = self.get_wmts_layer_bbox(capabilities_url, native_layer)
|
|
321
|
+
|
|
322
|
+
path = f"/rest/workspaces/{workspace}/wmtsstores/{wmts_store}/layers"
|
|
323
|
+
payload = Templates.wmts_layer(
|
|
324
|
+
published_layer, native_layer, wgs84_bbox=wgs84_bbox, epsg=epsg
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
return self.post_request(path, json=payload)
|
|
328
|
+
|
|
329
|
+
def publish_gwc_layer(
|
|
330
|
+
self, workspace: str, layer: str, epsg: int = 4326
|
|
331
|
+
) -> Response | None:
|
|
332
|
+
# Reload config to make sure GWC is aware of GeoServer layers
|
|
333
|
+
self.post_request(
|
|
334
|
+
"/gwc/rest/reload",
|
|
335
|
+
headers={"Content-Type": "application/json"},
|
|
336
|
+
data="reload_configuration=1",
|
|
337
|
+
)
|
|
338
|
+
payload = Templates.gwc_layer(workspace, layer, f"EPSG:{epsg}")
|
|
339
|
+
return self.put_request(
|
|
340
|
+
f"/gwc/rest/layers/{workspace}:{layer}.json",
|
|
341
|
+
json=payload,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def create_style_from_file(
|
|
345
|
+
self,
|
|
346
|
+
style: str,
|
|
347
|
+
file: str,
|
|
348
|
+
workspace: str | None = None,
|
|
349
|
+
) -> Response:
|
|
350
|
+
"""Create a style from a file, or update it if it already exists.
|
|
351
|
+
Supported file extensions are .sld and .zip."""
|
|
352
|
+
path = (
|
|
353
|
+
"/rest/styles" if not workspace else f"/rest/workspaces/{workspace}/styles"
|
|
354
|
+
)
|
|
355
|
+
resource_path = (
|
|
356
|
+
f"/rest/styles/{style}.json"
|
|
357
|
+
if not workspace
|
|
358
|
+
else f"/rest/workspaces/{workspace}/styles/{style}.json"
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
file_ext = os.path.splitext(file)[1]
|
|
362
|
+
if file_ext == ".sld":
|
|
363
|
+
content_type = "application/vnd.ogc.sld+xml"
|
|
364
|
+
elif file_ext == ".zip":
|
|
365
|
+
content_type = "application/zip"
|
|
366
|
+
else:
|
|
367
|
+
raise ValueError(f"Unsupported file extension: {file_ext}")
|
|
368
|
+
with open(f"{file}", "rb") as fs:
|
|
369
|
+
data: bytes = fs.read()
|
|
370
|
+
headers: dict[str, str] = {"Content-Type": content_type}
|
|
371
|
+
|
|
372
|
+
if not self.resource_exists(resource_path):
|
|
373
|
+
return self.post_request(path, data=data, headers=headers)
|
|
374
|
+
else:
|
|
375
|
+
return self.put_request(resource_path, data=data, headers=headers)
|
|
376
|
+
|
|
377
|
+
def set_default_layer_style(
|
|
378
|
+
self, layer: str, workspace: str, style: str
|
|
379
|
+
) -> Response:
|
|
380
|
+
path = f"/rest/layers/{workspace}:{layer}.json"
|
|
381
|
+
data = {"layer": {"defaultStyle": {"name": style}}}
|
|
382
|
+
return self.put_request(path, json=data)
|
|
383
|
+
|
|
384
|
+
def get_wms_capabilities(
|
|
385
|
+
self, workspace: str, accept_languages=None
|
|
386
|
+
) -> dict[str, Any]:
|
|
387
|
+
path: str = f"/{workspace}/wms"
|
|
388
|
+
params: dict[str, str] = {
|
|
389
|
+
"service": "WMS",
|
|
390
|
+
"version": "1.3.0",
|
|
391
|
+
"request": "GetCapabilities",
|
|
392
|
+
}
|
|
393
|
+
if accept_languages:
|
|
394
|
+
params["AcceptLanguages"] = accept_languages
|
|
395
|
+
response: Response = self.get_request(path, params=params)
|
|
396
|
+
return xmltodict.parse(response.content)
|
|
397
|
+
|
|
398
|
+
def get_wms_layers(
|
|
399
|
+
self, workspace: str, accept_languages: str | None = None
|
|
400
|
+
) -> Any | dict[str, Any]:
|
|
401
|
+
capabilities: dict[str, Any] = self.get_wms_capabilities(
|
|
402
|
+
workspace, accept_languages
|
|
403
|
+
)
|
|
404
|
+
try:
|
|
405
|
+
return capabilities["WMS_Capabilities"]["Capability"]["Layer"]
|
|
406
|
+
except KeyError:
|
|
407
|
+
return capabilities
|
|
408
|
+
|
|
409
|
+
def get_wfs_capabilities(self, workspace: str) -> dict[str, Any]:
|
|
410
|
+
path: str = f"/{workspace}/wfs"
|
|
411
|
+
params: dict[str, str] = {
|
|
412
|
+
"service": "WFS",
|
|
413
|
+
"version": "1.1.0",
|
|
414
|
+
"request": "GetCapabilities",
|
|
415
|
+
}
|
|
416
|
+
response: Response = self.get_request(path, params=params)
|
|
417
|
+
return xmltodict.parse(response.content)
|
|
418
|
+
|
|
419
|
+
def get_wfs_layers(self, workspace: str) -> Any | dict[str, Any]:
|
|
420
|
+
capabilities: dict[str, Any] = self.get_wfs_capabilities(workspace)
|
|
421
|
+
try:
|
|
422
|
+
return capabilities["wfs:WFS_Capabilities"]["FeatureTypeList"]
|
|
423
|
+
except KeyError:
|
|
424
|
+
return capabilities
|
|
425
|
+
|
|
426
|
+
def get_map(
|
|
427
|
+
self,
|
|
428
|
+
layers: list[str],
|
|
429
|
+
bbox: tuple[float, float, float, float],
|
|
430
|
+
size: tuple[int, int],
|
|
431
|
+
srs: str = "EPSG:2056",
|
|
432
|
+
format: str = "image/png",
|
|
433
|
+
transparent: bool = True,
|
|
434
|
+
styles: list[str] | None = None,
|
|
435
|
+
language: str | None = None,
|
|
436
|
+
) -> ResponseWrapper | None:
|
|
437
|
+
if not self.wms:
|
|
438
|
+
self.create_wms()
|
|
439
|
+
params: dict[str, Any] = {
|
|
440
|
+
"layers": layers,
|
|
441
|
+
"srs": srs,
|
|
442
|
+
"bbox": bbox,
|
|
443
|
+
"size": size,
|
|
444
|
+
"format": format,
|
|
445
|
+
"transparent": transparent,
|
|
446
|
+
"styles": styles,
|
|
447
|
+
}
|
|
448
|
+
if language is not None:
|
|
449
|
+
params["language"] = language
|
|
450
|
+
if self.wms:
|
|
451
|
+
return self.wms.getmap(
|
|
452
|
+
**params,
|
|
453
|
+
timeout=120,
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
def get_feature_info(
|
|
457
|
+
self,
|
|
458
|
+
layers: list[str],
|
|
459
|
+
bbox: tuple[float, float, float, float],
|
|
460
|
+
size: tuple[int, int],
|
|
461
|
+
srs: str = "EPSG:2056",
|
|
462
|
+
info_format: str = "application/json",
|
|
463
|
+
transparent: bool = True,
|
|
464
|
+
styles: list[str] | None = None,
|
|
465
|
+
xy: list[float] = [0, 0],
|
|
466
|
+
) -> ResponseWrapper | None:
|
|
467
|
+
if not self.wms:
|
|
468
|
+
self.create_wms()
|
|
469
|
+
params = {
|
|
470
|
+
"layers": layers,
|
|
471
|
+
"srs": srs,
|
|
472
|
+
"bbox": bbox,
|
|
473
|
+
"size": size,
|
|
474
|
+
"info_format": info_format,
|
|
475
|
+
"transparent": transparent,
|
|
476
|
+
"styles": styles,
|
|
477
|
+
"xy": xy,
|
|
478
|
+
}
|
|
479
|
+
if self.wms:
|
|
480
|
+
return self.wms.getfeatureinfo(
|
|
481
|
+
**params,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
def get_legend_graphic(
|
|
485
|
+
self,
|
|
486
|
+
layer: list[str],
|
|
487
|
+
format: str = "image/png",
|
|
488
|
+
language: str | None = None,
|
|
489
|
+
style: str | None = None,
|
|
490
|
+
workspace: str | None = None,
|
|
491
|
+
) -> Response:
|
|
492
|
+
if not workspace:
|
|
493
|
+
path = "/wms"
|
|
494
|
+
else:
|
|
495
|
+
path: str = f"/{workspace}/wms"
|
|
496
|
+
params: dict[str, Any] = {
|
|
497
|
+
"service": "WMS",
|
|
498
|
+
"version": "1.3.0",
|
|
499
|
+
"request": "GetLegendGraphic",
|
|
500
|
+
"format": format,
|
|
501
|
+
"layer": layer,
|
|
502
|
+
}
|
|
503
|
+
if language:
|
|
504
|
+
params["language"] = language
|
|
505
|
+
if style:
|
|
506
|
+
params["style"] = style
|
|
507
|
+
response: Response = self.get_request(path, params=params)
|
|
508
|
+
return response
|
|
509
|
+
|
|
510
|
+
def get_tile(
|
|
511
|
+
self, layer, format, tile_matrix_set, tile_matrix, row, column
|
|
512
|
+
) -> ResponseWrapper | None:
|
|
513
|
+
if not self.wmts:
|
|
514
|
+
self.create_wmts()
|
|
515
|
+
if self.wmts:
|
|
516
|
+
return self.wmts.gettile(
|
|
517
|
+
layer=layer,
|
|
518
|
+
format=format,
|
|
519
|
+
tilematrixset=tile_matrix_set,
|
|
520
|
+
tilematrix=tile_matrix,
|
|
521
|
+
row=row,
|
|
522
|
+
column=column,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
def create_role(self, role_name: str):
|
|
526
|
+
self.post_request(f"/rest/security/roles/role/{role_name}")
|
|
527
|
+
|
|
528
|
+
def delete_role(self, role_name: str):
|
|
529
|
+
self.delete_request(f"/rest/security/roles/role/{role_name}")
|
|
530
|
+
|
|
531
|
+
def create_role_if_not_exists(self, role_name: str) -> Response | None:
|
|
532
|
+
if self.role_exists(role_name):
|
|
533
|
+
return None
|
|
534
|
+
return self.create_role(role_name)
|
|
535
|
+
|
|
536
|
+
def role_exists(self, role_name: str) -> bool:
|
|
537
|
+
response = self.get_request(
|
|
538
|
+
f"/rest/security/roles", headers={"Accept": "application/json"}
|
|
539
|
+
)
|
|
540
|
+
roles = response.json().get("roles", [])
|
|
541
|
+
return role_name in roles
|
|
542
|
+
|
|
543
|
+
def create_acl_admin_rule(
|
|
544
|
+
self,
|
|
545
|
+
priority: int = 0,
|
|
546
|
+
access: str = "ADMIN",
|
|
547
|
+
role: str | None = None,
|
|
548
|
+
user: str | None = None,
|
|
549
|
+
workspace: str | None = None,
|
|
550
|
+
) -> Response:
|
|
551
|
+
path = "/acl/api/adminrules"
|
|
552
|
+
return self.post_request(
|
|
553
|
+
path,
|
|
554
|
+
json={
|
|
555
|
+
"priority": priority,
|
|
556
|
+
"access": access,
|
|
557
|
+
"role": role,
|
|
558
|
+
"user": user,
|
|
559
|
+
"workspace": workspace,
|
|
560
|
+
},
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
def delete_acl_admin_rule(self, id: int) -> Response:
|
|
564
|
+
path = f"/acl/api/adminrules/id/{id}"
|
|
565
|
+
return self.delete_request(path)
|
|
566
|
+
|
|
567
|
+
def delete_all_acl_admin_rules(self) -> Response:
|
|
568
|
+
path = "/acl/api/adminrules"
|
|
569
|
+
return self.delete_request(path)
|
|
570
|
+
|
|
571
|
+
def create_acl_rule(
|
|
572
|
+
self,
|
|
573
|
+
priority: int = 0,
|
|
574
|
+
access: str = "DENY",
|
|
575
|
+
role: str | None = None,
|
|
576
|
+
service: str | None = None,
|
|
577
|
+
workspace: str | None = None,
|
|
578
|
+
) -> Response:
|
|
579
|
+
path = "/acl/api/rules"
|
|
580
|
+
return self.post_request(
|
|
581
|
+
path,
|
|
582
|
+
json={
|
|
583
|
+
"priority": priority,
|
|
584
|
+
"access": access,
|
|
585
|
+
"role": role,
|
|
586
|
+
"service": service,
|
|
587
|
+
"workspace": workspace,
|
|
588
|
+
},
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
def delete_all_acl_rules(self) -> Response:
|
|
592
|
+
path = "/acl/api/rules"
|
|
593
|
+
return self.delete_request(path)
|
|
594
|
+
|
|
595
|
+
def create_or_update_resource(self, path, resource_path, payload) -> Response:
|
|
596
|
+
if not self.resource_exists(resource_path):
|
|
597
|
+
return self.post_request(path, json=payload)
|
|
598
|
+
else:
|
|
599
|
+
return self.put_request(resource_path, json=payload)
|
|
600
|
+
|
|
601
|
+
def create_gridset(self, epsg: int) -> Response | None:
|
|
602
|
+
resource_path: str = f"/gwc/rest/gridsets/EPSG:{epsg}.xml"
|
|
603
|
+
if self.resource_exists(resource_path):
|
|
604
|
+
return None
|
|
605
|
+
file_path: Path = Path(__file__).parent / "gridsets" / f"{epsg}.xml"
|
|
606
|
+
headers: dict[str, str] = {"Content-Type": "application/xml"}
|
|
607
|
+
try:
|
|
608
|
+
data: bytes = file_path.read_bytes()
|
|
609
|
+
except FileNotFoundError:
|
|
610
|
+
raise ValueError(f"No gridset definition found for EPSG:{epsg}")
|
|
611
|
+
return self.put_request(resource_path, data=data, headers=headers)
|
|
612
|
+
|
|
613
|
+
def resource_exists(self, path: str) -> bool:
|
|
614
|
+
# GeoServer raises a 500 when posting to a datastore or feature type that already exists, so first do
|
|
615
|
+
# a get request
|
|
616
|
+
response = self.get_request(path)
|
|
617
|
+
return response.status_code == 200
|
|
618
|
+
|
|
619
|
+
def get_request(
|
|
620
|
+
self,
|
|
621
|
+
path,
|
|
622
|
+
params: dict[str, str] | None = None,
|
|
623
|
+
headers: dict[str, str] | None = None,
|
|
624
|
+
) -> Response:
|
|
625
|
+
return self.rest_service.get(path, params=params, headers=headers)
|
|
626
|
+
|
|
627
|
+
def post_request(
|
|
628
|
+
self,
|
|
629
|
+
path: str,
|
|
630
|
+
params: dict[str, str] | None = None,
|
|
631
|
+
headers: dict[str, str] | None = None,
|
|
632
|
+
json: dict[str, Any] | None = None,
|
|
633
|
+
data: bytes | None = None,
|
|
634
|
+
) -> Response:
|
|
635
|
+
return self.rest_service.post(
|
|
636
|
+
path, params=params, headers=headers, json=json, data=data
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
def put_request(
|
|
640
|
+
self,
|
|
641
|
+
path: str,
|
|
642
|
+
params: dict[str, str] | None = None,
|
|
643
|
+
headers: dict[str, str] | None = None,
|
|
644
|
+
json: dict[str, dict[str, Any]] | None = None,
|
|
645
|
+
data: bytes | None = None,
|
|
646
|
+
) -> Response:
|
|
647
|
+
return self.rest_service.put(
|
|
648
|
+
path, params=params, headers=headers, json=json, data=data
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
def delete_request(
|
|
652
|
+
self,
|
|
653
|
+
path: str,
|
|
654
|
+
params: dict[str, str] | None = None,
|
|
655
|
+
headers: dict[str, str] | None = None,
|
|
656
|
+
) -> Response:
|
|
657
|
+
return self.rest_service.delete(path, params=params, headers=headers)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<gridSet>
|
|
3
|
+
<name>EPSG:3857</name>
|
|
4
|
+
<description>This well-known scale set has been defined to be compatible with Google Maps and
|
|
5
|
+
Microsoft Live Map projections and zoom levels. Level 0 allows representing the whole world
|
|
6
|
+
in a single 256x256 pixels. The next level represents the whole world in 2x2 tiles of
|
|
7
|
+
256x256 pixels and so on in powers of 2. Scale denominator is only accurate near the
|
|
8
|
+
equator.</description>
|
|
9
|
+
<srs>
|
|
10
|
+
<number>3857</number>
|
|
11
|
+
</srs>
|
|
12
|
+
<extent>
|
|
13
|
+
<coords>
|
|
14
|
+
<double>-2.003750834E7</double>
|
|
15
|
+
<double>-2.003750834E7</double>
|
|
16
|
+
<double>2.003750834E7</double>
|
|
17
|
+
<double>2.003750834E7</double>
|
|
18
|
+
</coords>
|
|
19
|
+
</extent>
|
|
20
|
+
<alignTopLeft>false</alignTopLeft>
|
|
21
|
+
<resolutions>
|
|
22
|
+
<double>156543.03390625</double>
|
|
23
|
+
<double>78271.516953125</double>
|
|
24
|
+
<double>39135.7584765625</double>
|
|
25
|
+
<double>19567.87923828125</double>
|
|
26
|
+
<double>9783.939619140625</double>
|
|
27
|
+
<double>4891.9698095703125</double>
|
|
28
|
+
<double>2445.9849047851562</double>
|
|
29
|
+
<double>1222.9924523925781</double>
|
|
30
|
+
<double>611.4962261962891</double>
|
|
31
|
+
<double>305.74811309814453</double>
|
|
32
|
+
<double>152.87405654907226</double>
|
|
33
|
+
<double>76.43702827453613</double>
|
|
34
|
+
<double>38.218514137268066</double>
|
|
35
|
+
<double>19.109257068634033</double>
|
|
36
|
+
<double>9.554628534317017</double>
|
|
37
|
+
<double>4.777314267158508</double>
|
|
38
|
+
<double>2.388657133579254</double>
|
|
39
|
+
<double>1.194328566789627</double>
|
|
40
|
+
<double>0.5971642833948135</double>
|
|
41
|
+
<double>0.29858214169740677</double>
|
|
42
|
+
<double>0.14929107084870338</double>
|
|
43
|
+
<double>0.07464553542435169</double>
|
|
44
|
+
<double>0.037322767712175846</double>
|
|
45
|
+
<double>0.018661383856087923</double>
|
|
46
|
+
<double>0.009330691928043961</double>
|
|
47
|
+
<double>0.004665345964021981</double>
|
|
48
|
+
<double>0.0023326729820109904</double>
|
|
49
|
+
<double>0.0011663364910054952</double>
|
|
50
|
+
<double>5.831682455027476E-4</double>
|
|
51
|
+
<double>2.915841227513738E-4</double>
|
|
52
|
+
<double>1.457920613756869E-4</double>
|
|
53
|
+
</resolutions>
|
|
54
|
+
<metersPerUnit>1.0</metersPerUnit>
|
|
55
|
+
<pixelSize>2.8E-4</pixelSize>
|
|
56
|
+
<scaleNames>
|
|
57
|
+
<string>EPSG:3857:0</string>
|
|
58
|
+
<string>EPSG:3857:1</string>
|
|
59
|
+
<string>EPSG:3857:2</string>
|
|
60
|
+
<string>EPSG:3857:3</string>
|
|
61
|
+
<string>EPSG:3857:4</string>
|
|
62
|
+
<string>EPSG:3857:5</string>
|
|
63
|
+
<string>EPSG:3857:6</string>
|
|
64
|
+
<string>EPSG:3857:7</string>
|
|
65
|
+
<string>EPSG:3857:8</string>
|
|
66
|
+
<string>EPSG:3857:9</string>
|
|
67
|
+
<string>EPSG:3857:10</string>
|
|
68
|
+
<string>EPSG:3857:11</string>
|
|
69
|
+
<string>EPSG:3857:12</string>
|
|
70
|
+
<string>EPSG:3857:13</string>
|
|
71
|
+
<string>EPSG:3857:14</string>
|
|
72
|
+
<string>EPSG:3857:15</string>
|
|
73
|
+
<string>EPSG:3857:16</string>
|
|
74
|
+
<string>EPSG:3857:17</string>
|
|
75
|
+
<string>EPSG:3857:18</string>
|
|
76
|
+
<string>EPSG:3857:19</string>
|
|
77
|
+
<string>EPSG:3857:20</string>
|
|
78
|
+
<string>EPSG:3857:21</string>
|
|
79
|
+
<string>EPSG:3857:22</string>
|
|
80
|
+
<string>EPSG:3857:23</string>
|
|
81
|
+
<string>EPSG:3857:24</string>
|
|
82
|
+
<string>EPSG:3857:25</string>
|
|
83
|
+
<string>EPSG:3857:26</string>
|
|
84
|
+
<string>EPSG:3857:27</string>
|
|
85
|
+
<string>EPSG:3857:28</string>
|
|
86
|
+
<string>EPSG:3857:29</string>
|
|
87
|
+
<string>EPSG:3857:30</string>
|
|
88
|
+
</scaleNames>
|
|
89
|
+
<tileHeight>256</tileHeight>
|
|
90
|
+
<tileWidth>256</tileWidth>
|
|
91
|
+
<yCoordinateFirst>false</yCoordinateFirst>
|
|
92
|
+
</gridSet>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RestService:
|
|
7
|
+
def __init__(self, url: str, auth: tuple[str, str]) -> None:
|
|
8
|
+
self.url: str = url
|
|
9
|
+
self.auth: tuple[str, str] = auth
|
|
10
|
+
|
|
11
|
+
def get(
|
|
12
|
+
self,
|
|
13
|
+
path: str,
|
|
14
|
+
params: dict[str, str] | None = None,
|
|
15
|
+
headers: dict[str, str] | None = None,
|
|
16
|
+
) -> requests.Response:
|
|
17
|
+
response: requests.Response = requests.get(
|
|
18
|
+
f"{self.url}{path}",
|
|
19
|
+
params=params,
|
|
20
|
+
headers=headers,
|
|
21
|
+
auth=self.auth,
|
|
22
|
+
)
|
|
23
|
+
if response.status_code != 404:
|
|
24
|
+
response.raise_for_status()
|
|
25
|
+
return response
|
|
26
|
+
|
|
27
|
+
def post(
|
|
28
|
+
self,
|
|
29
|
+
path: str,
|
|
30
|
+
params: dict[str, str] | None = None,
|
|
31
|
+
headers: dict[str, str] | None = None,
|
|
32
|
+
json: dict[str, dict[str, Any]] | None = None,
|
|
33
|
+
data: bytes | None = None,
|
|
34
|
+
) -> requests.Response:
|
|
35
|
+
|
|
36
|
+
response: requests.Response = requests.post(
|
|
37
|
+
f"{self.url}{path}",
|
|
38
|
+
params=params,
|
|
39
|
+
headers=headers,
|
|
40
|
+
json=json,
|
|
41
|
+
data=data,
|
|
42
|
+
auth=self.auth,
|
|
43
|
+
)
|
|
44
|
+
if response.status_code != 409:
|
|
45
|
+
response.raise_for_status()
|
|
46
|
+
return response
|
|
47
|
+
|
|
48
|
+
def put(
|
|
49
|
+
self,
|
|
50
|
+
path: str,
|
|
51
|
+
params: dict[str, str] | None = None,
|
|
52
|
+
headers: dict[str, str] | None = None,
|
|
53
|
+
json: dict[str, dict[str, Any]] | None = None,
|
|
54
|
+
data: bytes | None = None,
|
|
55
|
+
) -> requests.Response:
|
|
56
|
+
response: requests.Response = requests.put(
|
|
57
|
+
f"{self.url}{path}",
|
|
58
|
+
params=params,
|
|
59
|
+
headers=headers,
|
|
60
|
+
json=json,
|
|
61
|
+
data=data,
|
|
62
|
+
auth=self.auth,
|
|
63
|
+
)
|
|
64
|
+
response.raise_for_status()
|
|
65
|
+
return response
|
|
66
|
+
|
|
67
|
+
def delete(
|
|
68
|
+
self,
|
|
69
|
+
path: str,
|
|
70
|
+
params: dict[str, str] | None = None,
|
|
71
|
+
headers: dict[str, str] | None = None,
|
|
72
|
+
) -> requests.Response:
|
|
73
|
+
response: requests.Response = requests.delete(
|
|
74
|
+
f"{self.url}{path}",
|
|
75
|
+
params=params,
|
|
76
|
+
headers=headers,
|
|
77
|
+
auth=self.auth,
|
|
78
|
+
)
|
|
79
|
+
if response.status_code != 404:
|
|
80
|
+
response.raise_for_status()
|
|
81
|
+
return response
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
EPSG_BBOX = {
|
|
4
|
+
2056: {
|
|
5
|
+
"nativeBoundingBox": {
|
|
6
|
+
"crs": {"$": f"EPSG:2056", "@class": "projected"},
|
|
7
|
+
"maxx": 2837016.9329778464,
|
|
8
|
+
"maxy": 1299782.763494124,
|
|
9
|
+
"minx": 2485014.052451379,
|
|
10
|
+
"miny": 1074188.6943776933,
|
|
11
|
+
},
|
|
12
|
+
"latLonBoundingBox": {
|
|
13
|
+
"crs": "EPSG:4326",
|
|
14
|
+
"maxx": 10.603307860867739,
|
|
15
|
+
"maxy": 47.8485348773655,
|
|
16
|
+
"minx": 5.902662003204146,
|
|
17
|
+
"miny": 45.7779277267225,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
4326: {
|
|
21
|
+
"nativeBoundingBox": {
|
|
22
|
+
"crs": {"$": f"EPSG:4326", "@class": "projected"},
|
|
23
|
+
"maxx": 180,
|
|
24
|
+
"maxy": 90,
|
|
25
|
+
"minx": -180,
|
|
26
|
+
"miny": -90,
|
|
27
|
+
},
|
|
28
|
+
"latLonBoundingBox": {
|
|
29
|
+
"crs": "EPSG:4326",
|
|
30
|
+
"maxx": 180,
|
|
31
|
+
"maxy": 90,
|
|
32
|
+
"minx": -180,
|
|
33
|
+
"miny": -90,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Templates:
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def workspace_wms(workspace: str) -> dict[str, dict[str, Any]]:
|
|
43
|
+
return {
|
|
44
|
+
"wms": {
|
|
45
|
+
"workspace": {"name": workspace},
|
|
46
|
+
"enabled": True,
|
|
47
|
+
"name": "WMS",
|
|
48
|
+
"versions": {
|
|
49
|
+
"org.geotools.util.Version": [
|
|
50
|
+
{"version": "1.1.1"},
|
|
51
|
+
{"version": "1.3.0"},
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"citeCompliant": False,
|
|
55
|
+
"schemaBaseURL": "http://schemas.opengis.net",
|
|
56
|
+
"verbose": False,
|
|
57
|
+
"bboxForEachCRS": False,
|
|
58
|
+
"watermark": {
|
|
59
|
+
"enabled": False,
|
|
60
|
+
"position": "BOT_RIGHT",
|
|
61
|
+
"transparency": 100,
|
|
62
|
+
},
|
|
63
|
+
"interpolation": "Nearest",
|
|
64
|
+
"getFeatureInfoMimeTypeCheckingEnabled": False,
|
|
65
|
+
"getMapMimeTypeCheckingEnabled": False,
|
|
66
|
+
"dynamicStylingDisabled": False,
|
|
67
|
+
"featuresReprojectionDisabled": False,
|
|
68
|
+
"maxBuffer": 0,
|
|
69
|
+
"maxRequestMemory": 0,
|
|
70
|
+
"maxRenderingTime": 0,
|
|
71
|
+
"maxRenderingErrors": 0,
|
|
72
|
+
"maxRequestedDimensionValues": 100,
|
|
73
|
+
"cacheConfiguration": {
|
|
74
|
+
"enabled": False,
|
|
75
|
+
"maxEntries": 1000,
|
|
76
|
+
"maxEntrySize": 51200,
|
|
77
|
+
},
|
|
78
|
+
"remoteStyleMaxRequestTime": 60000,
|
|
79
|
+
"remoteStyleTimeout": 30000,
|
|
80
|
+
"defaultGroupStyleEnabled": True,
|
|
81
|
+
"transformFeatureInfoDisabled": False,
|
|
82
|
+
"autoEscapeTemplateValues": False,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def postgis_data_store(
|
|
88
|
+
datastore: str,
|
|
89
|
+
pg_host: str,
|
|
90
|
+
pg_port: int,
|
|
91
|
+
pg_db: str,
|
|
92
|
+
pg_user: str,
|
|
93
|
+
pg_password: str,
|
|
94
|
+
namespace: str,
|
|
95
|
+
pg_schema: str = "public",
|
|
96
|
+
) -> dict[str, dict[str, Any]]:
|
|
97
|
+
return {
|
|
98
|
+
"dataStore": {
|
|
99
|
+
"name": datastore,
|
|
100
|
+
"connectionParameters": {
|
|
101
|
+
"entry": [
|
|
102
|
+
{"@key": "dbtype", "$": "postgis"},
|
|
103
|
+
{"@key": "host", "$": pg_host},
|
|
104
|
+
{"@key": "port", "$": pg_port},
|
|
105
|
+
{"@key": "database", "$": pg_db},
|
|
106
|
+
{"@key": "user", "$": pg_user},
|
|
107
|
+
{"@key": "passwd", "$": pg_password},
|
|
108
|
+
{"@key": "schema", "$": pg_schema},
|
|
109
|
+
{
|
|
110
|
+
"@key": "namespace",
|
|
111
|
+
"$": namespace,
|
|
112
|
+
},
|
|
113
|
+
{"@key": "Expose primary keys", "$": "true"},
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def postgis_jndi_data_store(
|
|
121
|
+
datastore: str,
|
|
122
|
+
jndi_reference: str,
|
|
123
|
+
namespace: str,
|
|
124
|
+
pg_schema: str = "public",
|
|
125
|
+
description: str | None = None,
|
|
126
|
+
) -> dict[str, dict[str, Any]]:
|
|
127
|
+
return {
|
|
128
|
+
"dataStore": {
|
|
129
|
+
"name": datastore,
|
|
130
|
+
"description": description,
|
|
131
|
+
"connectionParameters": {
|
|
132
|
+
"entry": [
|
|
133
|
+
{"@key": "dbtype", "$": "postgis"},
|
|
134
|
+
{
|
|
135
|
+
"@key": "jndiReferenceName",
|
|
136
|
+
"$": jndi_reference,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"@key": "schema",
|
|
140
|
+
"$": pg_schema,
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"@key": "namespace",
|
|
144
|
+
"$": namespace,
|
|
145
|
+
},
|
|
146
|
+
{"@key": "Expose primary keys", "$": "true"},
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def wmts_store(
|
|
154
|
+
workspace: str, name: str, capabilities: str
|
|
155
|
+
) -> dict[str, dict[str, Any]]:
|
|
156
|
+
return {
|
|
157
|
+
"wmtsStore": {
|
|
158
|
+
"name": name,
|
|
159
|
+
"type": "WMTS",
|
|
160
|
+
"capabilitiesURL": capabilities,
|
|
161
|
+
"workspace": {"name": workspace},
|
|
162
|
+
"enabled": True,
|
|
163
|
+
"metadata": {"entry": {"@key": "useConnectionPooling", "text": True}},
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def geom_point_attribute() -> dict[str, Any]:
|
|
169
|
+
return {
|
|
170
|
+
"geom": {
|
|
171
|
+
"type": "Point",
|
|
172
|
+
"required": True,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def feature_type(
|
|
178
|
+
layer: str,
|
|
179
|
+
workspace: str,
|
|
180
|
+
datastore: str,
|
|
181
|
+
attributes: list[dict],
|
|
182
|
+
epsg: int = 4326,
|
|
183
|
+
) -> dict[str, dict[str, Any]]:
|
|
184
|
+
return {
|
|
185
|
+
"featureType": {
|
|
186
|
+
"name": layer,
|
|
187
|
+
"nativeName": layer,
|
|
188
|
+
"srs": f"EPSG:{epsg}",
|
|
189
|
+
"enabled": True,
|
|
190
|
+
"store": {
|
|
191
|
+
"name": f"{workspace}:{datastore}",
|
|
192
|
+
},
|
|
193
|
+
"attributes": {
|
|
194
|
+
"attribute": attributes,
|
|
195
|
+
},
|
|
196
|
+
"nativeBoundingBox": {
|
|
197
|
+
"crs": EPSG_BBOX[epsg]["nativeBoundingBox"]["crs"],
|
|
198
|
+
"maxx": EPSG_BBOX[epsg]["nativeBoundingBox"]["maxx"],
|
|
199
|
+
"maxy": EPSG_BBOX[epsg]["nativeBoundingBox"]["maxy"],
|
|
200
|
+
"minx": EPSG_BBOX[epsg]["nativeBoundingBox"]["minx"],
|
|
201
|
+
"miny": EPSG_BBOX[epsg]["nativeBoundingBox"]["miny"],
|
|
202
|
+
},
|
|
203
|
+
"latLonBoundingBox": {
|
|
204
|
+
"crs": EPSG_BBOX[epsg]["latLonBoundingBox"]["crs"],
|
|
205
|
+
"maxx": EPSG_BBOX[epsg]["latLonBoundingBox"]["maxx"],
|
|
206
|
+
"maxy": EPSG_BBOX[epsg]["latLonBoundingBox"]["maxy"],
|
|
207
|
+
"minx": EPSG_BBOX[epsg]["latLonBoundingBox"]["minx"],
|
|
208
|
+
"miny": EPSG_BBOX[epsg]["latLonBoundingBox"]["miny"],
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def layer_group(
|
|
215
|
+
group: str,
|
|
216
|
+
layers: list[str],
|
|
217
|
+
workspace: str,
|
|
218
|
+
title: str | dict[str, Any],
|
|
219
|
+
abstract: str | dict[str, Any],
|
|
220
|
+
epsg: int = 4326,
|
|
221
|
+
) -> dict[str, dict[str, Any]]:
|
|
222
|
+
template = {
|
|
223
|
+
"layerGroup": {
|
|
224
|
+
"name": group,
|
|
225
|
+
"workspace": {"name": workspace},
|
|
226
|
+
"mode": "SINGLE",
|
|
227
|
+
"publishables": {
|
|
228
|
+
"published": [
|
|
229
|
+
{"@type": "layer", "name": f"{workspace}:{layer}"}
|
|
230
|
+
for layer in layers
|
|
231
|
+
]
|
|
232
|
+
},
|
|
233
|
+
"styles": {"style": [{"name": ""}] * len(layers)},
|
|
234
|
+
"bounds": {
|
|
235
|
+
"minx": EPSG_BBOX[epsg]["nativeBoundingBox"]["minx"],
|
|
236
|
+
"maxx": EPSG_BBOX[epsg]["nativeBoundingBox"]["maxx"],
|
|
237
|
+
"miny": EPSG_BBOX[epsg]["nativeBoundingBox"]["miny"],
|
|
238
|
+
"maxy": EPSG_BBOX[epsg]["nativeBoundingBox"]["maxy"],
|
|
239
|
+
"crs": f"EPSG:{epsg}",
|
|
240
|
+
},
|
|
241
|
+
"enabled": True,
|
|
242
|
+
"advertised": True,
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if title:
|
|
246
|
+
if type(title) is dict:
|
|
247
|
+
template["layerGroup"]["internationalTitle"] = title
|
|
248
|
+
else:
|
|
249
|
+
template["layerGroup"]["title"] = title
|
|
250
|
+
if abstract:
|
|
251
|
+
if type(abstract) is dict:
|
|
252
|
+
template["layerGroup"]["internationalAbstract"] = abstract
|
|
253
|
+
else:
|
|
254
|
+
template["layerGroup"]["abstract"] = abstract
|
|
255
|
+
return template
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def wmts_layer(
|
|
259
|
+
name: str,
|
|
260
|
+
native_name: str,
|
|
261
|
+
epsg: int = 4326,
|
|
262
|
+
wgs84_bbox: tuple[float, float, float, float] | None = None,
|
|
263
|
+
) -> dict[str, dict[str, Any]]:
|
|
264
|
+
template = {
|
|
265
|
+
"wmtsLayer": {
|
|
266
|
+
"advertised": True,
|
|
267
|
+
"enabled": True,
|
|
268
|
+
"name": name,
|
|
269
|
+
"nativeName": native_name,
|
|
270
|
+
"projectionPolicy": "FORCE_DECLARED",
|
|
271
|
+
"serviceConfiguration": False,
|
|
272
|
+
"simpleConversionEnabled": False,
|
|
273
|
+
"srs": f"EPSG:{epsg}",
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if wgs84_bbox:
|
|
277
|
+
template["wmtsLayer"]["latLonBoundingBox"] = {
|
|
278
|
+
"crs": "EPSG:4326",
|
|
279
|
+
"minx": wgs84_bbox[0],
|
|
280
|
+
"maxx": wgs84_bbox[2],
|
|
281
|
+
"miny": wgs84_bbox[1],
|
|
282
|
+
"maxy": wgs84_bbox[3],
|
|
283
|
+
}
|
|
284
|
+
if epsg == 4326:
|
|
285
|
+
template["wmtsLayer"]["nativeBoundingBox"] = {
|
|
286
|
+
"crs": "EPSG:4326",
|
|
287
|
+
"minx": wgs84_bbox[0],
|
|
288
|
+
"maxx": wgs84_bbox[2],
|
|
289
|
+
"miny": wgs84_bbox[1],
|
|
290
|
+
"maxy": wgs84_bbox[3],
|
|
291
|
+
}
|
|
292
|
+
return template
|
|
293
|
+
|
|
294
|
+
@staticmethod
|
|
295
|
+
def gwc_layer(
|
|
296
|
+
workspace: str, layer: str, gridset: str
|
|
297
|
+
) -> dict[str, dict[str, Any]]:
|
|
298
|
+
return {
|
|
299
|
+
"GeoServerLayer": {
|
|
300
|
+
"name": f"{workspace}:{layer}",
|
|
301
|
+
"enabled": "true",
|
|
302
|
+
"gridSubsets": {"gridSubset": [{"gridSetName": gridset}]},
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
def java_binding(data_type: str) -> str:
|
|
2
|
+
match data_type:
|
|
3
|
+
case "string":
|
|
4
|
+
return "java.lang.String"
|
|
5
|
+
case "integer":
|
|
6
|
+
return "java.lang.Integer"
|
|
7
|
+
case "float":
|
|
8
|
+
return "java.lang.Float"
|
|
9
|
+
case "datetime":
|
|
10
|
+
return "java.util.Date"
|
|
11
|
+
case "Point":
|
|
12
|
+
return "org.locationtech.jts.geom.Point"
|
|
13
|
+
case "Line":
|
|
14
|
+
return "org.locationtech.jts.geom.LineString"
|
|
15
|
+
case "Polygon":
|
|
16
|
+
return "org.locationtech.jts.geom.Polygon"
|
|
17
|
+
case "MultiPolygon":
|
|
18
|
+
return "org.locationtech.jts.geom.MultiPolygon"
|
|
19
|
+
case _:
|
|
20
|
+
return "java.lang.String"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def convert_attributes(attributes: dict) -> list[dict]:
|
|
24
|
+
geoserver_attributes = []
|
|
25
|
+
for name, data in attributes.items():
|
|
26
|
+
required: bool = data.get("required", False)
|
|
27
|
+
geoserver_attributes.append(
|
|
28
|
+
{
|
|
29
|
+
"name": name,
|
|
30
|
+
"minOccurs": int(required),
|
|
31
|
+
"maxOccurs": 1,
|
|
32
|
+
"nillable": not required,
|
|
33
|
+
"binding": java_binding(data["type"]),
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
return geoserver_attributes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "geoservercloud"
|
|
3
|
+
version = "0.1.0.dev1"
|
|
4
|
+
description = "Lightweight Python client to interact with GeoServer Cloud REST API, GeoServer ACL and OGC services"
|
|
5
|
+
authors = ["Camptocamp <info@camptocamp.com>"]
|
|
6
|
+
license = "BSD-2-Clause"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
packages = [{include = "geoservercloud"}]
|
|
9
|
+
|
|
10
|
+
[tool.poetry.dependencies]
|
|
11
|
+
python = ">=3.9,<4.0"
|
|
12
|
+
requests = "2.32.3"
|
|
13
|
+
OWSLib = "0.31.0"
|
|
14
|
+
xmltodict = "0.13.0"
|
|
15
|
+
|
|
16
|
+
[tool.poetry.group.dev.dependencies]
|
|
17
|
+
pytest = "8.2.2"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = [
|
|
21
|
+
"poetry-core>=1.0.0",
|
|
22
|
+
"poetry-dynamic-versioning[plugin]",
|
|
23
|
+
"poetry-plugin-tweak-dependencies-version"
|
|
24
|
+
]
|
|
25
|
+
build-backend = "poetry.core.masonry.api"
|
|
26
|
+
|
|
27
|
+
[tool.poetry-dynamic-versioning]
|
|
28
|
+
enable = false
|
|
29
|
+
vcs = "git"
|
|
30
|
+
pattern = "^(?P<base>\\d+(\\.\\d+)*)"
|
|
31
|
+
format-jinja = """
|
|
32
|
+
{%- if env.get("VERSION_TYPE") == "version_branch" -%}
|
|
33
|
+
{{serialize_pep440(bump_version(base, 1 if env.get("IS_MASTER") == "TRUE" else 2), dev=distance)}}
|
|
34
|
+
{%- elif distance == 0 -%}
|
|
35
|
+
{{serialize_pep440(base)}}
|
|
36
|
+
{%- else -%}
|
|
37
|
+
{{serialize_pep440(bump_version(base), dev=distance)}}
|
|
38
|
+
{%- endif -%}
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
[tool.poetry-plugin-tweak-dependencies-version]
|
|
42
|
+
default = "present"
|