Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions geoservercloud/exceptions.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
from dataclasses import dataclass, field
from typing import Any

from requests import Request, Response


@dataclass
class GsDetail:
message: str
info: dict[str, Any] = field(default_factory=lambda: {})


class GsException(Exception):
def __init__(
self,
code: int,
detail: GsDetail,
parent_request: Request | None = None,
parent_response: Response | None = None,
):
super().__init__()
self.code = code
self.detail = detail
self.parent_request = parent_request
self.parent_response = parent_response


class AuthException(GsException):
pass


class DatastoreMissing(GsException):
pass


class WorkspaceMissing(GsException):
pass
5 changes: 3 additions & 2 deletions geoservercloud/geoservercloud.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,14 +37,15 @@ def __init__(
url: str = "http://localhost:9090/geoserver/cloud",
user: str = "admin",
password: str = "geoserver", # nosec
verifytls: bool = True,
) -> None:

self.url: str = url.strip("/")
self.user: str = user
self.password: str = password
self.auth: tuple[str, str] = (user, password)
self.rest_service: RestService = RestService(url, self.auth)
self.ows_service: OwsService = OwsService(url, self.auth)
self.rest_service: RestService = RestService(url, self.auth, verifytls)
self.ows_service: OwsService = OwsService(url, self.auth, verifytls)
self.wms: WebMapService_1_3_0 | None = None
self.wmts: WebMapTileService | None = None
self.default_workspace: str | None = None
Expand Down
98 changes: 83 additions & 15 deletions geoservercloud/geoservercloudsync.py
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
import json
from argparse import ArgumentParser

from requests.exceptions import HTTPError

from geoservercloud.exceptions import DatastoreMissing, GsDetail, WorkspaceMissing
from geoservercloud.models.layer import Layer
from geoservercloud.services import RestService


Expand DownExpand Up@@ -31,17 +36,23 @@ def __init__(
dst_url: str,
dst_user: str,
dst_password: str,
src_verifytls: bool = True,
dst_verifytls: bool = True,
) -> None:
self.src_url: str = src_url.strip("/")
self.src_user: str = src_user
self.src_password: str = src_password
self.src_auth: tuple[str, str] = (src_user, src_password)
self.src_instance: RestService = RestService(src_url, self.src_auth)
self.src_instance: RestService = RestService(
src_url, self.src_auth, src_verifytls
)
self.dst_url: str = dst_url.strip("/")
self.dst_user: str = dst_user
self.dst_password: str = dst_password
self.dst_auth: tuple[str, str] = (dst_user, dst_password)
self.dst_instance: RestService = RestService(dst_url, self.dst_auth)
self.dst_instance: RestService = RestService(
dst_url, self.dst_auth, src_verifytls
)

def copy_workspace(
self, workspace_name: str, deep_copy: bool = False
Expand DownExpand Up@@ -163,9 +174,47 @@ def copy_layer(
layer, status_code = self.src_instance.get_layer(
workspace_name, feature_type_name
)
if isinstance(layer, str):
return layer, status_code
return self.dst_instance.update_layer(layer, workspace_name)
if (status_code != 200) or isinstance(layer, str):
return f"Error: unexpected response {layer}", 500
resource, status_code = self.src_instance.get_resource_from_layer(layer)
layer_string = json.dumps(layer.asdict())
dst_layer = Layer.from_get_response_payload(
{
"layer": json.loads(
layer_string.replace(self.src_instance.url, self.dst_instance.url)
)
}
)

xml_resource_route = self.src_instance.get_resource_route_from_layer(layer)
if xml_resource_route is None:
return f"Error: cannot get resource route for {layer}", 500
try:
self.dst_instance.create_layer_resource(resource, xml_resource_route)
except HTTPError:
dst_resource = resource.decode().replace(
self.src_instance.url, self.dst_instance.url
)
store = json.loads(dst_resource)["featureType"]["store"]
if not self.dst_instance.check_href(store):
raise DatastoreMissing(
404,
GsDetail(
f"Datastore {store['name']} not found",
),
)
for ws_name, workspace in self.dst_instance.get_workspaces_from_store(
store
).items():
if not self.dst_instance.check_href(workspace):
raise WorkspaceMissing(
404,
GsDetail(
f"Workspace {ws_name} not found",
),
)

return self.dst_instance.update_layer(dst_layer, workspace_name)

def copy_layer_groups(self, workspace_name: str) -> tuple[str, int]:
"""
Expand DownExpand Up@@ -228,20 +277,39 @@ def copy_style(
"""
Copy a style from source to destination GeoServer instance
"""
style_definition, status_code = self.src_instance.get_style_definition(
style_info, status_code = self.src_instance.get_style_info(
style_name, workspace_name
)
if isinstance(style_definition, str):
return style_definition, status_code
content, status_code = self.dst_instance.create_style_definition(
style_name, style_definition, workspace_name
)
if self.not_ok(status_code):
return f"Error getting {style_info}", status_code
style_definition_response = self.src_instance.get_raw_style_definition(
style_name, workspace_name, style_info.format
)
if self.not_ok(style_definition_response.status_code):
content: str = style_definition_response.content.decode()
return content, status_code
style, status_code = self.src_instance.get_style(style_name, workspace_name)
if isinstance(style, str):
return style, status_code
return self.dst_instance.create_style(style_name, style, workspace_name)
try:
content, status_code = self.dst_instance.create_style_info(
style_name, style_info, workspace_name
)
if self.not_ok(status_code):
return content, status_code
except HTTPError:
if not self.dst_instance.check_workspace_for_style(style_info):
raise WorkspaceMissing(
404,
GsDetail(
f"Workspace {style_info.workspace_name} not found",
),
)

return self.dst_instance.fill_style_definition(
style_name,
style_definition_response.content,
style_info.format,
style_definition_response.headers["content-type"],
workspace_name,
)

def copy_style_images(self, workspace_name: str | None = None) -> tuple[str, int]:
"""
Expand Down
23 changes: 20 additions & 3 deletions geoservercloud/models/layer.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ def __init__(
self,
name: str,
resource_name: str | None = None,
resource_href: str | None = None,
type: str | None = None,
default_style_name: str | None = None,
styles: list | None = None,
Expand All@@ -19,7 +20,7 @@ def __init__(
self.type: str | None = type
self.resource: ReferencedObjectModel | None = None
if resource_name:
self.resource = ReferencedObjectModel(resource_name)
self.resource = ReferencedObjectModel(resource_name, resource_href)
self.default_style: ReferencedObjectModel | None = None
if default_style_name:
self.default_style = ReferencedObjectModel(default_style_name)
Expand All@@ -35,6 +36,15 @@ def resource_name(self) -> str | None:
def default_style_name(self) -> str | None:
return self.default_style.name if self.default_style else None

@property
def all_style_names(self) -> set[str]:
all_styles = set()
if self.default_style_name is not None:
all_styles.add(self.default_style_name)
if self.styles is not None:
all_styles.update(self.styles)
return all_styles

@classmethod
def from_get_response_payload(cls, content: dict):
layer = content["layer"]
Expand All@@ -45,6 +55,7 @@ def from_get_response_payload(cls, content: dict):
return cls(
name=layer["name"],
resource_name=layer["resource"]["name"],
resource_href=layer["resource"]["href"],
type=layer["type"],
default_style_name=layer["defaultStyle"]["name"],
styles=styles,
Expand All@@ -59,11 +70,17 @@ def asdict(self) -> dict[str, Any]:
optional_items = {
"name": self.name,
"type": self.type,
"resource": self.resource_name,
"defaultStyle": self.default_style_name,
"defaultStyle": {
"name": self.default_style_name,
},
"attribution": self.attribution,
"queryable": self.queryable,
}
if self.resource is not None:
optional_items["resource"] = {
"name": self.resource.name,
"href": self.resource.href,
}
return EntityModel.add_items_to_dict(content, optional_items)

def post_payload(self) -> dict[str, dict[str, Any]]:
Expand Down
4 changes: 2 additions & 2 deletions geoservercloud/services/owsservice.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,11 +10,11 @@


class OwsService:
def __init__(self, url: str, auth: tuple[str, str]) -> None:
def __init__(self, url: str, auth: tuple[str, str], verifytls: bool = True) -> None:
self.url: str = url
self.auth: tuple[str, str] = auth
self.ows_endpoints = self.OwsEndpoints()
self.rest_client = RestClient(url, auth)
self.rest_client = RestClient(url, auth, verifytls)

def create_wms(self, workspace_name: str | None = None) -> WebMapService_1_3_0:
if workspace_name is None:
Expand Down
46 changes: 40 additions & 6 deletions geoservercloud/services/restclient.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,8 @@

import requests

from .restlogger import gs_logger

TIMEOUT = 120


Expand All@@ -17,22 +19,31 @@ class RestClient:
username and password for GeoServer
"""

def __init__(self, url: str, auth: tuple[str, str]) -> None:
def __init__(self, url: str, auth: tuple[str, str], verifytls: bool = True) -> None:
self.url: str = url
self.auth: tuple[str, str] = auth
self.verifytls: bool = verifytls

def get(
self,
path: str,
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.get(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 404:
response.raise_for_status()
Expand All@@ -46,15 +57,22 @@ def post(
json: dict[str, dict[str, Any] | Any] | None = None,
data: bytes | None = None,
) -> requests.Response:

full_url = f"{self.url}{path}"
response: requests.Response = requests.post(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
json=json,
data=data,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 409:
response.raise_for_status()
Expand All@@ -68,14 +86,22 @@ def put(
json: dict[str, dict[str, Any]] | None = None,
data: bytes | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.put(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
json=json,
data=data,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
response.raise_for_status()
return response
Expand All@@ -86,12 +112,20 @@ def delete(
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.delete(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 404:
response.raise_for_status()
Expand Down
12 changes: 12 additions & 0 deletions geoservercloud/services/restlogger.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import logging

time_formatter = logging.Formatter(
"{asctime} - {name}:{levelname} - {message}",
style="{",
datefmt="%Y-%m-%d %H:%M",
)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(time_formatter)
gs_logger = logging.getLogger("GS Session")
gs_logger.setLevel(logging.INFO)
gs_logger.addHandler(stream_handler)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Copy layers styles by mki-c2c · Pull Request #79 · camptocamp/python-geoservercloud · GitHub
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions geoservercloud/exceptions.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
from dataclasses import dataclass, field
from typing import Any

from requests import Request, Response


@dataclass
class GsDetail:
message: str
info: dict[str, Any] = field(default_factory=lambda: {})


class GsException(Exception):
def __init__(
self,
code: int,
detail: GsDetail,
parent_request: Request | None = None,
parent_response: Response | None = None,
):
super().__init__()
self.code = code
self.detail = detail
self.parent_request = parent_request
self.parent_response = parent_response


class AuthException(GsException):
pass


class DatastoreMissing(GsException):
pass


class WorkspaceMissing(GsException):
pass
5 changes: 3 additions & 2 deletions geoservercloud/geoservercloud.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,14 +37,15 @@ def __init__(
url: str = "http://localhost:9090/geoserver/cloud",
user: str = "admin",
password: str = "geoserver", # nosec
verifytls: bool = True,
) -> None:

self.url: str = url.strip("/")
self.user: str = user
self.password: str = password
self.auth: tuple[str, str] = (user, password)
self.rest_service: RestService = RestService(url, self.auth)
self.ows_service: OwsService = OwsService(url, self.auth)
self.rest_service: RestService = RestService(url, self.auth, verifytls)
self.ows_service: OwsService = OwsService(url, self.auth, verifytls)
self.wms: WebMapService_1_3_0 | None = None
self.wmts: WebMapTileService | None = None
self.default_workspace: str | None = None
Expand Down
98 changes: 83 additions & 15 deletions geoservercloud/geoservercloudsync.py
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
import json
from argparse import ArgumentParser

from requests.exceptions import HTTPError

from geoservercloud.exceptions import DatastoreMissing, GsDetail, WorkspaceMissing
from geoservercloud.models.layer import Layer
from geoservercloud.services import RestService


Expand DownExpand Up@@ -31,17 +36,23 @@ def __init__(
dst_url: str,
dst_user: str,
dst_password: str,
src_verifytls: bool = True,
dst_verifytls: bool = True,
) -> None:
self.src_url: str = src_url.strip("/")
self.src_user: str = src_user
self.src_password: str = src_password
self.src_auth: tuple[str, str] = (src_user, src_password)
self.src_instance: RestService = RestService(src_url, self.src_auth)
self.src_instance: RestService = RestService(
src_url, self.src_auth, src_verifytls
)
self.dst_url: str = dst_url.strip("/")
self.dst_user: str = dst_user
self.dst_password: str = dst_password
self.dst_auth: tuple[str, str] = (dst_user, dst_password)
self.dst_instance: RestService = RestService(dst_url, self.dst_auth)
self.dst_instance: RestService = RestService(
dst_url, self.dst_auth, src_verifytls
)

def copy_workspace(
self, workspace_name: str, deep_copy: bool = False
Expand DownExpand Up@@ -163,9 +174,47 @@ def copy_layer(
layer, status_code = self.src_instance.get_layer(
workspace_name, feature_type_name
)
if isinstance(layer, str):
return layer, status_code
return self.dst_instance.update_layer(layer, workspace_name)
if (status_code != 200) or isinstance(layer, str):
return f"Error: unexpected response {layer}", 500
resource, status_code = self.src_instance.get_resource_from_layer(layer)
layer_string = json.dumps(layer.asdict())
dst_layer = Layer.from_get_response_payload(
{
"layer": json.loads(
layer_string.replace(self.src_instance.url, self.dst_instance.url)
)
}
)

xml_resource_route = self.src_instance.get_resource_route_from_layer(layer)
if xml_resource_route is None:
return f"Error: cannot get resource route for {layer}", 500
try:
self.dst_instance.create_layer_resource(resource, xml_resource_route)
except HTTPError:
dst_resource = resource.decode().replace(
self.src_instance.url, self.dst_instance.url
)
store = json.loads(dst_resource)["featureType"]["store"]
if not self.dst_instance.check_href(store):
raise DatastoreMissing(
404,
GsDetail(
f"Datastore {store['name']} not found",
),
)
for ws_name, workspace in self.dst_instance.get_workspaces_from_store(
store
).items():
if not self.dst_instance.check_href(workspace):
raise WorkspaceMissing(
404,
GsDetail(
f"Workspace {ws_name} not found",
),
)

return self.dst_instance.update_layer(dst_layer, workspace_name)

def copy_layer_groups(self, workspace_name: str) -> tuple[str, int]:
"""
Expand DownExpand Up@@ -228,20 +277,39 @@ def copy_style(
"""
Copy a style from source to destination GeoServer instance
"""
style_definition, status_code = self.src_instance.get_style_definition(
style_info, status_code = self.src_instance.get_style_info(
style_name, workspace_name
)
if isinstance(style_definition, str):
return style_definition, status_code
content, status_code = self.dst_instance.create_style_definition(
style_name, style_definition, workspace_name
)
if self.not_ok(status_code):
return f"Error getting {style_info}", status_code
style_definition_response = self.src_instance.get_raw_style_definition(
style_name, workspace_name, style_info.format
)
if self.not_ok(style_definition_response.status_code):
content: str = style_definition_response.content.decode()
return content, status_code
style, status_code = self.src_instance.get_style(style_name, workspace_name)
if isinstance(style, str):
return style, status_code
return self.dst_instance.create_style(style_name, style, workspace_name)
try:
content, status_code = self.dst_instance.create_style_info(
style_name, style_info, workspace_name
)
if self.not_ok(status_code):
return content, status_code
except HTTPError:
if not self.dst_instance.check_workspace_for_style(style_info):
raise WorkspaceMissing(
404,
GsDetail(
f"Workspace {style_info.workspace_name} not found",
),
)

return self.dst_instance.fill_style_definition(
style_name,
style_definition_response.content,
style_info.format,
style_definition_response.headers["content-type"],
workspace_name,
)

def copy_style_images(self, workspace_name: str | None = None) -> tuple[str, int]:
"""
Expand Down
23 changes: 20 additions & 3 deletions geoservercloud/models/layer.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ def __init__(
self,
name: str,
resource_name: str | None = None,
resource_href: str | None = None,
type: str | None = None,
default_style_name: str | None = None,
styles: list | None = None,
Expand All@@ -19,7 +20,7 @@ def __init__(
self.type: str | None = type
self.resource: ReferencedObjectModel | None = None
if resource_name:
self.resource = ReferencedObjectModel(resource_name)
self.resource = ReferencedObjectModel(resource_name, resource_href)
self.default_style: ReferencedObjectModel | None = None
if default_style_name:
self.default_style = ReferencedObjectModel(default_style_name)
Expand All@@ -35,6 +36,15 @@ def resource_name(self) -> str | None:
def default_style_name(self) -> str | None:
return self.default_style.name if self.default_style else None

@property
def all_style_names(self) -> set[str]:
all_styles = set()
if self.default_style_name is not None:
all_styles.add(self.default_style_name)
if self.styles is not None:
all_styles.update(self.styles)
return all_styles

@classmethod
def from_get_response_payload(cls, content: dict):
layer = content["layer"]
Expand All@@ -45,6 +55,7 @@ def from_get_response_payload(cls, content: dict):
return cls(
name=layer["name"],
resource_name=layer["resource"]["name"],
resource_href=layer["resource"]["href"],
type=layer["type"],
default_style_name=layer["defaultStyle"]["name"],
styles=styles,
Expand All@@ -59,11 +70,17 @@ def asdict(self) -> dict[str, Any]:
optional_items = {
"name": self.name,
"type": self.type,
"resource": self.resource_name,
"defaultStyle": self.default_style_name,
"defaultStyle": {
"name": self.default_style_name,
},
"attribution": self.attribution,
"queryable": self.queryable,
}
if self.resource is not None:
optional_items["resource"] = {
"name": self.resource.name,
"href": self.resource.href,
}
return EntityModel.add_items_to_dict(content, optional_items)

def post_payload(self) -> dict[str, dict[str, Any]]:
Expand Down
4 changes: 2 additions & 2 deletions geoservercloud/services/owsservice.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,11 +10,11 @@


class OwsService:
def __init__(self, url: str, auth: tuple[str, str]) -> None:
def __init__(self, url: str, auth: tuple[str, str], verifytls: bool = True) -> None:
self.url: str = url
self.auth: tuple[str, str] = auth
self.ows_endpoints = self.OwsEndpoints()
self.rest_client = RestClient(url, auth)
self.rest_client = RestClient(url, auth, verifytls)

def create_wms(self, workspace_name: str | None = None) -> WebMapService_1_3_0:
if workspace_name is None:
Expand Down
46 changes: 40 additions & 6 deletions geoservercloud/services/restclient.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,8 @@

import requests

from .restlogger import gs_logger

TIMEOUT = 120


Expand All@@ -17,22 +19,31 @@ class RestClient:
username and password for GeoServer
"""

def __init__(self, url: str, auth: tuple[str, str]) -> None:
def __init__(self, url: str, auth: tuple[str, str], verifytls: bool = True) -> None:
self.url: str = url
self.auth: tuple[str, str] = auth
self.verifytls: bool = verifytls

def get(
self,
path: str,
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.get(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 404:
response.raise_for_status()
Expand All@@ -46,15 +57,22 @@ def post(
json: dict[str, dict[str, Any] | Any] | None = None,
data: bytes | None = None,
) -> requests.Response:

full_url = f"{self.url}{path}"
response: requests.Response = requests.post(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
json=json,
data=data,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 409:
response.raise_for_status()
Expand All@@ -68,14 +86,22 @@ def put(
json: dict[str, dict[str, Any]] | None = None,
data: bytes | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.put(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
json=json,
data=data,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
response.raise_for_status()
return response
Expand All@@ -86,12 +112,20 @@ def delete(
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.delete(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 404:
response.raise_for_status()
Expand Down
12 changes: 12 additions & 0 deletions geoservercloud/services/restlogger.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import logging

time_formatter = logging.Formatter(
"{asctime} - {name}:{levelname} - {message}",
style="{",
datefmt="%Y-%m-%d %H:%M",
)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(time_formatter)
gs_logger = logging.getLogger("GS Session")
gs_logger.setLevel(logging.INFO)
gs_logger.addHandler(stream_handler)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Copy layers styles by mki-c2c · Pull Request #79 · camptocamp/python-geoservercloud · GitHub
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions geoservercloud/exceptions.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
from dataclasses import dataclass, field
from typing import Any

from requests import Request, Response


@dataclass
class GsDetail:
message: str
info: dict[str, Any] = field(default_factory=lambda: {})


class GsException(Exception):
def __init__(
self,
code: int,
detail: GsDetail,
parent_request: Request | None = None,
parent_response: Response | None = None,
):
super().__init__()
self.code = code
self.detail = detail
self.parent_request = parent_request
self.parent_response = parent_response


class AuthException(GsException):
pass


class DatastoreMissing(GsException):
pass


class WorkspaceMissing(GsException):
pass
5 changes: 3 additions & 2 deletions geoservercloud/geoservercloud.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,14 +37,15 @@ def __init__(
url: str = "http://localhost:9090/geoserver/cloud",
user: str = "admin",
password: str = "geoserver", # nosec
verifytls: bool = True,
) -> None:

self.url: str = url.strip("/")
self.user: str = user
self.password: str = password
self.auth: tuple[str, str] = (user, password)
self.rest_service: RestService = RestService(url, self.auth)
self.ows_service: OwsService = OwsService(url, self.auth)
self.rest_service: RestService = RestService(url, self.auth, verifytls)
self.ows_service: OwsService = OwsService(url, self.auth, verifytls)
self.wms: WebMapService_1_3_0 | None = None
self.wmts: WebMapTileService | None = None
self.default_workspace: str | None = None
Expand Down
98 changes: 83 additions & 15 deletions geoservercloud/geoservercloudsync.py
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
import json
from argparse import ArgumentParser

from requests.exceptions import HTTPError

from geoservercloud.exceptions import DatastoreMissing, GsDetail, WorkspaceMissing
from geoservercloud.models.layer import Layer
from geoservercloud.services import RestService


Expand DownExpand Up@@ -31,17 +36,23 @@ def __init__(
dst_url: str,
dst_user: str,
dst_password: str,
src_verifytls: bool = True,
dst_verifytls: bool = True,
) -> None:
self.src_url: str = src_url.strip("/")
self.src_user: str = src_user
self.src_password: str = src_password
self.src_auth: tuple[str, str] = (src_user, src_password)
self.src_instance: RestService = RestService(src_url, self.src_auth)
self.src_instance: RestService = RestService(
src_url, self.src_auth, src_verifytls
)
self.dst_url: str = dst_url.strip("/")
self.dst_user: str = dst_user
self.dst_password: str = dst_password
self.dst_auth: tuple[str, str] = (dst_user, dst_password)
self.dst_instance: RestService = RestService(dst_url, self.dst_auth)
self.dst_instance: RestService = RestService(
dst_url, self.dst_auth, src_verifytls
)

def copy_workspace(
self, workspace_name: str, deep_copy: bool = False
Expand DownExpand Up@@ -163,9 +174,47 @@ def copy_layer(
layer, status_code = self.src_instance.get_layer(
workspace_name, feature_type_name
)
if isinstance(layer, str):
return layer, status_code
return self.dst_instance.update_layer(layer, workspace_name)
if (status_code != 200) or isinstance(layer, str):
return f"Error: unexpected response {layer}", 500
resource, status_code = self.src_instance.get_resource_from_layer(layer)
layer_string = json.dumps(layer.asdict())
dst_layer = Layer.from_get_response_payload(
{
"layer": json.loads(
layer_string.replace(self.src_instance.url, self.dst_instance.url)
)
}
)

xml_resource_route = self.src_instance.get_resource_route_from_layer(layer)
if xml_resource_route is None:
return f"Error: cannot get resource route for {layer}", 500
try:
self.dst_instance.create_layer_resource(resource, xml_resource_route)
except HTTPError:
dst_resource = resource.decode().replace(
self.src_instance.url, self.dst_instance.url
)
store = json.loads(dst_resource)["featureType"]["store"]
if not self.dst_instance.check_href(store):
raise DatastoreMissing(
404,
GsDetail(
f"Datastore {store['name']} not found",
),
)
for ws_name, workspace in self.dst_instance.get_workspaces_from_store(
store
).items():
if not self.dst_instance.check_href(workspace):
raise WorkspaceMissing(
404,
GsDetail(
f"Workspace {ws_name} not found",
),
)

return self.dst_instance.update_layer(dst_layer, workspace_name)

def copy_layer_groups(self, workspace_name: str) -> tuple[str, int]:
"""
Expand DownExpand Up@@ -228,20 +277,39 @@ def copy_style(
"""
Copy a style from source to destination GeoServer instance
"""
style_definition, status_code = self.src_instance.get_style_definition(
style_info, status_code = self.src_instance.get_style_info(
style_name, workspace_name
)
if isinstance(style_definition, str):
return style_definition, status_code
content, status_code = self.dst_instance.create_style_definition(
style_name, style_definition, workspace_name
)
if self.not_ok(status_code):
return f"Error getting {style_info}", status_code
style_definition_response = self.src_instance.get_raw_style_definition(
style_name, workspace_name, style_info.format
)
if self.not_ok(style_definition_response.status_code):
content: str = style_definition_response.content.decode()
return content, status_code
style, status_code = self.src_instance.get_style(style_name, workspace_name)
if isinstance(style, str):
return style, status_code
return self.dst_instance.create_style(style_name, style, workspace_name)
try:
content, status_code = self.dst_instance.create_style_info(
style_name, style_info, workspace_name
)
if self.not_ok(status_code):
return content, status_code
except HTTPError:
if not self.dst_instance.check_workspace_for_style(style_info):
raise WorkspaceMissing(
404,
GsDetail(
f"Workspace {style_info.workspace_name} not found",
),
)

return self.dst_instance.fill_style_definition(
style_name,
style_definition_response.content,
style_info.format,
style_definition_response.headers["content-type"],
workspace_name,
)

def copy_style_images(self, workspace_name: str | None = None) -> tuple[str, int]:
"""
Expand Down
23 changes: 20 additions & 3 deletions geoservercloud/models/layer.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ def __init__(
self,
name: str,
resource_name: str | None = None,
resource_href: str | None = None,
type: str | None = None,
default_style_name: str | None = None,
styles: list | None = None,
Expand All@@ -19,7 +20,7 @@ def __init__(
self.type: str | None = type
self.resource: ReferencedObjectModel | None = None
if resource_name:
self.resource = ReferencedObjectModel(resource_name)
self.resource = ReferencedObjectModel(resource_name, resource_href)
self.default_style: ReferencedObjectModel | None = None
if default_style_name:
self.default_style = ReferencedObjectModel(default_style_name)
Expand All@@ -35,6 +36,15 @@ def resource_name(self) -> str | None:
def default_style_name(self) -> str | None:
return self.default_style.name if self.default_style else None

@property
def all_style_names(self) -> set[str]:
all_styles = set()
if self.default_style_name is not None:
all_styles.add(self.default_style_name)
if self.styles is not None:
all_styles.update(self.styles)
return all_styles

@classmethod
def from_get_response_payload(cls, content: dict):
layer = content["layer"]
Expand All@@ -45,6 +55,7 @@ def from_get_response_payload(cls, content: dict):
return cls(
name=layer["name"],
resource_name=layer["resource"]["name"],
resource_href=layer["resource"]["href"],
type=layer["type"],
default_style_name=layer["defaultStyle"]["name"],
styles=styles,
Expand All@@ -59,11 +70,17 @@ def asdict(self) -> dict[str, Any]:
optional_items = {
"name": self.name,
"type": self.type,
"resource": self.resource_name,
"defaultStyle": self.default_style_name,
"defaultStyle": {
"name": self.default_style_name,
},
"attribution": self.attribution,
"queryable": self.queryable,
}
if self.resource is not None:
optional_items["resource"] = {
"name": self.resource.name,
"href": self.resource.href,
}
return EntityModel.add_items_to_dict(content, optional_items)

def post_payload(self) -> dict[str, dict[str, Any]]:
Expand Down
4 changes: 2 additions & 2 deletions geoservercloud/services/owsservice.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,11 +10,11 @@


class OwsService:
def __init__(self, url: str, auth: tuple[str, str]) -> None:
def __init__(self, url: str, auth: tuple[str, str], verifytls: bool = True) -> None:
self.url: str = url
self.auth: tuple[str, str] = auth
self.ows_endpoints = self.OwsEndpoints()
self.rest_client = RestClient(url, auth)
self.rest_client = RestClient(url, auth, verifytls)

def create_wms(self, workspace_name: str | None = None) -> WebMapService_1_3_0:
if workspace_name is None:
Expand Down
46 changes: 40 additions & 6 deletions geoservercloud/services/restclient.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,8 @@

import requests

from .restlogger import gs_logger

TIMEOUT = 120


Expand All@@ -17,22 +19,31 @@ class RestClient:
username and password for GeoServer
"""

def __init__(self, url: str, auth: tuple[str, str]) -> None:
def __init__(self, url: str, auth: tuple[str, str], verifytls: bool = True) -> None:
self.url: str = url
self.auth: tuple[str, str] = auth
self.verifytls: bool = verifytls

def get(
self,
path: str,
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.get(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 404:
response.raise_for_status()
Expand All@@ -46,15 +57,22 @@ def post(
json: dict[str, dict[str, Any] | Any] | None = None,
data: bytes | None = None,
) -> requests.Response:

full_url = f"{self.url}{path}"
response: requests.Response = requests.post(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
json=json,
data=data,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 409:
response.raise_for_status()
Expand All@@ -68,14 +86,22 @@ def put(
json: dict[str, dict[str, Any]] | None = None,
data: bytes | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.put(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
json=json,
data=data,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
response.raise_for_status()
return response
Expand All@@ -86,12 +112,20 @@ def delete(
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.delete(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 404:
response.raise_for_status()
Expand Down
12 changes: 12 additions & 0 deletions geoservercloud/services/restlogger.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import logging

time_formatter = logging.Formatter(
"{asctime} - {name}:{levelname} - {message}",
style="{",
datefmt="%Y-%m-%d %H:%M",
)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(time_formatter)
gs_logger = logging.getLogger("GS Session")
gs_logger.setLevel(logging.INFO)
gs_logger.addHandler(stream_handler)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Copy layers styles by mki-c2c · Pull Request #79 · camptocamp/python-geoservercloud · GitHub
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions geoservercloud/exceptions.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
from dataclasses import dataclass, field
from typing import Any

from requests import Request, Response


@dataclass
class GsDetail:
message: str
info: dict[str, Any] = field(default_factory=lambda: {})


class GsException(Exception):
def __init__(
self,
code: int,
detail: GsDetail,
parent_request: Request | None = None,
parent_response: Response | None = None,
):
super().__init__()
self.code = code
self.detail = detail
self.parent_request = parent_request
self.parent_response = parent_response


class AuthException(GsException):
pass


class DatastoreMissing(GsException):
pass


class WorkspaceMissing(GsException):
pass
5 changes: 3 additions & 2 deletions geoservercloud/geoservercloud.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,14 +37,15 @@ def __init__(
url: str = "http://localhost:9090/geoserver/cloud",
user: str = "admin",
password: str = "geoserver", # nosec
verifytls: bool = True,
) -> None:

self.url: str = url.strip("/")
self.user: str = user
self.password: str = password
self.auth: tuple[str, str] = (user, password)
self.rest_service: RestService = RestService(url, self.auth)
self.ows_service: OwsService = OwsService(url, self.auth)
self.rest_service: RestService = RestService(url, self.auth, verifytls)
self.ows_service: OwsService = OwsService(url, self.auth, verifytls)
self.wms: WebMapService_1_3_0 | None = None
self.wmts: WebMapTileService | None = None
self.default_workspace: str | None = None
Expand Down
98 changes: 83 additions & 15 deletions geoservercloud/geoservercloudsync.py
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
import json
from argparse import ArgumentParser

from requests.exceptions import HTTPError

from geoservercloud.exceptions import DatastoreMissing, GsDetail, WorkspaceMissing
from geoservercloud.models.layer import Layer
from geoservercloud.services import RestService


Expand DownExpand Up@@ -31,17 +36,23 @@ def __init__(
dst_url: str,
dst_user: str,
dst_password: str,
src_verifytls: bool = True,
dst_verifytls: bool = True,
) -> None:
self.src_url: str = src_url.strip("/")
self.src_user: str = src_user
self.src_password: str = src_password
self.src_auth: tuple[str, str] = (src_user, src_password)
self.src_instance: RestService = RestService(src_url, self.src_auth)
self.src_instance: RestService = RestService(
src_url, self.src_auth, src_verifytls
)
self.dst_url: str = dst_url.strip("/")
self.dst_user: str = dst_user
self.dst_password: str = dst_password
self.dst_auth: tuple[str, str] = (dst_user, dst_password)
self.dst_instance: RestService = RestService(dst_url, self.dst_auth)
self.dst_instance: RestService = RestService(
dst_url, self.dst_auth, src_verifytls
)

def copy_workspace(
self, workspace_name: str, deep_copy: bool = False
Expand DownExpand Up@@ -163,9 +174,47 @@ def copy_layer(
layer, status_code = self.src_instance.get_layer(
workspace_name, feature_type_name
)
if isinstance(layer, str):
return layer, status_code
return self.dst_instance.update_layer(layer, workspace_name)
if (status_code != 200) or isinstance(layer, str):
return f"Error: unexpected response {layer}", 500
resource, status_code = self.src_instance.get_resource_from_layer(layer)
layer_string = json.dumps(layer.asdict())
dst_layer = Layer.from_get_response_payload(
{
"layer": json.loads(
layer_string.replace(self.src_instance.url, self.dst_instance.url)
)
}
)

xml_resource_route = self.src_instance.get_resource_route_from_layer(layer)
if xml_resource_route is None:
return f"Error: cannot get resource route for {layer}", 500
try:
self.dst_instance.create_layer_resource(resource, xml_resource_route)
except HTTPError:
dst_resource = resource.decode().replace(
self.src_instance.url, self.dst_instance.url
)
store = json.loads(dst_resource)["featureType"]["store"]
if not self.dst_instance.check_href(store):
raise DatastoreMissing(
404,
GsDetail(
f"Datastore {store['name']} not found",
),
)
for ws_name, workspace in self.dst_instance.get_workspaces_from_store(
store
).items():
if not self.dst_instance.check_href(workspace):
raise WorkspaceMissing(
404,
GsDetail(
f"Workspace {ws_name} not found",
),
)

return self.dst_instance.update_layer(dst_layer, workspace_name)

def copy_layer_groups(self, workspace_name: str) -> tuple[str, int]:
"""
Expand DownExpand Up@@ -228,20 +277,39 @@ def copy_style(
"""
Copy a style from source to destination GeoServer instance
"""
style_definition, status_code = self.src_instance.get_style_definition(
style_info, status_code = self.src_instance.get_style_info(
style_name, workspace_name
)
if isinstance(style_definition, str):
return style_definition, status_code
content, status_code = self.dst_instance.create_style_definition(
style_name, style_definition, workspace_name
)
if self.not_ok(status_code):
return f"Error getting {style_info}", status_code
style_definition_response = self.src_instance.get_raw_style_definition(
style_name, workspace_name, style_info.format
)
if self.not_ok(style_definition_response.status_code):
content: str = style_definition_response.content.decode()
return content, status_code
style, status_code = self.src_instance.get_style(style_name, workspace_name)
if isinstance(style, str):
return style, status_code
return self.dst_instance.create_style(style_name, style, workspace_name)
try:
content, status_code = self.dst_instance.create_style_info(
style_name, style_info, workspace_name
)
if self.not_ok(status_code):
return content, status_code
except HTTPError:
if not self.dst_instance.check_workspace_for_style(style_info):
raise WorkspaceMissing(
404,
GsDetail(
f"Workspace {style_info.workspace_name} not found",
),
)

return self.dst_instance.fill_style_definition(
style_name,
style_definition_response.content,
style_info.format,
style_definition_response.headers["content-type"],
workspace_name,
)

def copy_style_images(self, workspace_name: str | None = None) -> tuple[str, int]:
"""
Expand Down
23 changes: 20 additions & 3 deletions geoservercloud/models/layer.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ def __init__(
self,
name: str,
resource_name: str | None = None,
resource_href: str | None = None,
type: str | None = None,
default_style_name: str | None = None,
styles: list | None = None,
Expand All@@ -19,7 +20,7 @@ def __init__(
self.type: str | None = type
self.resource: ReferencedObjectModel | None = None
if resource_name:
self.resource = ReferencedObjectModel(resource_name)
self.resource = ReferencedObjectModel(resource_name, resource_href)
self.default_style: ReferencedObjectModel | None = None
if default_style_name:
self.default_style = ReferencedObjectModel(default_style_name)
Expand All@@ -35,6 +36,15 @@ def resource_name(self) -> str | None:
def default_style_name(self) -> str | None:
return self.default_style.name if self.default_style else None

@property
def all_style_names(self) -> set[str]:
all_styles = set()
if self.default_style_name is not None:
all_styles.add(self.default_style_name)
if self.styles is not None:
all_styles.update(self.styles)
return all_styles

@classmethod
def from_get_response_payload(cls, content: dict):
layer = content["layer"]
Expand All@@ -45,6 +55,7 @@ def from_get_response_payload(cls, content: dict):
return cls(
name=layer["name"],
resource_name=layer["resource"]["name"],
resource_href=layer["resource"]["href"],
type=layer["type"],
default_style_name=layer["defaultStyle"]["name"],
styles=styles,
Expand All@@ -59,11 +70,17 @@ def asdict(self) -> dict[str, Any]:
optional_items = {
"name": self.name,
"type": self.type,
"resource": self.resource_name,
"defaultStyle": self.default_style_name,
"defaultStyle": {
"name": self.default_style_name,
},
"attribution": self.attribution,
"queryable": self.queryable,
}
if self.resource is not None:
optional_items["resource"] = {
"name": self.resource.name,
"href": self.resource.href,
}
return EntityModel.add_items_to_dict(content, optional_items)

def post_payload(self) -> dict[str, dict[str, Any]]:
Expand Down
4 changes: 2 additions & 2 deletions geoservercloud/services/owsservice.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,11 +10,11 @@


class OwsService:
def __init__(self, url: str, auth: tuple[str, str]) -> None:
def __init__(self, url: str, auth: tuple[str, str], verifytls: bool = True) -> None:
self.url: str = url
self.auth: tuple[str, str] = auth
self.ows_endpoints = self.OwsEndpoints()
self.rest_client = RestClient(url, auth)
self.rest_client = RestClient(url, auth, verifytls)

def create_wms(self, workspace_name: str | None = None) -> WebMapService_1_3_0:
if workspace_name is None:
Expand Down
46 changes: 40 additions & 6 deletions geoservercloud/services/restclient.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,8 @@

import requests

from .restlogger import gs_logger

TIMEOUT = 120


Expand All@@ -17,22 +19,31 @@ class RestClient:
username and password for GeoServer
"""

def __init__(self, url: str, auth: tuple[str, str]) -> None:
def __init__(self, url: str, auth: tuple[str, str], verifytls: bool = True) -> None:
self.url: str = url
self.auth: tuple[str, str] = auth
self.verifytls: bool = verifytls

def get(
self,
path: str,
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.get(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 404:
response.raise_for_status()
Expand All@@ -46,15 +57,22 @@ def post(
json: dict[str, dict[str, Any] | Any] | None = None,
data: bytes | None = None,
) -> requests.Response:

full_url = f"{self.url}{path}"
response: requests.Response = requests.post(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
json=json,
data=data,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 409:
response.raise_for_status()
Expand All@@ -68,14 +86,22 @@ def put(
json: dict[str, dict[str, Any]] | None = None,
data: bytes | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.put(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
json=json,
data=data,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
response.raise_for_status()
return response
Expand All@@ -86,12 +112,20 @@ def delete(
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.delete(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 404:
response.raise_for_status()
Expand Down
12 changes: 12 additions & 0 deletions geoservercloud/services/restlogger.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import logging

time_formatter = logging.Formatter(
"{asctime} - {name}:{levelname} - {message}",
style="{",
datefmt="%Y-%m-%d %H:%M",
)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(time_formatter)
gs_logger = logging.getLogger("GS Session")
gs_logger.setLevel(logging.INFO)
gs_logger.addHandler(stream_handler)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Copy layers styles by mki-c2c · Pull Request #79 · camptocamp/python-geoservercloud · GitHub
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions geoservercloud/exceptions.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
from dataclasses import dataclass, field
from typing import Any

from requests import Request, Response


@dataclass
class GsDetail:
message: str
info: dict[str, Any] = field(default_factory=lambda: {})


class GsException(Exception):
def __init__(
self,
code: int,
detail: GsDetail,
parent_request: Request | None = None,
parent_response: Response | None = None,
):
super().__init__()
self.code = code
self.detail = detail
self.parent_request = parent_request
self.parent_response = parent_response


class AuthException(GsException):
pass


class DatastoreMissing(GsException):
pass


class WorkspaceMissing(GsException):
pass
5 changes: 3 additions & 2 deletions geoservercloud/geoservercloud.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,14 +37,15 @@ def __init__(
url: str = "http://localhost:9090/geoserver/cloud",
user: str = "admin",
password: str = "geoserver", # nosec
verifytls: bool = True,
) -> None:

self.url: str = url.strip("/")
self.user: str = user
self.password: str = password
self.auth: tuple[str, str] = (user, password)
self.rest_service: RestService = RestService(url, self.auth)
self.ows_service: OwsService = OwsService(url, self.auth)
self.rest_service: RestService = RestService(url, self.auth, verifytls)
self.ows_service: OwsService = OwsService(url, self.auth, verifytls)
self.wms: WebMapService_1_3_0 | None = None
self.wmts: WebMapTileService | None = None
self.default_workspace: str | None = None
Expand Down
98 changes: 83 additions & 15 deletions geoservercloud/geoservercloudsync.py
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
import json
from argparse import ArgumentParser

from requests.exceptions import HTTPError

from geoservercloud.exceptions import DatastoreMissing, GsDetail, WorkspaceMissing
from geoservercloud.models.layer import Layer
from geoservercloud.services import RestService


Expand DownExpand Up@@ -31,17 +36,23 @@ def __init__(
dst_url: str,
dst_user: str,
dst_password: str,
src_verifytls: bool = True,
dst_verifytls: bool = True,
) -> None:
self.src_url: str = src_url.strip("/")
self.src_user: str = src_user
self.src_password: str = src_password
self.src_auth: tuple[str, str] = (src_user, src_password)
self.src_instance: RestService = RestService(src_url, self.src_auth)
self.src_instance: RestService = RestService(
src_url, self.src_auth, src_verifytls
)
self.dst_url: str = dst_url.strip("/")
self.dst_user: str = dst_user
self.dst_password: str = dst_password
self.dst_auth: tuple[str, str] = (dst_user, dst_password)
self.dst_instance: RestService = RestService(dst_url, self.dst_auth)
self.dst_instance: RestService = RestService(
dst_url, self.dst_auth, src_verifytls
)

def copy_workspace(
self, workspace_name: str, deep_copy: bool = False
Expand DownExpand Up@@ -163,9 +174,47 @@ def copy_layer(
layer, status_code = self.src_instance.get_layer(
workspace_name, feature_type_name
)
if isinstance(layer, str):
return layer, status_code
return self.dst_instance.update_layer(layer, workspace_name)
if (status_code != 200) or isinstance(layer, str):
return f"Error: unexpected response {layer}", 500
resource, status_code = self.src_instance.get_resource_from_layer(layer)
layer_string = json.dumps(layer.asdict())
dst_layer = Layer.from_get_response_payload(
{
"layer": json.loads(
layer_string.replace(self.src_instance.url, self.dst_instance.url)
)
}
)

xml_resource_route = self.src_instance.get_resource_route_from_layer(layer)
if xml_resource_route is None:
return f"Error: cannot get resource route for {layer}", 500
try:
self.dst_instance.create_layer_resource(resource, xml_resource_route)
except HTTPError:
dst_resource = resource.decode().replace(
self.src_instance.url, self.dst_instance.url
)
store = json.loads(dst_resource)["featureType"]["store"]
if not self.dst_instance.check_href(store):
raise DatastoreMissing(
404,
GsDetail(
f"Datastore {store['name']} not found",
),
)
for ws_name, workspace in self.dst_instance.get_workspaces_from_store(
store
).items():
if not self.dst_instance.check_href(workspace):
raise WorkspaceMissing(
404,
GsDetail(
f"Workspace {ws_name} not found",
),
)

return self.dst_instance.update_layer(dst_layer, workspace_name)

def copy_layer_groups(self, workspace_name: str) -> tuple[str, int]:
"""
Expand DownExpand Up@@ -228,20 +277,39 @@ def copy_style(
"""
Copy a style from source to destination GeoServer instance
"""
style_definition, status_code = self.src_instance.get_style_definition(
style_info, status_code = self.src_instance.get_style_info(
style_name, workspace_name
)
if isinstance(style_definition, str):
return style_definition, status_code
content, status_code = self.dst_instance.create_style_definition(
style_name, style_definition, workspace_name
)
if self.not_ok(status_code):
return f"Error getting {style_info}", status_code
style_definition_response = self.src_instance.get_raw_style_definition(
style_name, workspace_name, style_info.format
)
if self.not_ok(style_definition_response.status_code):
content: str = style_definition_response.content.decode()
return content, status_code
style, status_code = self.src_instance.get_style(style_name, workspace_name)
if isinstance(style, str):
return style, status_code
return self.dst_instance.create_style(style_name, style, workspace_name)
try:
content, status_code = self.dst_instance.create_style_info(
style_name, style_info, workspace_name
)
if self.not_ok(status_code):
return content, status_code
except HTTPError:
if not self.dst_instance.check_workspace_for_style(style_info):
raise WorkspaceMissing(
404,
GsDetail(
f"Workspace {style_info.workspace_name} not found",
),
)

return self.dst_instance.fill_style_definition(
style_name,
style_definition_response.content,
style_info.format,
style_definition_response.headers["content-type"],
workspace_name,
)

def copy_style_images(self, workspace_name: str | None = None) -> tuple[str, int]:
"""
Expand Down
23 changes: 20 additions & 3 deletions geoservercloud/models/layer.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ def __init__(
self,
name: str,
resource_name: str | None = None,
resource_href: str | None = None,
type: str | None = None,
default_style_name: str | None = None,
styles: list | None = None,
Expand All@@ -19,7 +20,7 @@ def __init__(
self.type: str | None = type
self.resource: ReferencedObjectModel | None = None
if resource_name:
self.resource = ReferencedObjectModel(resource_name)
self.resource = ReferencedObjectModel(resource_name, resource_href)
self.default_style: ReferencedObjectModel | None = None
if default_style_name:
self.default_style = ReferencedObjectModel(default_style_name)
Expand All@@ -35,6 +36,15 @@ def resource_name(self) -> str | None:
def default_style_name(self) -> str | None:
return self.default_style.name if self.default_style else None

@property
def all_style_names(self) -> set[str]:
all_styles = set()
if self.default_style_name is not None:
all_styles.add(self.default_style_name)
if self.styles is not None:
all_styles.update(self.styles)
return all_styles

@classmethod
def from_get_response_payload(cls, content: dict):
layer = content["layer"]
Expand All@@ -45,6 +55,7 @@ def from_get_response_payload(cls, content: dict):
return cls(
name=layer["name"],
resource_name=layer["resource"]["name"],
resource_href=layer["resource"]["href"],
type=layer["type"],
default_style_name=layer["defaultStyle"]["name"],
styles=styles,
Expand All@@ -59,11 +70,17 @@ def asdict(self) -> dict[str, Any]:
optional_items = {
"name": self.name,
"type": self.type,
"resource": self.resource_name,
"defaultStyle": self.default_style_name,
"defaultStyle": {
"name": self.default_style_name,
},
"attribution": self.attribution,
"queryable": self.queryable,
}
if self.resource is not None:
optional_items["resource"] = {
"name": self.resource.name,
"href": self.resource.href,
}
return EntityModel.add_items_to_dict(content, optional_items)

def post_payload(self) -> dict[str, dict[str, Any]]:
Expand Down
4 changes: 2 additions & 2 deletions geoservercloud/services/owsservice.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,11 +10,11 @@


class OwsService:
def __init__(self, url: str, auth: tuple[str, str]) -> None:
def __init__(self, url: str, auth: tuple[str, str], verifytls: bool = True) -> None:
self.url: str = url
self.auth: tuple[str, str] = auth
self.ows_endpoints = self.OwsEndpoints()
self.rest_client = RestClient(url, auth)
self.rest_client = RestClient(url, auth, verifytls)

def create_wms(self, workspace_name: str | None = None) -> WebMapService_1_3_0:
if workspace_name is None:
Expand Down
46 changes: 40 additions & 6 deletions geoservercloud/services/restclient.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,8 @@

import requests

from .restlogger import gs_logger

TIMEOUT = 120


Expand All@@ -17,22 +19,31 @@ class RestClient:
username and password for GeoServer
"""

def __init__(self, url: str, auth: tuple[str, str]) -> None:
def __init__(self, url: str, auth: tuple[str, str], verifytls: bool = True) -> None:
self.url: str = url
self.auth: tuple[str, str] = auth
self.verifytls: bool = verifytls

def get(
self,
path: str,
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.get(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 404:
response.raise_for_status()
Expand All@@ -46,15 +57,22 @@ def post(
json: dict[str, dict[str, Any] | Any] | None = None,
data: bytes | None = None,
) -> requests.Response:

full_url = f"{self.url}{path}"
response: requests.Response = requests.post(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
json=json,
data=data,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 409:
response.raise_for_status()
Expand All@@ -68,14 +86,22 @@ def put(
json: dict[str, dict[str, Any]] | None = None,
data: bytes | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.put(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
json=json,
data=data,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
response.raise_for_status()
return response
Expand All@@ -86,12 +112,20 @@ def delete(
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.delete(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 404:
response.raise_for_status()
Expand Down
12 changes: 12 additions & 0 deletions geoservercloud/services/restlogger.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import logging

time_formatter = logging.Formatter(
"{asctime} - {name}:{levelname} - {message}",
style="{",
datefmt="%Y-%m-%d %H:%M",
)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(time_formatter)
gs_logger = logging.getLogger("GS Session")
gs_logger.setLevel(logging.INFO)
gs_logger.addHandler(stream_handler)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Copy layers styles by mki-c2c · Pull Request #79 · camptocamp/python-geoservercloud · GitHub
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions geoservercloud/exceptions.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
from dataclasses import dataclass, field
from typing import Any

from requests import Request, Response


@dataclass
class GsDetail:
message: str
info: dict[str, Any] = field(default_factory=lambda: {})


class GsException(Exception):
def __init__(
self,
code: int,
detail: GsDetail,
parent_request: Request | None = None,
parent_response: Response | None = None,
):
super().__init__()
self.code = code
self.detail = detail
self.parent_request = parent_request
self.parent_response = parent_response


class AuthException(GsException):
pass


class DatastoreMissing(GsException):
pass


class WorkspaceMissing(GsException):
pass
5 changes: 3 additions & 2 deletions geoservercloud/geoservercloud.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,14 +37,15 @@ def __init__(
url: str = "http://localhost:9090/geoserver/cloud",
user: str = "admin",
password: str = "geoserver", # nosec
verifytls: bool = True,
) -> None:

self.url: str = url.strip("/")
self.user: str = user
self.password: str = password
self.auth: tuple[str, str] = (user, password)
self.rest_service: RestService = RestService(url, self.auth)
self.ows_service: OwsService = OwsService(url, self.auth)
self.rest_service: RestService = RestService(url, self.auth, verifytls)
self.ows_service: OwsService = OwsService(url, self.auth, verifytls)
self.wms: WebMapService_1_3_0 | None = None
self.wmts: WebMapTileService | None = None
self.default_workspace: str | None = None
Expand Down
98 changes: 83 additions & 15 deletions geoservercloud/geoservercloudsync.py
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
import json
from argparse import ArgumentParser

from requests.exceptions import HTTPError

from geoservercloud.exceptions import DatastoreMissing, GsDetail, WorkspaceMissing
from geoservercloud.models.layer import Layer
from geoservercloud.services import RestService


Expand DownExpand Up@@ -31,17 +36,23 @@ def __init__(
dst_url: str,
dst_user: str,
dst_password: str,
src_verifytls: bool = True,
dst_verifytls: bool = True,
) -> None:
self.src_url: str = src_url.strip("/")
self.src_user: str = src_user
self.src_password: str = src_password
self.src_auth: tuple[str, str] = (src_user, src_password)
self.src_instance: RestService = RestService(src_url, self.src_auth)
self.src_instance: RestService = RestService(
src_url, self.src_auth, src_verifytls
)
self.dst_url: str = dst_url.strip("/")
self.dst_user: str = dst_user
self.dst_password: str = dst_password
self.dst_auth: tuple[str, str] = (dst_user, dst_password)
self.dst_instance: RestService = RestService(dst_url, self.dst_auth)
self.dst_instance: RestService = RestService(
dst_url, self.dst_auth, src_verifytls
)

def copy_workspace(
self, workspace_name: str, deep_copy: bool = False
Expand DownExpand Up@@ -163,9 +174,47 @@ def copy_layer(
layer, status_code = self.src_instance.get_layer(
workspace_name, feature_type_name
)
if isinstance(layer, str):
return layer, status_code
return self.dst_instance.update_layer(layer, workspace_name)
if (status_code != 200) or isinstance(layer, str):
return f"Error: unexpected response {layer}", 500
resource, status_code = self.src_instance.get_resource_from_layer(layer)
layer_string = json.dumps(layer.asdict())
dst_layer = Layer.from_get_response_payload(
{
"layer": json.loads(
layer_string.replace(self.src_instance.url, self.dst_instance.url)
)
}
)

xml_resource_route = self.src_instance.get_resource_route_from_layer(layer)
if xml_resource_route is None:
return f"Error: cannot get resource route for {layer}", 500
try:
self.dst_instance.create_layer_resource(resource, xml_resource_route)
except HTTPError:
dst_resource = resource.decode().replace(
self.src_instance.url, self.dst_instance.url
)
store = json.loads(dst_resource)["featureType"]["store"]
if not self.dst_instance.check_href(store):
raise DatastoreMissing(
404,
GsDetail(
f"Datastore {store['name']} not found",
),
)
for ws_name, workspace in self.dst_instance.get_workspaces_from_store(
store
).items():
if not self.dst_instance.check_href(workspace):
raise WorkspaceMissing(
404,
GsDetail(
f"Workspace {ws_name} not found",
),
)

return self.dst_instance.update_layer(dst_layer, workspace_name)

def copy_layer_groups(self, workspace_name: str) -> tuple[str, int]:
"""
Expand DownExpand Up@@ -228,20 +277,39 @@ def copy_style(
"""
Copy a style from source to destination GeoServer instance
"""
style_definition, status_code = self.src_instance.get_style_definition(
style_info, status_code = self.src_instance.get_style_info(
style_name, workspace_name
)
if isinstance(style_definition, str):
return style_definition, status_code
content, status_code = self.dst_instance.create_style_definition(
style_name, style_definition, workspace_name
)
if self.not_ok(status_code):
return f"Error getting {style_info}", status_code
style_definition_response = self.src_instance.get_raw_style_definition(
style_name, workspace_name, style_info.format
)
if self.not_ok(style_definition_response.status_code):
content: str = style_definition_response.content.decode()
return content, status_code
style, status_code = self.src_instance.get_style(style_name, workspace_name)
if isinstance(style, str):
return style, status_code
return self.dst_instance.create_style(style_name, style, workspace_name)
try:
content, status_code = self.dst_instance.create_style_info(
style_name, style_info, workspace_name
)
if self.not_ok(status_code):
return content, status_code
except HTTPError:
if not self.dst_instance.check_workspace_for_style(style_info):
raise WorkspaceMissing(
404,
GsDetail(
f"Workspace {style_info.workspace_name} not found",
),
)

return self.dst_instance.fill_style_definition(
style_name,
style_definition_response.content,
style_info.format,
style_definition_response.headers["content-type"],
workspace_name,
)

def copy_style_images(self, workspace_name: str | None = None) -> tuple[str, int]:
"""
Expand Down
23 changes: 20 additions & 3 deletions geoservercloud/models/layer.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ def __init__(
self,
name: str,
resource_name: str | None = None,
resource_href: str | None = None,
type: str | None = None,
default_style_name: str | None = None,
styles: list | None = None,
Expand All@@ -19,7 +20,7 @@ def __init__(
self.type: str | None = type
self.resource: ReferencedObjectModel | None = None
if resource_name:
self.resource = ReferencedObjectModel(resource_name)
self.resource = ReferencedObjectModel(resource_name, resource_href)
self.default_style: ReferencedObjectModel | None = None
if default_style_name:
self.default_style = ReferencedObjectModel(default_style_name)
Expand All@@ -35,6 +36,15 @@ def resource_name(self) -> str | None:
def default_style_name(self) -> str | None:
return self.default_style.name if self.default_style else None

@property
def all_style_names(self) -> set[str]:
all_styles = set()
if self.default_style_name is not None:
all_styles.add(self.default_style_name)
if self.styles is not None:
all_styles.update(self.styles)
return all_styles

@classmethod
def from_get_response_payload(cls, content: dict):
layer = content["layer"]
Expand All@@ -45,6 +55,7 @@ def from_get_response_payload(cls, content: dict):
return cls(
name=layer["name"],
resource_name=layer["resource"]["name"],
resource_href=layer["resource"]["href"],
type=layer["type"],
default_style_name=layer["defaultStyle"]["name"],
styles=styles,
Expand All@@ -59,11 +70,17 @@ def asdict(self) -> dict[str, Any]:
optional_items = {
"name": self.name,
"type": self.type,
"resource": self.resource_name,
"defaultStyle": self.default_style_name,
"defaultStyle": {
"name": self.default_style_name,
},
"attribution": self.attribution,
"queryable": self.queryable,
}
if self.resource is not None:
optional_items["resource"] = {
"name": self.resource.name,
"href": self.resource.href,
}
return EntityModel.add_items_to_dict(content, optional_items)

def post_payload(self) -> dict[str, dict[str, Any]]:
Expand Down
4 changes: 2 additions & 2 deletions geoservercloud/services/owsservice.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,11 +10,11 @@


class OwsService:
def __init__(self, url: str, auth: tuple[str, str]) -> None:
def __init__(self, url: str, auth: tuple[str, str], verifytls: bool = True) -> None:
self.url: str = url
self.auth: tuple[str, str] = auth
self.ows_endpoints = self.OwsEndpoints()
self.rest_client = RestClient(url, auth)
self.rest_client = RestClient(url, auth, verifytls)

def create_wms(self, workspace_name: str | None = None) -> WebMapService_1_3_0:
if workspace_name is None:
Expand Down
46 changes: 40 additions & 6 deletions geoservercloud/services/restclient.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,8 @@

import requests

from .restlogger import gs_logger

TIMEOUT = 120


Expand All@@ -17,22 +19,31 @@ class RestClient:
username and password for GeoServer
"""

def __init__(self, url: str, auth: tuple[str, str]) -> None:
def __init__(self, url: str, auth: tuple[str, str], verifytls: bool = True) -> None:
self.url: str = url
self.auth: tuple[str, str] = auth
self.verifytls: bool = verifytls

def get(
self,
path: str,
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.get(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 404:
response.raise_for_status()
Expand All@@ -46,15 +57,22 @@ def post(
json: dict[str, dict[str, Any] | Any] | None = None,
data: bytes | None = None,
) -> requests.Response:

full_url = f"{self.url}{path}"
response: requests.Response = requests.post(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
json=json,
data=data,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 409:
response.raise_for_status()
Expand All@@ -68,14 +86,22 @@ def put(
json: dict[str, dict[str, Any]] | None = None,
data: bytes | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.put(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
json=json,
data=data,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
response.raise_for_status()
return response
Expand All@@ -86,12 +112,20 @@ def delete(
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.delete(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 404:
response.raise_for_status()
Expand Down
12 changes: 12 additions & 0 deletions geoservercloud/services/restlogger.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import logging

time_formatter = logging.Formatter(
"{asctime} - {name}:{levelname} - {message}",
style="{",
datefmt="%Y-%m-%d %H:%M",
)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(time_formatter)
gs_logger = logging.getLogger("GS Session")
gs_logger.setLevel(logging.INFO)
gs_logger.addHandler(stream_handler)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Copy layers styles by mki-c2c · Pull Request #79 · camptocamp/python-geoservercloud · GitHub
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions geoservercloud/exceptions.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
from dataclasses import dataclass, field
from typing import Any

from requests import Request, Response


@dataclass
class GsDetail:
message: str
info: dict[str, Any] = field(default_factory=lambda: {})


class GsException(Exception):
def __init__(
self,
code: int,
detail: GsDetail,
parent_request: Request | None = None,
parent_response: Response | None = None,
):
super().__init__()
self.code = code
self.detail = detail
self.parent_request = parent_request
self.parent_response = parent_response


class AuthException(GsException):
pass


class DatastoreMissing(GsException):
pass


class WorkspaceMissing(GsException):
pass
5 changes: 3 additions & 2 deletions geoservercloud/geoservercloud.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,14 +37,15 @@ def __init__(
url: str = "http://localhost:9090/geoserver/cloud",
user: str = "admin",
password: str = "geoserver", # nosec
verifytls: bool = True,
) -> None:

self.url: str = url.strip("/")
self.user: str = user
self.password: str = password
self.auth: tuple[str, str] = (user, password)
self.rest_service: RestService = RestService(url, self.auth)
self.ows_service: OwsService = OwsService(url, self.auth)
self.rest_service: RestService = RestService(url, self.auth, verifytls)
self.ows_service: OwsService = OwsService(url, self.auth, verifytls)
self.wms: WebMapService_1_3_0 | None = None
self.wmts: WebMapTileService | None = None
self.default_workspace: str | None = None
Expand Down
98 changes: 83 additions & 15 deletions geoservercloud/geoservercloudsync.py
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
import json
from argparse import ArgumentParser

from requests.exceptions import HTTPError

from geoservercloud.exceptions import DatastoreMissing, GsDetail, WorkspaceMissing
from geoservercloud.models.layer import Layer
from geoservercloud.services import RestService


Expand DownExpand Up@@ -31,17 +36,23 @@ def __init__(
dst_url: str,
dst_user: str,
dst_password: str,
src_verifytls: bool = True,
dst_verifytls: bool = True,
) -> None:
self.src_url: str = src_url.strip("/")
self.src_user: str = src_user
self.src_password: str = src_password
self.src_auth: tuple[str, str] = (src_user, src_password)
self.src_instance: RestService = RestService(src_url, self.src_auth)
self.src_instance: RestService = RestService(
src_url, self.src_auth, src_verifytls
)
self.dst_url: str = dst_url.strip("/")
self.dst_user: str = dst_user
self.dst_password: str = dst_password
self.dst_auth: tuple[str, str] = (dst_user, dst_password)
self.dst_instance: RestService = RestService(dst_url, self.dst_auth)
self.dst_instance: RestService = RestService(
dst_url, self.dst_auth, src_verifytls
)

def copy_workspace(
self, workspace_name: str, deep_copy: bool = False
Expand DownExpand Up@@ -163,9 +174,47 @@ def copy_layer(
layer, status_code = self.src_instance.get_layer(
workspace_name, feature_type_name
)
if isinstance(layer, str):
return layer, status_code
return self.dst_instance.update_layer(layer, workspace_name)
if (status_code != 200) or isinstance(layer, str):
return f"Error: unexpected response {layer}", 500
resource, status_code = self.src_instance.get_resource_from_layer(layer)
layer_string = json.dumps(layer.asdict())
dst_layer = Layer.from_get_response_payload(
{
"layer": json.loads(
layer_string.replace(self.src_instance.url, self.dst_instance.url)
)
}
)

xml_resource_route = self.src_instance.get_resource_route_from_layer(layer)
if xml_resource_route is None:
return f"Error: cannot get resource route for {layer}", 500
try:
self.dst_instance.create_layer_resource(resource, xml_resource_route)
except HTTPError:
dst_resource = resource.decode().replace(
self.src_instance.url, self.dst_instance.url
)
store = json.loads(dst_resource)["featureType"]["store"]
if not self.dst_instance.check_href(store):
raise DatastoreMissing(
404,
GsDetail(
f"Datastore {store['name']} not found",
),
)
for ws_name, workspace in self.dst_instance.get_workspaces_from_store(
store
).items():
if not self.dst_instance.check_href(workspace):
raise WorkspaceMissing(
404,
GsDetail(
f"Workspace {ws_name} not found",
),
)

return self.dst_instance.update_layer(dst_layer, workspace_name)

def copy_layer_groups(self, workspace_name: str) -> tuple[str, int]:
"""
Expand DownExpand Up@@ -228,20 +277,39 @@ def copy_style(
"""
Copy a style from source to destination GeoServer instance
"""
style_definition, status_code = self.src_instance.get_style_definition(
style_info, status_code = self.src_instance.get_style_info(
style_name, workspace_name
)
if isinstance(style_definition, str):
return style_definition, status_code
content, status_code = self.dst_instance.create_style_definition(
style_name, style_definition, workspace_name
)
if self.not_ok(status_code):
return f"Error getting {style_info}", status_code
style_definition_response = self.src_instance.get_raw_style_definition(
style_name, workspace_name, style_info.format
)
if self.not_ok(style_definition_response.status_code):
content: str = style_definition_response.content.decode()
return content, status_code
style, status_code = self.src_instance.get_style(style_name, workspace_name)
if isinstance(style, str):
return style, status_code
return self.dst_instance.create_style(style_name, style, workspace_name)
try:
content, status_code = self.dst_instance.create_style_info(
style_name, style_info, workspace_name
)
if self.not_ok(status_code):
return content, status_code
except HTTPError:
if not self.dst_instance.check_workspace_for_style(style_info):
raise WorkspaceMissing(
404,
GsDetail(
f"Workspace {style_info.workspace_name} not found",
),
)

return self.dst_instance.fill_style_definition(
style_name,
style_definition_response.content,
style_info.format,
style_definition_response.headers["content-type"],
workspace_name,
)

def copy_style_images(self, workspace_name: str | None = None) -> tuple[str, int]:
"""
Expand Down
23 changes: 20 additions & 3 deletions geoservercloud/models/layer.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ def __init__(
self,
name: str,
resource_name: str | None = None,
resource_href: str | None = None,
type: str | None = None,
default_style_name: str | None = None,
styles: list | None = None,
Expand All@@ -19,7 +20,7 @@ def __init__(
self.type: str | None = type
self.resource: ReferencedObjectModel | None = None
if resource_name:
self.resource = ReferencedObjectModel(resource_name)
self.resource = ReferencedObjectModel(resource_name, resource_href)
self.default_style: ReferencedObjectModel | None = None
if default_style_name:
self.default_style = ReferencedObjectModel(default_style_name)
Expand All@@ -35,6 +36,15 @@ def resource_name(self) -> str | None:
def default_style_name(self) -> str | None:
return self.default_style.name if self.default_style else None

@property
def all_style_names(self) -> set[str]:
all_styles = set()
if self.default_style_name is not None:
all_styles.add(self.default_style_name)
if self.styles is not None:
all_styles.update(self.styles)
return all_styles

@classmethod
def from_get_response_payload(cls, content: dict):
layer = content["layer"]
Expand All@@ -45,6 +55,7 @@ def from_get_response_payload(cls, content: dict):
return cls(
name=layer["name"],
resource_name=layer["resource"]["name"],
resource_href=layer["resource"]["href"],
type=layer["type"],
default_style_name=layer["defaultStyle"]["name"],
styles=styles,
Expand All@@ -59,11 +70,17 @@ def asdict(self) -> dict[str, Any]:
optional_items = {
"name": self.name,
"type": self.type,
"resource": self.resource_name,
"defaultStyle": self.default_style_name,
"defaultStyle": {
"name": self.default_style_name,
},
"attribution": self.attribution,
"queryable": self.queryable,
}
if self.resource is not None:
optional_items["resource"] = {
"name": self.resource.name,
"href": self.resource.href,
}
return EntityModel.add_items_to_dict(content, optional_items)

def post_payload(self) -> dict[str, dict[str, Any]]:
Expand Down
4 changes: 2 additions & 2 deletions geoservercloud/services/owsservice.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,11 +10,11 @@


class OwsService:
def __init__(self, url: str, auth: tuple[str, str]) -> None:
def __init__(self, url: str, auth: tuple[str, str], verifytls: bool = True) -> None:
self.url: str = url
self.auth: tuple[str, str] = auth
self.ows_endpoints = self.OwsEndpoints()
self.rest_client = RestClient(url, auth)
self.rest_client = RestClient(url, auth, verifytls)

def create_wms(self, workspace_name: str | None = None) -> WebMapService_1_3_0:
if workspace_name is None:
Expand Down
46 changes: 40 additions & 6 deletions geoservercloud/services/restclient.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,8 @@

import requests

from .restlogger import gs_logger

TIMEOUT = 120


Expand All@@ -17,22 +19,31 @@ class RestClient:
username and password for GeoServer
"""

def __init__(self, url: str, auth: tuple[str, str]) -> None:
def __init__(self, url: str, auth: tuple[str, str], verifytls: bool = True) -> None:
self.url: str = url
self.auth: tuple[str, str] = auth
self.verifytls: bool = verifytls

def get(
self,
path: str,
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.get(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 404:
response.raise_for_status()
Expand All@@ -46,15 +57,22 @@ def post(
json: dict[str, dict[str, Any] | Any] | None = None,
data: bytes | None = None,
) -> requests.Response:

full_url = f"{self.url}{path}"
response: requests.Response = requests.post(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
json=json,
data=data,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 409:
response.raise_for_status()
Expand All@@ -68,14 +86,22 @@ def put(
json: dict[str, dict[str, Any]] | None = None,
data: bytes | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.put(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
json=json,
data=data,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
response.raise_for_status()
return response
Expand All@@ -86,12 +112,20 @@ def delete(
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.delete(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 404:
response.raise_for_status()
Expand Down
12 changes: 12 additions & 0 deletions geoservercloud/services/restlogger.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import logging

time_formatter = logging.Formatter(
"{asctime} - {name}:{levelname} - {message}",
style="{",
datefmt="%Y-%m-%d %H:%M",
)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(time_formatter)
gs_logger = logging.getLogger("GS Session")
gs_logger.setLevel(logging.INFO)
gs_logger.addHandler(stream_handler)
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Copy layers styles by mki-c2c · Pull Request #79 · camptocamp/python-geoservercloud · GitHub
Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions geoservercloud/exceptions.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
from dataclasses import dataclass, field
from typing import Any

from requests import Request, Response


@dataclass
class GsDetail:
message: str
info: dict[str, Any] = field(default_factory=lambda: {})


class GsException(Exception):
def __init__(
self,
code: int,
detail: GsDetail,
parent_request: Request | None = None,
parent_response: Response | None = None,
):
super().__init__()
self.code = code
self.detail = detail
self.parent_request = parent_request
self.parent_response = parent_response


class AuthException(GsException):
pass


class DatastoreMissing(GsException):
pass


class WorkspaceMissing(GsException):
pass
5 changes: 3 additions & 2 deletions geoservercloud/geoservercloud.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,14 +37,15 @@ def __init__(
url: str = "http://localhost:9090/geoserver/cloud",
user: str = "admin",
password: str = "geoserver", # nosec
verifytls: bool = True,
) -> None:

self.url: str = url.strip("/")
self.user: str = user
self.password: str = password
self.auth: tuple[str, str] = (user, password)
self.rest_service: RestService = RestService(url, self.auth)
self.ows_service: OwsService = OwsService(url, self.auth)
self.rest_service: RestService = RestService(url, self.auth, verifytls)
self.ows_service: OwsService = OwsService(url, self.auth, verifytls)
self.wms: WebMapService_1_3_0 | None = None
self.wmts: WebMapTileService | None = None
self.default_workspace: str | None = None
Expand Down
98 changes: 83 additions & 15 deletions geoservercloud/geoservercloudsync.py
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
import json
from argparse import ArgumentParser

from requests.exceptions import HTTPError

from geoservercloud.exceptions import DatastoreMissing, GsDetail, WorkspaceMissing
from geoservercloud.models.layer import Layer
from geoservercloud.services import RestService


Expand DownExpand Up@@ -31,17 +36,23 @@ def __init__(
dst_url: str,
dst_user: str,
dst_password: str,
src_verifytls: bool = True,
dst_verifytls: bool = True,
) -> None:
self.src_url: str = src_url.strip("/")
self.src_user: str = src_user
self.src_password: str = src_password
self.src_auth: tuple[str, str] = (src_user, src_password)
self.src_instance: RestService = RestService(src_url, self.src_auth)
self.src_instance: RestService = RestService(
src_url, self.src_auth, src_verifytls
)
self.dst_url: str = dst_url.strip("/")
self.dst_user: str = dst_user
self.dst_password: str = dst_password
self.dst_auth: tuple[str, str] = (dst_user, dst_password)
self.dst_instance: RestService = RestService(dst_url, self.dst_auth)
self.dst_instance: RestService = RestService(
dst_url, self.dst_auth, src_verifytls
)

def copy_workspace(
self, workspace_name: str, deep_copy: bool = False
Expand DownExpand Up@@ -163,9 +174,47 @@ def copy_layer(
layer, status_code = self.src_instance.get_layer(
workspace_name, feature_type_name
)
if isinstance(layer, str):
return layer, status_code
return self.dst_instance.update_layer(layer, workspace_name)
if (status_code != 200) or isinstance(layer, str):
return f"Error: unexpected response {layer}", 500
resource, status_code = self.src_instance.get_resource_from_layer(layer)
layer_string = json.dumps(layer.asdict())
dst_layer = Layer.from_get_response_payload(
{
"layer": json.loads(
layer_string.replace(self.src_instance.url, self.dst_instance.url)
)
}
)

xml_resource_route = self.src_instance.get_resource_route_from_layer(layer)
if xml_resource_route is None:
return f"Error: cannot get resource route for {layer}", 500
try:
self.dst_instance.create_layer_resource(resource, xml_resource_route)
except HTTPError:
dst_resource = resource.decode().replace(
self.src_instance.url, self.dst_instance.url
)
store = json.loads(dst_resource)["featureType"]["store"]
if not self.dst_instance.check_href(store):
raise DatastoreMissing(
404,
GsDetail(
f"Datastore {store['name']} not found",
),
)
for ws_name, workspace in self.dst_instance.get_workspaces_from_store(
store
).items():
if not self.dst_instance.check_href(workspace):
raise WorkspaceMissing(
404,
GsDetail(
f"Workspace {ws_name} not found",
),
)

return self.dst_instance.update_layer(dst_layer, workspace_name)

def copy_layer_groups(self, workspace_name: str) -> tuple[str, int]:
"""
Expand DownExpand Up@@ -228,20 +277,39 @@ def copy_style(
"""
Copy a style from source to destination GeoServer instance
"""
style_definition, status_code = self.src_instance.get_style_definition(
style_info, status_code = self.src_instance.get_style_info(
style_name, workspace_name
)
if isinstance(style_definition, str):
return style_definition, status_code
content, status_code = self.dst_instance.create_style_definition(
style_name, style_definition, workspace_name
)
if self.not_ok(status_code):
return f"Error getting {style_info}", status_code
style_definition_response = self.src_instance.get_raw_style_definition(
style_name, workspace_name, style_info.format
)
if self.not_ok(style_definition_response.status_code):
content: str = style_definition_response.content.decode()
return content, status_code
style, status_code = self.src_instance.get_style(style_name, workspace_name)
if isinstance(style, str):
return style, status_code
return self.dst_instance.create_style(style_name, style, workspace_name)
try:
content, status_code = self.dst_instance.create_style_info(
style_name, style_info, workspace_name
)
if self.not_ok(status_code):
return content, status_code
except HTTPError:
if not self.dst_instance.check_workspace_for_style(style_info):
raise WorkspaceMissing(
404,
GsDetail(
f"Workspace {style_info.workspace_name} not found",
),
)

return self.dst_instance.fill_style_definition(
style_name,
style_definition_response.content,
style_info.format,
style_definition_response.headers["content-type"],
workspace_name,
)

def copy_style_images(self, workspace_name: str | None = None) -> tuple[str, int]:
"""
Expand Down
23 changes: 20 additions & 3 deletions geoservercloud/models/layer.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@ def __init__(
self,
name: str,
resource_name: str | None = None,
resource_href: str | None = None,
type: str | None = None,
default_style_name: str | None = None,
styles: list | None = None,
Expand All@@ -19,7 +20,7 @@ def __init__(
self.type: str | None = type
self.resource: ReferencedObjectModel | None = None
if resource_name:
self.resource = ReferencedObjectModel(resource_name)
self.resource = ReferencedObjectModel(resource_name, resource_href)
self.default_style: ReferencedObjectModel | None = None
if default_style_name:
self.default_style = ReferencedObjectModel(default_style_name)
Expand All@@ -35,6 +36,15 @@ def resource_name(self) -> str | None:
def default_style_name(self) -> str | None:
return self.default_style.name if self.default_style else None

@property
def all_style_names(self) -> set[str]:
all_styles = set()
if self.default_style_name is not None:
all_styles.add(self.default_style_name)
if self.styles is not None:
all_styles.update(self.styles)
return all_styles

@classmethod
def from_get_response_payload(cls, content: dict):
layer = content["layer"]
Expand All@@ -45,6 +55,7 @@ def from_get_response_payload(cls, content: dict):
return cls(
name=layer["name"],
resource_name=layer["resource"]["name"],
resource_href=layer["resource"]["href"],
type=layer["type"],
default_style_name=layer["defaultStyle"]["name"],
styles=styles,
Expand All@@ -59,11 +70,17 @@ def asdict(self) -> dict[str, Any]:
optional_items = {
"name": self.name,
"type": self.type,
"resource": self.resource_name,
"defaultStyle": self.default_style_name,
"defaultStyle": {
"name": self.default_style_name,
},
"attribution": self.attribution,
"queryable": self.queryable,
}
if self.resource is not None:
optional_items["resource"] = {
"name": self.resource.name,
"href": self.resource.href,
}
return EntityModel.add_items_to_dict(content, optional_items)

def post_payload(self) -> dict[str, dict[str, Any]]:
Expand Down
4 changes: 2 additions & 2 deletions geoservercloud/services/owsservice.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,11 +10,11 @@


class OwsService:
def __init__(self, url: str, auth: tuple[str, str]) -> None:
def __init__(self, url: str, auth: tuple[str, str], verifytls: bool = True) -> None:
self.url: str = url
self.auth: tuple[str, str] = auth
self.ows_endpoints = self.OwsEndpoints()
self.rest_client = RestClient(url, auth)
self.rest_client = RestClient(url, auth, verifytls)

def create_wms(self, workspace_name: str | None = None) -> WebMapService_1_3_0:
if workspace_name is None:
Expand Down
46 changes: 40 additions & 6 deletions geoservercloud/services/restclient.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,8 @@

import requests

from .restlogger import gs_logger

TIMEOUT = 120


Expand All@@ -17,22 +19,31 @@ class RestClient:
username and password for GeoServer
"""

def __init__(self, url: str, auth: tuple[str, str]) -> None:
def __init__(self, url: str, auth: tuple[str, str], verifytls: bool = True) -> None:
self.url: str = url
self.auth: tuple[str, str] = auth
self.verifytls: bool = verifytls

def get(
self,
path: str,
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.get(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 404:
response.raise_for_status()
Expand All@@ -46,15 +57,22 @@ def post(
json: dict[str, dict[str, Any] | Any] | None = None,
data: bytes | None = None,
) -> requests.Response:

full_url = f"{self.url}{path}"
response: requests.Response = requests.post(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
json=json,
data=data,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 409:
response.raise_for_status()
Expand All@@ -68,14 +86,22 @@ def put(
json: dict[str, dict[str, Any]] | None = None,
data: bytes | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.put(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
json=json,
data=data,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
response.raise_for_status()
return response
Expand All@@ -86,12 +112,20 @@ def delete(
params: dict[str, str] | None = None,
headers: dict[str, str] | None = None,
) -> requests.Response:
full_url = f"{self.url}{path}"
response: requests.Response = requests.delete(
f"{self.url}{path}",
full_url,
params=params,
headers=headers,
auth=self.auth,
timeout=TIMEOUT,
verify=self.verifytls,
)
gs_logger.info(
"[GET] (%s) - %s",
response.status_code,
full_url,
extra={"response": response},
)
if response.status_code != 404:
response.raise_for_status()
Expand Down
12 changes: 12 additions & 0 deletions geoservercloud/services/restlogger.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import logging

time_formatter = logging.Formatter(
"{asctime} - {name}:{levelname} - {message}",
style="{",
datefmt="%Y-%m-%d %H:%M",
)
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(time_formatter)
gs_logger = logging.getLogger("GS Session")
gs_logger.setLevel(logging.INFO)
gs_logger.addHandler(stream_handler)
Loading