Repository: camptocamp/python-geoservercloud

Issue:
Generic style generation method to handle XML, SLD and zip
Rework the existing methods `create_style` and `create_style_from_file` to a more generic method capable of posting a style as XML, SLD or zip. According to the [doc](https://docs.geoserver.org/latest/en/api/#1.0.0/styles.yaml) it should be possible to post the style definition as JSON as well, but we couldn't make it work so far so we probably won't implement it.

Regarding SLD, we don't plan to create or manipulate them, but instead to read them from the file system (as currently implemented) or from the REST API (to copy an existing style). They will be passed as binary blobs so that serialization / deserialization is not needed.



Discussion:

Patch:
diff --git a/geoservercloud/geoservercloud.py b/geoservercloud/geoservercloud.py
index 622dc65..7521de4 100644
--- a/geoservercloud/geoservercloud.py
+++ b/geoservercloud/geoservercloud.py
@@ -1,3 +1,4 @@
+from pathlib import Path
 from typing import Any
 
 from owslib.map.wms130 import WebMapService_1_3_0
@@ -6,12 +7,11 @@ from owslib.wmts import WebMapTileService
 from requests import Response
 
 from geoservercloud import utils
-from geoservercloud.models import (
-    FeatureType,
-    PostGisDataStore,
-    Workspace,
-)
+from geoservercloud.models.datastore import PostGisDataStore
+from geoservercloud.models.featuretype import FeatureType
 from geoservercloud.models.layer import Layer
+from geoservercloud.models.style import Style
+from geoservercloud.models.workspace import Workspace
 from geoservercloud.services import OwsService, RestService
 from geoservercloud.templates import Templates
 
@@ -382,26 +382,49 @@ class GeoServerCloud:
             return content, code
         return content.aslist(), code
 
-    def get_style(
+    def get_style_definition(
         self, style: str, workspace_name: str | None = None
     ) -> tuple[dict[str, Any] | str, int]:
         """
-        Get a specific style
+        Get a style definition by name
         """
-        content, code = self.rest_service.get_style(style, workspace_name)
+        content, code = self.rest_service.get_style_definition(style, workspace_name)
         if isinstance(content, str):
             return content, code
         return content.asdict(), code
 
+    def create_style_definition(
+        self,
+        style_name: str,
+        filename: str,
+        workspace_name: str | None = None,
+    ) -> tuple[str, int]:
+        """Create a style definition"""
+        style = Style(name=style_name, filename=filename, workspace_name=workspace_name)
+        return self.rest_service.create_style_definition(
+            style_name=style_name, style=style, workspace_name=workspace_name
+        )
+
     def create_style_from_file(
         self,
-        style: str,
+        style_name: str,
         file: str,
         workspace_name: str | None = None,
     ) -> tuple[str, int]:
-        """Create a style from a file, or update it if it already exists.
+        """Create a style (SLD) from a file, or update it if it already exists.
         Supported file extensions are .sld and .zip."""
-        return self.rest_service.create_style_from_file(style, file, workspace_name)
+        file_ext = Path(file).suffix
+        if file_ext == ".sld":
+            file_format = "sld"
+        elif file_ext == ".zip":
+            file_format = "zip"
+        else:
+            raise ValueError(f"Unsupported file extension: {file_ext}")
+        with open(f"{file}", "rb") as fs:
+            style: bytes = fs.read()
+        return self.rest_service.create_style(
+            style_name, style, workspace_name, format=file_format
+        )
 
     def set_default_layer_style(
         self, layer_name: str, workspace_name: str, style: str
diff --git a/geoservercloud/geoservercloudsync.py b/geoservercloud/geoservercloudsync.py
index 768ed43..146e6b2 100644
--- a/geoservercloud/geoservercloudsync.py
+++ b/geoservercloud/geoservercloudsync.py
@@ -1,3 +1,4 @@
+from geoservercloud.models.resourcedirectory import ResourceDirectory
 from geoservercloud.services import RestService
 
 
@@ -41,11 +42,101 @@ class GeoServerCloudSync:
         self.dst_auth: tuple[str, str] = (dst_user, dst_password)
         self.dst_instance: RestService = RestService(dst_url, self.dst_auth)
 
-    def copy_workspace(self, workspace_name: str) -> tuple[str, int]:
+    def copy_workspace(
+        self, workspace_name: str, deep_copy: bool = False
+    ) -> tuple[str, int]:
         """
-        Shallow copy a workspace from source to destination GeoServer instance
+        Copy a workspace from the source to the destination GeoServer instance.
+        If deep_copy is True, the copy includes the styles in the workspace (including images).
         """
         workspace, status_code = self.src_instance.get_workspace(workspace_name)
         if isinstance(workspace, str):
             return workspace, status_code
-        return self.dst_instance.create_workspace(workspace)
+        new_workspace, new_ws_status_code = self.dst_instance.create_workspace(
+            workspace
+        )
+        if self.not_ok(new_ws_status_code):
+            return new_workspace, new_ws_status_code
+        if deep_copy:
+            content, status_code = self.copy_styles(workspace_name)
+            if self.not_ok(status_code):
+                return content, status_code
+        return new_workspace, new_ws_status_code
+
+    def copy_styles(
+        self, workspace_name: str | None = None, include_images: bool = True
+    ) -> tuple[str, int]:
+        """
+        Copy all styles in a workspace (if a workspace is provided) or all global styles
+        """
+        if include_images:
+            content, status_code = self.copy_style_images(workspace_name)
+            if self.not_ok(status_code):
+                return content, status_code
+        styles, status_code = self.src_instance.get_styles(workspace_name)
+        if isinstance(styles, str):
+            return styles, status_code
+        for style in styles.aslist():
+            content, status_code = self.copy_style(style, workspace_name)
+            if self.not_ok(status_code):
+                return content, status_code
+        return content, status_code
+
+    def copy_style(
+        self,
+        style_name: str,
+        workspace_name: str | None = None,
+    ) -> tuple[str, int]:
+        """
+        Copy a style from source to destination GeoServer instance
+        """
+        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)
+
+    def copy_style_images(self, workspace_name: str | None = None) -> tuple[str, int]:
+        """
+        Copy all images in a workspace's style directory, or all global style images if no workspace is provided
+        """
+        resource_dir, status_code = self.src_instance.get_resource_directory(
+            path="styles", workspace_name=workspace_name
+        )
+        if isinstance(resource_dir, str):
+            return resource_dir, status_code
+        for child in resource_dir.children:
+            if child.is_image():
+                content, status_code = self.copy_resource(
+                    resource_dir="styles",
+                    resource_name=child.name,
+                    content_type=child.type,
+                    workspace_name=workspace_name,
+                )
+        return content, status_code
+
+    def copy_resource(
+        self,
+        resource_dir: str,
+        resource_name: str,
+        content_type: str,
+        workspace_name: str | None = None,
+    ) -> tuple[str, int]:
+        """
+        Copy a resource from source to destination GeoServer instance
+        """
+        resource, status_code = self.src_instance.get_resource(
+            resource_dir, resource_name, workspace_name
+        )
+        if self.not_ok(status_code):
+            return resource.decode(), status_code
+        return self.dst_instance.put_resource(
+            path=resource_dir,
+            name=resource_name,
+            workspace_name=workspace_name,
+            content_type=content_type,
+            data=resource,
+        )
+
+    @staticmethod
+    def not_ok(http_status_code: int) -> bool:
+        return http_status_code >= 400
diff --git a/geoservercloud/models/__init__.py b/geoservercloud/models/__init__.py
index 3115182..e69de29 100644
--- a/geoservercloud/models/__init__.py
+++ b/geoservercloud/models/__init__.py
@@ -1,33 +0,0 @@
-from .common import (
-    I18N,
-    BaseModel,
-    EntityModel,
-    KeyDollarListDict,
-    ListModel,
-    ReferencedObjectModel,
-)
-from .datastore import PostGisDataStore
-from .datastores import DataStores
-from .featuretype import FeatureType
-from .featuretypes import FeatureTypes
-from .style import Style
-from .styles import Styles
-from .workspace import Workspace
-from .workspaces import Workspaces
-
-__all__ = [
-    "BaseModel",
-    "DataStores",
-    "EntityModel",
-    "ReferencedObjectModel",
-    "KeyDollarListDict",
-    "ListModel",
-    "FeatureType",
-    "FeatureTypes",
-    "I18N",
-    "PostGisDataStore",
-    "Style",
-    "Styles",
-    "Workspaces",
-    "Workspace",
-]
diff --git a/geoservercloud/models/datastore.py b/geoservercloud/models/datastore.py
index 5efe034..2863c4f 100644
--- a/geoservercloud/models/datastore.py
+++ b/geoservercloud/models/datastore.py
@@ -1,7 +1,11 @@
 import json
 from typing import Any
 
-from geoservercloud.models import EntityModel, KeyDollarListDict, ReferencedObjectModel
+from geoservercloud.models.common import (
+    EntityModel,
+    KeyDollarListDict,
+    ReferencedObjectModel,
+)
 
 
 class PostGisDataStore(EntityModel):
diff --git a/geoservercloud/models/datastores.py b/geoservercloud/models/datastores.py
index b682e17..e49ba5a 100644
--- a/geoservercloud/models/datastores.py
+++ b/geoservercloud/models/datastores.py
@@ -1,4 +1,4 @@
-from geoservercloud.models import ListModel
+from geoservercloud.models.common import ListModel
 
 
 class DataStores(ListModel):
diff --git a/geoservercloud/models/featuretype.py b/geoservercloud/models/featuretype.py
index a80dfac..19829ae 100644
--- a/geoservercloud/models/featuretype.py
+++ b/geoservercloud/models/featuretype.py
@@ -1,7 +1,7 @@
 import json
 from typing import Any
 
-from geoservercloud.models import I18N, EntityModel, ReferencedObjectModel
+from geoservercloud.models.common import I18N, EntityModel, ReferencedObjectModel
 from geoservercloud.utils import EPSG_BBOX
 
 
diff --git a/geoservercloud/models/featuretypes.py b/geoservercloud/models/featuretypes.py
index 95724ae..36b5807 100644
--- a/geoservercloud/models/featuretypes.py
+++ b/geoservercloud/models/featuretypes.py
@@ -1,6 +1,6 @@
 import json
 
-from geoservercloud.models import ListModel
+from geoservercloud.models.common import ListModel
 
 
 class FeatureTypes(ListModel):
diff --git a/geoservercloud/models/layer.py b/geoservercloud/models/layer.py
index 2d97097..611fbeb 100644
--- a/geoservercloud/models/layer.py
+++ b/geoservercloud/models/layer.py
@@ -1,6 +1,6 @@
 from typing import Any
 
-from geoservercloud.models import EntityModel, ReferencedObjectModel
+from geoservercloud.models.common import EntityModel, ReferencedObjectModel
 from geoservercloud.models.styles import Styles
 
 
diff --git a/geoservercloud/models/resourcedirectory.py b/geoservercloud/models/resourcedirectory.py
new file mode 100644
index 0000000..83aefb4
--- /dev/null
+++ b/geoservercloud/models/resourcedirectory.py
@@ -0,0 +1,44 @@
+from typing import Any
+
+from geoservercloud.models.common import EntityModel
+
+
+class Resource(EntityModel):
+    def __init__(
+        self,
+        name: str,
+        href: str,
+        type: str,
+    ) -> None:
+        self.name: str = name
+        self.href: str = href
+        self.type: str = type
+
+    def is_image(self) -> bool:
+        return self.type.startswith("image")
+
+
+class ResourceDirectory(EntityModel):
+    def __init__(self, name: str, parent: Resource, children: list[Resource]) -> None:
+        self.name: str = name
+        self.parent: Resource = parent
+        self.children: list[Resource] = children
+
+    @classmethod
+    def from_get_response_payload(cls, payload: dict[str, Any]) -> "ResourceDirectory":
+        resource_directory = payload["ResourceDirectory"]
+        parent = resource_directory["parent"]
+        parent = Resource(
+            name=parent["path"],
+            href=parent["link"]["href"],
+            type=parent["link"]["type"],
+        )
+        children = [
+            Resource(child["name"], child["link"]["href"], child["link"]["type"])
+            for child in resource_directory.get("children", {}).get("child", [])
+        ]
+        return cls(
+            name=resource_directory["name"],
+            parent=parent,
+            children=children,
+        )
diff --git a/geoservercloud/models/style.py b/geoservercloud/models/style.py
index 138d67a..2e8b680 100644
--- a/geoservercloud/models/style.py
+++ b/geoservercloud/models/style.py
@@ -3,7 +3,7 @@ from typing import Any
 
 import xmltodict
 
-from geoservercloud.models import EntityModel, ReferencedObjectModel
+from geoservercloud.models.common import EntityModel, ReferencedObjectModel
 
 
 class Style(EntityModel):
diff --git a/geoservercloud/models/styles.py b/geoservercloud/models/styles.py
index b8dfe46..5f76a36 100644
--- a/geoservercloud/models/styles.py
+++ b/geoservercloud/models/styles.py
@@ -1,4 +1,4 @@
-from geoservercloud.models import ListModel
+from geoservercloud.models.common import ListModel
 
 
 class Styles(ListModel):
diff --git a/geoservercloud/models/workspace.py b/geoservercloud/models/workspace.py
index 504b03b..af4179b 100644
--- a/geoservercloud/models/workspace.py
+++ b/geoservercloud/models/workspace.py
@@ -1,7 +1,7 @@
 import json
 from typing import Any
 
-from geoservercloud.models import EntityModel
+from geoservercloud.models.common import EntityModel
 
 
 class Workspace(EntityModel):
diff --git a/geoservercloud/models/workspaces.py b/geoservercloud/models/workspaces.py
index 8772615..d4a95e8 100644
--- a/geoservercloud/models/workspaces.py
+++ b/geoservercloud/models/workspaces.py
@@ -1,4 +1,4 @@
-from geoservercloud.models import ListModel
+from geoservercloud.models.common import ListModel
 
 
 class Workspaces(ListModel):
diff --git a/geoservercloud/services/restservice.py b/geoservercloud/services/restservice.py
index 6d08239..ec4de21 100644
--- a/geoservercloud/services/restservice.py
+++ b/geoservercloud/services/restservice.py
@@ -5,18 +5,17 @@ from typing import Any
 from owslib.wmts import WebMapTileService
 from requests import Response
 
-from geoservercloud.models import (
-    BaseModel,
-    DataStores,
-    PostGisDataStore,
-    Style,
-    Styles,
-    Workspace,
-    Workspaces,
-)
+from geoservercloud.models.common import BaseModel
+from geoservercloud.models.datastore import PostGisDataStore
+from geoservercloud.models.datastores import DataStores
 from geoservercloud.models.featuretype import FeatureType
 from geoservercloud.models.featuretypes import FeatureTypes
 from geoservercloud.models.layer import Layer
+from geoservercloud.models.resourcedirectory import ResourceDirectory
+from geoservercloud.models.style import Style
+from geoservercloud.models.styles import Styles
+from geoservercloud.models.workspace import Workspace
+from geoservercloud.models.workspaces import Workspaces
 from geoservercloud.services.restclient import RestClient
 from geoservercloud.templates import Templates
 
@@ -323,76 +322,65 @@ class RestService:
         return response.content.decode(), response.status_code
 
     def get_styles(self, workspace_name: str | None = None) -> tuple[Styles | str, int]:
-        path = (
-            self.rest_endpoints.styles()
-            if not workspace_name
-            else self.rest_endpoints.workspace_styles(workspace_name)
-        )
+        path = self.rest_endpoints.styles(workspace_name=workspace_name)
         response: Response = self.rest_client.get(path)
         return self.deserialize_response(response, Styles)
 
-    def get_style(
+    def get_style_definition(
         self, style: str, workspace_name: str | None = None
     ) -> tuple[Style | str, int]:
-        path = (
-            self.rest_endpoints.style(style)
-            if not workspace_name
-            else self.rest_endpoints.workspace_style(workspace_name, style)
-        )
-        response: Response = self.rest_client.get(path)
-        return self.deserialize_response(response, Style)
+        path = self.rest_endpoints.style(style, workspace_name, format="json")
+        return self.deserialize_response(self.rest_client.get(path), Style)
 
-    def create_style(
-        self, style: Style, workspace_name: str | None = None
+    def create_style_definition(
+        self,
+        style_name: str,
+        style: Style,
+        workspace_name: str | None = None,
     ) -> tuple[str, int]:
-        path = (
-            self.rest_endpoints.styles()
-            if not workspace_name
-            else self.rest_endpoints.workspace_styles(workspace_name)
-        )
-        resource_path = (
-            self.rest_endpoints.style(style.name)
-            if not workspace_name
-            else self.rest_endpoints.workspace_style(workspace_name, style.name)
+        # Use XML because JSON is not supported for style creation
+        path = self.rest_endpoints.styles(workspace_name=workspace_name, format="xml")
+        resource_path = self.rest_endpoints.style(
+            workspace_name=workspace_name, style_name=style_name, format="xml"
         )
+        data: bytes = style.xml_post_payload().encode()
+        headers: dict[str, str] = {"Content-Type": "text/xml"}
         if not self.resource_exists(resource_path):
-            response: Response = self.rest_client.post(path, json=style.post_payload())
+            response: Response = self.rest_client.post(path, data=data, headers=headers)
         else:
-            response = self.rest_client.put(resource_path, json=style.put_payload())
+            response = self.rest_client.put(resource_path, data=data, headers=headers)
         return response.content.decode(), response.status_code
 
-    def create_style_from_file(
+    def get_style(
+        self, style: str, workspace_name: str | None = None, format: str = "sld"
+    ) -> tuple[bytes | str, int]:
+        path = self.rest_endpoints.style(style, workspace_name, format=format)
+        response: Response = self.rest_client.get(path)
+        if not response.ok:
+            return response.content.decode(), response.status_code
+        return response.content, response.status_code
+
+    def create_style(
         self,
-        style: str,
-        file: str,
+        style_name: str,
+        style: bytes,
         workspace_name: str | None = None,
+        format: str = "sld",
     ) -> tuple[str, int]:
-        path = (
-            self.rest_endpoints.styles()
-            if not workspace_name
-            else self.rest_endpoints.workspace_styles(workspace_name)
-        )
-        resource_path = (
-            self.rest_endpoints.style(style)
-            if not workspace_name
-            else self.rest_endpoints.workspace_style(workspace_name, style)
-        )
-
-        file_ext = Path(file).suffix
-        if file_ext == ".sld":
-            content_type = "application/vnd.ogc.sld+xml"
-        elif file_ext == ".zip":
-            content_type = "application/zip"
-        else:
-            raise ValueError(f"Unsupported file extension: {file_ext}")
-        with open(f"{file}", "rb") as fs:
-            data: bytes = fs.read()
-        headers: dict[str, str] = {"Content-Type": content_type}
-
+        path = self.rest_endpoints.styles(workspace_name=workspace_name, format=format)
+        resource_path = self.rest_endpoints.style(
+            workspace_name=workspace_name, style_name=style_name, format=format
+        )
+        if format == "sld":
+            headers = {"Content-Type": "application/vnd.ogc.sld+xml"}
+        elif format == "zip":
+            headers = {"Content-Type": "application/zip"}
         if not self.resource_exists(resource_path):
-            response: Response = self.rest_client.post(path, data=data, headers=headers)
+            response: Response = self.rest_client.post(
+                path, data=style, headers=headers
+            )
         else:
-            response = self.rest_client.put(resource_path, data=data, headers=headers)
+            response = self.rest_client.put(resource_path, data=style, headers=headers)
         return response.content.decode(), response.status_code
 
     def update_layer(self, layer: Layer, workspace_name: str) -> tuple[str, int]:
@@ -575,6 +563,39 @@ class RestService:
         response: Response = self.rest_client.delete(self.acl_endpoints.rules())
         return response.content.decode(), response.status_code
 
+    def get_resource_directory(
+        self, path: str, workspace_name: str | None = None
+    ) -> tuple[ResourceDirectory | str, int]:
+        response: Response = self.rest_client.get(
+            self.rest_endpoints.resource_directory(path, workspace_name),
+            headers={"Accept": "application/json"},
+        )
+        return self.deserialize_response(response, ResourceDirectory)
+
+    def get_resource(
+        self, path: str, resource_name: str, workspace_name: str | None = None
+    ) -> tuple[bytes, int]:
+        response: Response = self.rest_client.get(
+            self.rest_endpoints.resource(path, resource_name, workspace_name),
+        )
+        return response.content, response.status_code
+
+    def put_resource(
+        self,
+        path: str,
+        name: str,
+        content_type: str,
+        data: bytes,
+        workspace_name: str | None = None,
+    ) -> tuple[str, int]:
+        headers = {"Content-Type": content_type}
+        response: Response = self.rest_client.put(
+            path=self.rest_endpoints.resource(path, name, workspace_name),
+            headers=headers,
+            data=data,
+        )
+        return response.content.decode(), response.status_code
+
     @staticmethod
     def get_wmts_layer_bbox(
         url: str, layer_name: str
@@ -635,11 +656,30 @@ class RestService:
         def __init__(self, base_url: str = "/rest") -> None:
             self.base_url: str = base_url
 
-        def styles(self) -> str:
-            return f"{self.base_url}/styles.json"
-
-        def style(self, style_name: str) -> str:
-            return f"{self.base_url}/styles/{style_name}.json"
+        def styles(
+            self, workspace_name: str | None = None, format: str = "json"
+        ) -> str:
+            if not workspace_name:
+                url: str = f"{self.base_url}/styles"
+            else:
+                url = f"{self.base_url}/workspaces/{workspace_name}/styles"
+            if format == "json":
+                return f"{url}.json"
+            return url
+
+        def style(
+            self,
+            style_name: str,
+            workspace_name: str | None = None,
+            format: str = "json",
+        ) -> str:
+            if not workspace_name:
+                url: str = f"{self.base_url}/styles/{style_name}"
+            else:
+                url = f"{self.base_url}/workspaces/{workspace_name}/styles/{style_name}"
+            if format in ("json", "sld"):
+                return f"{url}.{format}"
+            return url
 
         def workspaces(self) -> str:
             return f"{self.base_url}/workspaces.json"
@@ -647,14 +687,6 @@ class RestService:
         def workspace(self, workspace_name: str) -> str:
             return f"{self.base_url}/workspaces/{workspace_name}.json"
 
-        def workspace_styles(self, workspace_name: str) -> str:
-            return f"{self.base_url}/workspaces/{workspace_name}/styles.json"
-
-        def workspace_style(self, workspace_name: str, style_name: str) -> str:
-            return (
-                f"{self.base_url}/workspaces/{workspace_name}/styles/{style_name}.json"
-            )
-
         def workspace_layer(self, workspace_name: str, layer_name: str) -> str:
             return f"{self.base_url}/layers/{workspace_name}:{layer_name}.json"
 
@@ -743,3 +775,22 @@ class RestService:
             return (
                 f"{self.base_url}/security/roles/role/{role_name}/user/{username}.json"
             )
+
+        def resource_directory(
+            self, relative_path: str, workspace_name: str | None = None
+        ) -> str:
+            if not workspace_name:
+                return f"{self.base_url}/resource/{relative_path}"
+            return (
+                f"{self.base_url}/resource/workspaces/{workspace_name}/{relative_path}"
+            )
+
+        def resource(
+            self,
+            relative_path: str,
+            resource_name: str,
+            workspace_name: str | None = None,
+        ) -> str:
+            if not workspace_name:
+                return f"{self.base_url}/resource/{relative_path}/{resource_name}"
+            return f"{self.base_url}/resource/workspaces/{workspace_name}/{relative_path}/{resource_name}"
diff --git a/poetry.lock b/poetry.lock
index a4a6017..755ece7 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,25 +1,5 @@
 # This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
 
-[[package]]
-name = "attrs"
-version = "24.2.0"
-description = "Classes Without Boilerplate"
-category = "main"
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"},
-    {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"},
-]
-
-[package.extras]
-benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
-cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
-dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
-docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
-tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
-tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
-
 [[package]]
 name = "certifi"
 version = "2024.8.30"
@@ -276,43 +256,6 @@ files = [
     {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
 ]
 
-[[package]]
-name = "jsonschema"
-version = "4.23.0"
-description = "An implementation of JSON Schema validation for Python"
-category = "main"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"},
-    {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"},
-]
-
-[package.dependencies]
-attrs = ">=22.2.0"
-jsonschema-specifications = ">=2023.03.6"
-referencing = ">=0.28.4"
-rpds-py = ">=0.7.1"
-
-[package.extras]
-format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"]
-format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"]
-
-[[package]]
-name = "jsonschema-specifications"
-version = "2024.10.1"
-description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry"
-category = "main"
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"},
-    {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"},
-]
-
-[package.dependencies]
-referencing = ">=0.31.0"
-
 [[package]]
 name = "lxml"
 version = "5.3.0"
@@ -633,22 +576,6 @@ files = [
     {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"},
 ]
 
-[[package]]
-name = "referencing"
-version = "0.35.1"
-description = "JSON Referencing + Python"
-category = "main"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"},
-    {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"},
-]
-
-[package.dependencies]
-attrs = ">=22.2.0"
-rpds-py = ">=0.7.0"
-
 [[package]]
 name = "requests"
 version = "2.32.3"
@@ -691,119 +618,6 @@ urllib3 = ">=1.25.10,<3.0"
 [package.extras]
 tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-PyYAML", "types-requests"]
 
-[[package]]
-name = "rpds-py"
-version = "0.20.1"
-description = "Python bindings to Rust's persistent data structures (rpds)"
-category = "main"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "rpds_py-0.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a649dfd735fff086e8a9d0503a9f0c7d01b7912a333c7ae77e1515c08c146dad"},
-    {file = "rpds_py-0.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f16bc1334853e91ddaaa1217045dd7be166170beec337576818461268a3de67f"},
-    {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14511a539afee6f9ab492b543060c7491c99924314977a55c98bfa2ee29ce78c"},
-    {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ccb8ac2d3c71cda472b75af42818981bdacf48d2e21c36331b50b4f16930163"},
-    {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c142b88039b92e7e0cb2552e8967077e3179b22359e945574f5e2764c3953dcf"},
-    {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f19169781dddae7478a32301b499b2858bc52fc45a112955e798ee307e294977"},
-    {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13c56de6518e14b9bf6edde23c4c39dac5b48dcf04160ea7bce8fca8397cdf86"},
-    {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:925d176a549f4832c6f69fa6026071294ab5910e82a0fe6c6228fce17b0706bd"},
-    {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:78f0b6877bfce7a3d1ff150391354a410c55d3cdce386f862926a4958ad5ab7e"},
-    {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3dd645e2b0dcb0fd05bf58e2e54c13875847687d0b71941ad2e757e5d89d4356"},
-    {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4f676e21db2f8c72ff0936f895271e7a700aa1f8d31b40e4e43442ba94973899"},
-    {file = "rpds_py-0.20.1-cp310-none-win32.whl", hash = "sha256:648386ddd1e19b4a6abab69139b002bc49ebf065b596119f8f37c38e9ecee8ff"},
-    {file = "rpds_py-0.20.1-cp310-none-win_amd64.whl", hash = "sha256:d9ecb51120de61e4604650666d1f2b68444d46ae18fd492245a08f53ad2b7711"},
-    {file = "rpds_py-0.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:762703bdd2b30983c1d9e62b4c88664df4a8a4d5ec0e9253b0231171f18f6d75"},
-    {file = "rpds_py-0.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0b581f47257a9fce535c4567782a8976002d6b8afa2c39ff616edf87cbeff712"},
-    {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:842c19a6ce894493563c3bd00d81d5100e8e57d70209e84d5491940fdb8b9e3a"},
-    {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42cbde7789f5c0bcd6816cb29808e36c01b960fb5d29f11e052215aa85497c93"},
-    {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c8e9340ce5a52f95fa7d3b552b35c7e8f3874d74a03a8a69279fd5fca5dc751"},
-    {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ba6f89cac95c0900d932c9efb7f0fb6ca47f6687feec41abcb1bd5e2bd45535"},
-    {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a916087371afd9648e1962e67403c53f9c49ca47b9680adbeef79da3a7811b0"},
-    {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:200a23239781f46149e6a415f1e870c5ef1e712939fe8fa63035cd053ac2638e"},
-    {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58b1d5dd591973d426cbb2da5e27ba0339209832b2f3315928c9790e13f159e8"},
-    {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6b73c67850ca7cae0f6c56f71e356d7e9fa25958d3e18a64927c2d930859b8e4"},
-    {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d8761c3c891cc51e90bc9926d6d2f59b27beaf86c74622c8979380a29cc23ac3"},
-    {file = "rpds_py-0.20.1-cp311-none-win32.whl", hash = "sha256:cd945871335a639275eee904caef90041568ce3b42f402c6959b460d25ae8732"},
-    {file = "rpds_py-0.20.1-cp311-none-win_amd64.whl", hash = "sha256:7e21b7031e17c6b0e445f42ccc77f79a97e2687023c5746bfb7a9e45e0921b84"},
-    {file = "rpds_py-0.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:36785be22066966a27348444b40389f8444671630063edfb1a2eb04318721e17"},
-    {file = "rpds_py-0.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:142c0a5124d9bd0e2976089484af5c74f47bd3298f2ed651ef54ea728d2ea42c"},
-    {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbddc10776ca7ebf2a299c41a4dde8ea0d8e3547bfd731cb87af2e8f5bf8962d"},
-    {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15a842bb369e00295392e7ce192de9dcbf136954614124a667f9f9f17d6a216f"},
-    {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be5ef2f1fc586a7372bfc355986226484e06d1dc4f9402539872c8bb99e34b01"},
-    {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbcf360c9e3399b056a238523146ea77eeb2a596ce263b8814c900263e46031a"},
-    {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd27a66740ffd621d20b9a2f2b5ee4129a56e27bfb9458a3bcc2e45794c96cb"},
-    {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0b937b2a1988f184a3e9e577adaa8aede21ec0b38320d6009e02bd026db04fa"},
-    {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6889469bfdc1eddf489729b471303739bf04555bb151fe8875931f8564309afc"},
-    {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:19b73643c802f4eaf13d97f7855d0fb527fbc92ab7013c4ad0e13a6ae0ed23bd"},
-    {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3c6afcf2338e7f374e8edc765c79fbcb4061d02b15dd5f8f314a4af2bdc7feb5"},
-    {file = "rpds_py-0.20.1-cp312-none-win32.whl", hash = "sha256:dc73505153798c6f74854aba69cc75953888cf9866465196889c7cdd351e720c"},
-    {file = "rpds_py-0.20.1-cp312-none-win_amd64.whl", hash = "sha256:8bbe951244a838a51289ee53a6bae3a07f26d4e179b96fc7ddd3301caf0518eb"},
-    {file = "rpds_py-0.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6ca91093a4a8da4afae7fe6a222c3b53ee4eef433ebfee4d54978a103435159e"},
-    {file = "rpds_py-0.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b9c2fe36d1f758b28121bef29ed1dee9b7a2453e997528e7d1ac99b94892527c"},
-    {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f009c69bc8c53db5dfab72ac760895dc1f2bc1b62ab7408b253c8d1ec52459fc"},
-    {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6740a3e8d43a32629bb9b009017ea5b9e713b7210ba48ac8d4cb6d99d86c8ee8"},
-    {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32b922e13d4c0080d03e7b62991ad7f5007d9cd74e239c4b16bc85ae8b70252d"},
-    {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe00a9057d100e69b4ae4a094203a708d65b0f345ed546fdef86498bf5390982"},
-    {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fe9b04b6fa685bd39237d45fad89ba19e9163a1ccaa16611a812e682913496"},
-    {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa7ac11e294304e615b43f8c441fee5d40094275ed7311f3420d805fde9b07b4"},
-    {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aa97af1558a9bef4025f8f5d8c60d712e0a3b13a2fe875511defc6ee77a1ab7"},
-    {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:483b29f6f7ffa6af845107d4efe2e3fa8fb2693de8657bc1849f674296ff6a5a"},
-    {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37fe0f12aebb6a0e3e17bb4cd356b1286d2d18d2e93b2d39fe647138458b4bcb"},
-    {file = "rpds_py-0.20.1-cp313-none-win32.whl", hash = "sha256:a624cc00ef2158e04188df5e3016385b9353638139a06fb77057b3498f794782"},
-    {file = "rpds_py-0.20.1-cp313-none-win_amd64.whl", hash = "sha256:b71b8666eeea69d6363248822078c075bac6ed135faa9216aa85f295ff009b1e"},
-    {file = "rpds_py-0.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5b48e790e0355865197ad0aca8cde3d8ede347831e1959e158369eb3493d2191"},
-    {file = "rpds_py-0.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3e310838a5801795207c66c73ea903deda321e6146d6f282e85fa7e3e4854804"},
-    {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249280b870e6a42c0d972339e9cc22ee98730a99cd7f2f727549af80dd5a963"},
-    {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e79059d67bea28b53d255c1437b25391653263f0e69cd7dec170d778fdbca95e"},
-    {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b431c777c9653e569986ecf69ff4a5dba281cded16043d348bf9ba505486f36"},
-    {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da584ff96ec95e97925174eb8237e32f626e7a1a97888cdd27ee2f1f24dd0ad8"},
-    {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a0629ec053fc013808a85178524e3cb63a61dbc35b22499870194a63578fb9"},
-    {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fbf15aff64a163db29a91ed0868af181d6f68ec1a3a7d5afcfe4501252840bad"},
-    {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:07924c1b938798797d60c6308fa8ad3b3f0201802f82e4a2c41bb3fafb44cc28"},
-    {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4a5a844f68776a7715ecb30843b453f07ac89bad393431efbf7accca3ef599c1"},
-    {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:518d2ca43c358929bf08f9079b617f1c2ca6e8848f83c1225c88caeac46e6cbc"},
-    {file = "rpds_py-0.20.1-cp38-none-win32.whl", hash = "sha256:3aea7eed3e55119635a74bbeb80b35e776bafccb70d97e8ff838816c124539f1"},
-    {file = "rpds_py-0.20.1-cp38-none-win_amd64.whl", hash = "sha256:7dca7081e9a0c3b6490a145593f6fe3173a94197f2cb9891183ef75e9d64c425"},
-    {file = "rpds_py-0.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b41b6321805c472f66990c2849e152aff7bc359eb92f781e3f606609eac877ad"},
-    {file = "rpds_py-0.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a90c373ea2975519b58dece25853dbcb9779b05cc46b4819cb1917e3b3215b6"},
-    {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16d4477bcb9fbbd7b5b0e4a5d9b493e42026c0bf1f06f723a9353f5153e75d30"},
-    {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84b8382a90539910b53a6307f7c35697bc7e6ffb25d9c1d4e998a13e842a5e83"},
-    {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4888e117dd41b9d34194d9e31631af70d3d526efc363085e3089ab1a62c32ed1"},
-    {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5265505b3d61a0f56618c9b941dc54dc334dc6e660f1592d112cd103d914a6db"},
-    {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e75ba609dba23f2c95b776efb9dd3f0b78a76a151e96f96cc5b6b1b0004de66f"},
-    {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1791ff70bc975b098fe6ecf04356a10e9e2bd7dc21fa7351c1742fdeb9b4966f"},
-    {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d126b52e4a473d40232ec2052a8b232270ed1f8c9571aaf33f73a14cc298c24f"},
-    {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c14937af98c4cc362a1d4374806204dd51b1e12dded1ae30645c298e5a5c4cb1"},
-    {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3d089d0b88996df627693639d123c8158cff41c0651f646cd8fd292c7da90eaf"},
-    {file = "rpds_py-0.20.1-cp39-none-win32.whl", hash = "sha256:653647b8838cf83b2e7e6a0364f49af96deec64d2a6578324db58380cff82aca"},
-    {file = "rpds_py-0.20.1-cp39-none-win_amd64.whl", hash = "sha256:fa41a64ac5b08b292906e248549ab48b69c5428f3987b09689ab2441f267d04d"},
-    {file = "rpds_py-0.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a07ced2b22f0cf0b55a6a510078174c31b6d8544f3bc00c2bcee52b3d613f74"},
-    {file = "rpds_py-0.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:68cb0a499f2c4a088fd2f521453e22ed3527154136a855c62e148b7883b99f9a"},
-    {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa3060d885657abc549b2a0f8e1b79699290e5d83845141717c6c90c2df38311"},
-    {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95f3b65d2392e1c5cec27cff08fdc0080270d5a1a4b2ea1d51d5f4a2620ff08d"},
-    {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2cc3712a4b0b76a1d45a9302dd2f53ff339614b1c29603a911318f2357b04dd2"},
-    {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d4eea0761e37485c9b81400437adb11c40e13ef513375bbd6973e34100aeb06"},
-    {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f5179583d7a6cdb981151dd349786cbc318bab54963a192692d945dd3f6435d"},
-    {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fbb0ffc754490aff6dabbf28064be47f0f9ca0b9755976f945214965b3ace7e"},
-    {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a94e52537a0e0a85429eda9e49f272ada715506d3b2431f64b8a3e34eb5f3e75"},
-    {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:92b68b79c0da2a980b1c4197e56ac3dd0c8a149b4603747c4378914a68706979"},
-    {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:93da1d3db08a827eda74356f9f58884adb254e59b6664f64cc04cdff2cc19b0d"},
-    {file = "rpds_py-0.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:754bbed1a4ca48479e9d4182a561d001bbf81543876cdded6f695ec3d465846b"},
-    {file = "rpds_py-0.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ca449520e7484534a2a44faf629362cae62b660601432d04c482283c47eaebab"},
-    {file = "rpds_py-0.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9c4cb04a16b0f199a8c9bf807269b2f63b7b5b11425e4a6bd44bd6961d28282c"},
-    {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63804105143c7e24cee7db89e37cb3f3941f8e80c4379a0b355c52a52b6780"},
-    {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:55cd1fa4ecfa6d9f14fbd97ac24803e6f73e897c738f771a9fe038f2f11ff07c"},
-    {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f8f741b6292c86059ed175d80eefa80997125b7c478fb8769fd9ac8943a16c0"},
-    {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fc212779bf8411667234b3cdd34d53de6c2b8b8b958e1e12cb473a5f367c338"},
-    {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ad56edabcdb428c2e33bbf24f255fe2b43253b7d13a2cdbf05de955217313e6"},
-    {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a3a1e9ee9728b2c1734f65d6a1d376c6f2f6fdcc13bb007a08cc4b1ff576dc5"},
-    {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e13de156137b7095442b288e72f33503a469aa1980ed856b43c353ac86390519"},
-    {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:07f59760ef99f31422c49038964b31c4dfcfeb5d2384ebfc71058a7c9adae2d2"},
-    {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:59240685e7da61fb78f65a9f07f8108e36a83317c53f7b276b4175dc44151684"},
-    {file = "rpds_py-0.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:83cba698cfb3c2c5a7c3c6bac12fe6c6a51aae69513726be6411076185a8b24a"},
-    {file = "rpds_py-0.20.1.tar.gz", hash = "sha256:e1791c4aabd117653530dccd24108fa03cc6baf21f58b950d0a73c3b3b29a350"},
-]
-
 [[package]]
 name = "six"
 version = "1.16.0"
@@ -888,4 +702,4 @@ files = [
 [metadata]
 lock-version = "2.0"
 python-versions = ">=3.10,<4.0"
-content-hash = "71e40edbadad556ddbc12ff75589954ed48cb80a09b4d4502abe96350887155b"
+content-hash = "7ec0f89a6abb1ef31540026e962d9fbda1813ef6190f2084029a3a5afbdb50e9"
diff --git a/pyproject.toml b/pyproject.toml
index 6d65ac8..a88fd37 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,7 +14,6 @@ OWSLib = "0.32.0"
 xmltodict = "0.14.2"
 types-requests = "2.32.0.20241016"
 types-xmltodict = "0.14.0.20241009"
-jsonschema = "^4.23.0"
 pytest-mock = "^3.14.0"
 
 [tool.poetry.group.dev.dependencies]


Test patch:
diff --git a/tests/models/test_common.py b/tests/models/test_common.py
index ebf6d4b..aea04de 100644
--- a/tests/models/test_common.py
+++ b/tests/models/test_common.py
@@ -2,7 +2,7 @@ import json
 
 import pytest
 
-from geoservercloud.models import I18N, KeyDollarListDict
+from geoservercloud.models.common import I18N, KeyDollarListDict
 
 
 def test_keydollarlistdict_initialization_with_input_list():
diff --git a/tests/models/test_datastore.py b/tests/models/test_datastore.py
index b50ce5c..6dc3d4f 100644
--- a/tests/models/test_datastore.py
+++ b/tests/models/test_datastore.py
@@ -1,6 +1,7 @@
 import pytest
 
-from geoservercloud.models import KeyDollarListDict, PostGisDataStore
+from geoservercloud.models.common import KeyDollarListDict
+from geoservercloud.models.datastore import PostGisDataStore
 
 
 @pytest.fixture(scope="module")
diff --git a/tests/models/test_datastores.py b/tests/models/test_datastores.py
index 7e9ab53..2d756e4 100644
--- a/tests/models/test_datastores.py
+++ b/tests/models/test_datastores.py
@@ -1,6 +1,6 @@
 from pytest import fixture
 
-from geoservercloud.models import DataStores
+from geoservercloud.models.datastores import DataStores
 
 
 @fixture(scope="module")
diff --git a/tests/models/test_featuretype.py b/tests/models/test_featuretype.py
index f467333..1a7364e 100644
--- a/tests/models/test_featuretype.py
+++ b/tests/models/test_featuretype.py
@@ -1,7 +1,6 @@
 import json
 
-from geoservercloud.models import FeatureType
-from geoservercloud.models.featuretype import MetadataLink
+from geoservercloud.models.featuretype import FeatureType, MetadataLink
 
 
 def test_featuretype_initialization():
diff --git a/tests/models/test_featuretypes.py b/tests/models/test_featuretypes.py
index 95c8c4f..5f94701 100644
--- a/tests/models/test_featuretypes.py
+++ b/tests/models/test_featuretypes.py
@@ -1,6 +1,6 @@
 import json
 
-from geoservercloud.models import FeatureTypes
+from geoservercloud.models.featuretypes import FeatureTypes
 
 
 def test_featuretypes_initialization():
diff --git a/tests/models/test_resourcedirectory.py b/tests/models/test_resourcedirectory.py
new file mode 100644
index 0000000..aeaacaf
--- /dev/null
+++ b/tests/models/test_resourcedirectory.py
@@ -0,0 +1,46 @@
+from geoservercloud.models.resourcedirectory import Resource, ResourceDirectory
+
+
+def test_from_get_response_payload():
+    payload = {
+        "ResourceDirectory": {
+            "name": "test_resource_directory",
+            "parent": {
+                "path": "workspace/parent",
+                "name": "parent",
+                "link": {"href": "http://example.com", "type": "application/json"},
+            },
+            "children": {
+                "child": [
+                    {
+                        "name": "child1.svg",
+                        "link": {
+                            "href": "http://example.com/child1",
+                            "type": "image/svg+xml",
+                        },
+                    },
+                    {
+                        "name": "child2.xml",
+                        "link": {
+                            "href": "http://example.com/child2",
+                            "type": "application/xml",
+                        },
+                    },
+                ]
+            },
+        }
+    }
+
+    resource_directory = ResourceDirectory.from_get_response_payload(payload)
+
+    assert resource_directory.name == "test_resource_directory"
+    assert resource_directory.parent.name == "workspace/parent"
+    assert resource_directory.parent.href == "http://example.com"
+    assert resource_directory.parent.type == "application/json"
+    assert len(resource_directory.children) == 2
+    assert resource_directory.children[0].name == "child1.svg"
+    assert resource_directory.children[0].href == "http://example.com/child1"
+    assert resource_directory.children[0].type == "image/svg+xml"
+    assert resource_directory.children[1].name == "child2.xml"
+    assert resource_directory.children[1].href == "http://example.com/child2"
+    assert resource_directory.children[1].type == "application/xml"
diff --git a/tests/models/test_style.py b/tests/models/test_style.py
index c33c096..9f79b85 100644
--- a/tests/models/test_style.py
+++ b/tests/models/test_style.py
@@ -1,4 +1,4 @@
-from geoservercloud.models import Style
+from geoservercloud.models.style import Style
 
 
 def test_style_initialization():
@@ -71,7 +71,7 @@ def test_style_put_payload_with_legend():
     assert style.put_payload() == expected_payload
 
 
-def test_style_put_payload_without_legend(mocker):
+def test_style_put_payload_without_legend():
     style = Style(
         name="test_style",
         workspace_name="test_workspace",
diff --git a/tests/models/test_styles.py b/tests/models/test_styles.py
index 55357ea..d7d12af 100644
--- a/tests/models/test_styles.py
+++ b/tests/models/test_styles.py
@@ -1,6 +1,6 @@
 import pytest
 
-from geoservercloud.models import Styles
+from geoservercloud.models.styles import Styles
 
 
 @pytest.fixture
diff --git a/tests/models/test_workspace.py b/tests/models/test_workspace.py
index a8900d7..e1f4244 100644
--- a/tests/models/test_workspace.py
+++ b/tests/models/test_workspace.py
@@ -1,4 +1,4 @@
-from geoservercloud.models import Workspace
+from geoservercloud.models.workspace import Workspace
 
 
 def test_workspace_initialization():
diff --git a/tests/models/test_workspaces.py b/tests/models/test_workspaces.py
index ea53694..82655d4 100644
--- a/tests/models/test_workspaces.py
+++ b/tests/models/test_workspaces.py
@@ -1,6 +1,6 @@
 from pytest import fixture
 
-from geoservercloud.models import Workspaces
+from geoservercloud.models.workspaces import Workspaces
 
 
 @fixture(scope="module")
diff --git a/tests/services/test_resource.py b/tests/services/test_resource.py
new file mode 100644
index 0000000..10ef7a0
--- /dev/null
+++ b/tests/services/test_resource.py
@@ -0,0 +1,74 @@
+import pytest
+import responses
+
+from geoservercloud.models.resourcedirectory import ResourceDirectory
+from geoservercloud.services.restservice import RestService
+
+
+@pytest.fixture
+def rest_service():
+    yield RestService("http://geoserver", auth=("test", "test"))
+
+
+@pytest.fixture
+def resource_dir_get_response():
+    yield {
+        "ResourceDirectory": {
+            "name": "test_resource_directory",
+            "parent": {
+                "path": "workspace/parent",
+                "name": "parent",
+                "link": {"href": "http://example.com", "type": "application/json"},
+            },
+            "children": {
+                "child": [
+                    {
+                        "name": "child1.svg",
+                        "link": {
+                            "href": "http://example.com/child1",
+                            "type": "image/svg+xml",
+                        },
+                    },
+                    {
+                        "name": "child2.xml",
+                        "link": {
+                            "href": "http://example.com/child2",
+                            "type": "application/xml",
+                        },
+                    },
+                ]
+            },
+        }
+    }
+
+
+def test_get_workspace_style_resource_directory(
+    rest_service: RestService, resource_dir_get_response: dict
+):
+    with responses.RequestsMock() as rsps:
+        rsps.get(
+            url=f"{rest_service.url}/rest/resource/workspaces/test/styles",
+            status=200,
+            json=resource_dir_get_response,
+            match=[responses.matchers.header_matcher({"Accept": "application/json"})],
+        )
+        resource_dir, code = rest_service.get_resource_directory(
+            path="styles", workspace_name="test"
+        )
+        assert isinstance(resource_dir, ResourceDirectory)
+        assert len(resource_dir.children) == 2
+
+
+def test_get_global_style_resource_directory(
+    rest_service: RestService, resource_dir_get_response: dict
+):
+    with responses.RequestsMock() as rsps:
+        rsps.get(
+            url=f"{rest_service.url}/rest/resource/styles",
+            status=200,
+            json=resource_dir_get_response,
+            match=[responses.matchers.header_matcher({"Accept": "application/json"})],
+        )
+        resource_dir, code = rest_service.get_resource_directory(path="styles")
+        assert isinstance(resource_dir, ResourceDirectory)
+        assert len(resource_dir.children) == 2
diff --git a/tests/services/test_rest_endpoints.py b/tests/services/test_rest_endpoints.py
new file mode 100644
index 0000000..6fb0c3e
--- /dev/null
+++ b/tests/services/test_rest_endpoints.py
@@ -0,0 +1,100 @@
+from collections.abc import Generator
+from typing import Any
+
+import pytest
+
+from geoservercloud.services.restservice import RestService
+
+
+@pytest.fixture
+def endpoints() -> Generator[RestService.RestEndpoints, Any, None]:
+    yield RestService("http://geoserver", auth=("test", "test")).rest_endpoints
+
+
+def test_styles_endpoint(endpoints: RestService.RestEndpoints):
+    assert endpoints.styles() == "/rest/styles.json"
+
+
+def test_style_endpoint(endpoints: RestService.RestEndpoints):
+    assert endpoints.style("test_style") == "/rest/styles/test_style.json"
+
+
+@pytest.mark.parametrize(
+    "format,path",
+    [
+        ("json", "/rest/styles.json"),
+        ("xml", "/rest/styles"),
+        ("sld", "/rest/styles"),
+        ("zip", "/rest/styles"),
+    ],
+)
+def test_styles_endpoint_format(
+    endpoints: RestService.RestEndpoints, format: str, path: str
+):
+    assert endpoints.styles(format=format) == path
+
+
+@pytest.mark.parametrize(
+    "format,path",
+    [
+        ("json", "/rest/styles/test_style.json"),
+        ("xml", "/rest/styles/test_style"),
+        ("sld", "/rest/styles/test_style.sld"),
+        ("zip", "/rest/styles/test_style"),
+    ],
+)
+def test_style_endpoint_format(
+    endpoints: RestService.RestEndpoints, format: str, path: str
+):
+    assert endpoints.style(style_name="test_style", format=format) == path
+
+
+@pytest.mark.parametrize(
+    "format,path",
+    [
+        ("json", "/rest/workspaces/test/styles.json"),
+        ("xml", "/rest/workspaces/test/styles"),
+        ("sld", "/rest/workspaces/test/styles"),
+        ("zip", "/rest/workspaces/test/styles"),
+    ],
+)
+def test_workspace_styles_endpoint(
+    endpoints: RestService.RestEndpoints, format: str, path: str
+):
+    assert endpoints.styles(workspace_name="test", format=format) == path
+
+
+@pytest.mark.parametrize(
+    "format,path",
+    [
+        ("json", "/rest/workspaces/test/styles/test_style.json"),
+        ("xml", "/rest/workspaces/test/styles/test_style"),
+        ("sld", "/rest/workspaces/test/styles/test_style.sld"),
+        ("zip", "/rest/workspaces/test/styles/test_style"),
+    ],
+)
+def test_workspace_style_endpoint(
+    endpoints: RestService.RestEndpoints, format: str, path: str
+):
+    assert (
+        endpoints.style(style_name="test_style", workspace_name="test", format=format)
+        == path
+    )
+
+
+def test_resource_directory_endpoint(endpoints: RestService.RestEndpoints):
+    assert (
+        endpoints.resource_directory("styles", "test_workspace")
+        == "/rest/resource/workspaces/test_workspace/styles"
+    )
+    assert endpoints.resource_directory("styles") == "/rest/resource/styles"
+
+
+def test_resource_endpoint(endpoints: RestService.RestEndpoints):
+    assert (
+        endpoints.resource("styles", "image.svg", "test_workspace")
+        == "/rest/resource/workspaces/test_workspace/styles/image.svg"
+    )
+    assert (
+        endpoints.resource("styles", "image.svg") == "/rest/resource/styles/image.svg"
+    )
diff --git a/tests/services/test_style.py b/tests/services/test_style.py
new file mode 100644
index 0000000..be327e6
--- /dev/null
+++ b/tests/services/test_style.py
@@ -0,0 +1,34 @@
+import pytest
+import responses
+
+from geoservercloud.services.restservice import RestService
+
+STYLE = "test_style"
+
+
+@pytest.fixture
+def rest_service() -> RestService:
+    return RestService("http://geoserver", auth=("test", "test"))
+
+
+def test_get_style_no_workspace(rest_service: RestService):
+    body = b"<sld>test</sld>"
+    with responses.RequestsMock() as rsps:
+        rsps.get(
+            url=f"http://geoserver/rest/styles/{STYLE}.sld",
+            status=200,
+            body=body,
+        )
+        assert rest_service.get_style(STYLE) == (body, 200)
+
+
+def test_get_style_with_workspace(rest_service: RestService):
+    workspace_name = "test_workspace"
+    body = b"<sld>test</sld>"
+    with responses.RequestsMock() as rsps:
+        rsps.get(
+            url=f"http://geoserver/rest/workspaces/{workspace_name}/styles/{STYLE}.sld",
+            status=200,
+            body=body,
+        )
+        assert rest_service.get_style(STYLE, workspace_name) == (body, 200)
diff --git a/tests/test_datastore.py b/tests/test_datastore.py
index 46ae8ae..8dd3f5f 100644
--- a/tests/test_datastore.py
+++ b/tests/test_datastore.py
@@ -6,7 +6,6 @@ import responses
 from responses import matchers
 
 from geoservercloud import GeoServerCloud
-from geoservercloud.models import PostGisDataStore
 from tests.conftest import GEOSERVER_URL
 
 WORKSPACE = "test_workspace"
diff --git a/tests/test_style.py b/tests/test_style.py
index a0ea46b..4eb6b38 100644
--- a/tests/test_style.py
+++ b/tests/test_style.py
@@ -2,9 +2,10 @@ from pathlib import Path
 
 import pytest
 import responses
+from requests import Response
 
 from geoservercloud import GeoServerCloud
-from tests.conftest import GEOSERVER_URL
+from geoservercloud.services.restclient import RestClient
 
 STYLE = "test_style"
 
@@ -12,18 +13,18 @@ STYLE = "test_style"
 def test_get_styles_no_workspace(geoserver: GeoServerCloud):
     with responses.RequestsMock() as rsps:
         rsps.get(
-            url=f"{GEOSERVER_URL}/rest/styles.json",
+            url=f"{geoserver.url}/rest/styles.json",
             status=200,
             json={
                 "styles": {
                     "style": [
                         {
                             "name": "style1",
-                            "href": f"{GEOSERVER_URL}/rest/styles/style1.json",
+                            "href": f"{geoserver.url}/rest/styles/style1.json",
                         },
                         {
                             "name": "style2",
-                            "href": f"{GEOSERVER_URL}/rest/styles/style2.json",
+                            "href": f"{geoserver.url}/rest/styles/style2.json",
                         },
                     ]
                 }
@@ -39,18 +40,18 @@ def test_get_styles_with_workspace(geoserver: GeoServerCloud):
     workspace_name = "test_workspace"
     with responses.RequestsMock() as rsps:
         rsps.get(
-            url=f"{GEOSERVER_URL}/rest/workspaces/{workspace_name}/styles.json",
+            url=f"{geoserver.url}/rest/workspaces/{workspace_name}/styles.json",
             status=200,
             json={
                 "styles": {
                     "style": [
                         {
                             "name": "style3",
-                            "href": f"{GEOSERVER_URL}/rest/workspaces/{workspace_name}/styles/style3.json",
+                            "href": f"{geoserver.url}/rest/workspaces/{workspace_name}/styles/style3.json",
                         },
                         {
                             "name": "style4",
-                            "href": f"{GEOSERVER_URL}/rest/workspaces/{workspace_name}/styles/style4.json",
+                            "href": f"{geoserver.url}/rest/workspaces/{workspace_name}/styles/style4.json",
                         },
                     ]
                 }
@@ -62,10 +63,10 @@ def test_get_styles_with_workspace(geoserver: GeoServerCloud):
     assert code == 200
 
 
-def test_get_style_no_workspace(geoserver: GeoServerCloud):
+def test_get_style_definition_no_workspace(geoserver: GeoServerCloud):
     with responses.RequestsMock() as rsps:
         rsps.get(
-            url=f"{GEOSERVER_URL}/rest/styles/{STYLE}.json",
+            url=f"{geoserver.url}/rest/styles/{STYLE}.json",
             status=200,
             json={
                 "style": {
@@ -76,7 +77,7 @@ def test_get_style_no_workspace(geoserver: GeoServerCloud):
                 }
             },
         )
-        content, code = geoserver.get_style(STYLE)
+        content, code = geoserver.get_style_definition(STYLE)
 
     assert content == {
         "name": STYLE,
@@ -87,11 +88,11 @@ def test_get_style_no_workspace(geoserver: GeoServerCloud):
     assert code == 200
 
 
-def test_get_style_with_workspace(geoserver: GeoServerCloud):
+def test_get_style_definition_with_workspace(geoserver: GeoServerCloud):
     workspace_name = "test_workspace"
     with responses.RequestsMock() as rsps:
         rsps.get(
-            url=f"{GEOSERVER_URL}/rest/workspaces/{workspace_name}/styles/{STYLE}.json",
+            url=f"{geoserver.url}/rest/workspaces/{workspace_name}/styles/{STYLE}.json",
             status=200,
             json={
                 "style": {
@@ -103,7 +104,7 @@ def test_get_style_with_workspace(geoserver: GeoServerCloud):
                 }
             },
         )
-        content, code = geoserver.get_style(STYLE, workspace_name)
+        content, code = geoserver.get_style_definition(STYLE, workspace_name)
 
     assert content == {
         "name": STYLE,
@@ -115,22 +116,60 @@ def test_get_style_with_workspace(geoserver: GeoServerCloud):
     assert code == 200
 
 
-def test_create_style(geoserver: GeoServerCloud) -> None:
+def test_create_style_definition(mocker, geoserver: GeoServerCloud) -> None:
+    # Use mocker to check the POST request payload (binary payload matching is not supported by responses)
+    response = Response()
+    response.status_code = 201
+    response._content = b"test_style"
+    mock_post = mocker.patch.object(
+        geoserver.rest_service.rest_client, "post", return_value=response
+    )
+    expected_xml = """
+    <style>
+        <name>test_style</name>
+        <format>sld</format>
+        <languageVersion><version>1.0.0</version></languageVersion>
+        <workspace><name>test_workspace</name></workspace>
+        <filename>style.sld</filename>
+    </style>
+    """
+    expected_payload: bytes = "".join(expected_xml.split()).encode("utf-8")
+    with responses.RequestsMock() as rsps:
+        rsps.get(
+            url=f"{geoserver.url}/rest/workspaces/test_workspace/styles/{STYLE}",
+            status=404,
+        )
+        geoserver.create_style_definition(
+            style_name=STYLE, filename="style.sld", workspace_name="test_workspace"
+        )
+        mock_post.assert_called_with(
+            "/rest/workspaces/test_workspace/styles",
+            data=expected_payload,
+            headers={"Content-Type": "text/xml"},
+        )
+
+
+def test_create_style_from_file(geoserver: GeoServerCloud) -> None:
     file_path = (Path(__file__).parent / "resources/style.sld").resolve()
     with responses.RequestsMock() as rsps:
         rsps.get(
-            url=f"{GEOSERVER_URL}/rest/styles/{STYLE}.json",
+            url=f"{geoserver.url}/rest/styles/{STYLE}.sld",
             status=404,
         )
         rsps.post(
-            url=f"{GEOSERVER_URL}/rest/styles.json",
+            url=f"{geoserver.url}/rest/styles",
             status=201,
             body=b"test_style",
+            match=[
+                responses.matchers.header_matcher(
+                    {"Content-Type": "application/vnd.ogc.sld+xml"}
+                )
+            ],
             # Matching of binary content is not supported by responses
         )
 
         content, code = geoserver.create_style_from_file(
-            style=STYLE,
+            style_name=STYLE,
             file=str(file_path),
         )
 
@@ -138,22 +177,27 @@ def test_create_style(geoserver: GeoServerCloud) -> None:
         assert code == 201
 
 
-def test_update_style(geoserver: GeoServerCloud) -> None:
+def test_update_style_from_file(geoserver: GeoServerCloud) -> None:
     file_path = (Path(__file__).parent / "resources/style.sld").resolve()
     with responses.RequestsMock() as rsps:
         rsps.get(
-            url=f"{GEOSERVER_URL}/rest/styles/{STYLE}.json",
+            url=f"{geoserver.url}/rest/styles/{STYLE}.sld",
             status=200,
         )
         rsps.put(
-            url=f"{GEOSERVER_URL}/rest/styles/{STYLE}.json",
+            url=f"{geoserver.url}/rest/styles/{STYLE}.sld",
             status=200,
             body=b"",
+            match=[
+                responses.matchers.header_matcher(
+                    {"Content-Type": "application/vnd.ogc.sld+xml"}
+                )
+            ],
             # Matching of binary content is not supported by responses
         )
 
         content, code = geoserver.create_style_from_file(
-            style=STYLE,
+            style_name=STYLE,
             file=str(file_path),
         )
 
@@ -161,22 +205,25 @@ def test_update_style(geoserver: GeoServerCloud) -> None:
         assert code == 200
 
 
-def test_create_style_zip(geoserver: GeoServerCloud) -> None:
+def test_create_style_from_file_zip(geoserver: GeoServerCloud) -> None:
     file_path = (Path(__file__).parent / "resources/style.zip").resolve()
     with responses.RequestsMock() as rsps:
         rsps.get(
-            url=f"{GEOSERVER_URL}/rest/styles/{STYLE}.json",
+            url=f"{geoserver.url}/rest/styles/{STYLE}",
             status=404,
         )
         rsps.post(
-            url=f"{GEOSERVER_URL}/rest/styles.json",
+            url=f"{geoserver.url}/rest/styles",
             status=201,
             body=b"test_style",
+            match=[
+                responses.matchers.header_matcher({"Content-Type": "application/zip"})
+            ],
             # Matching of binary content is not supported by responses
         )
 
         content, code = geoserver.create_style_from_file(
-            style=STYLE,
+            style_name=STYLE,
             file=str(file_path),
         )
 
@@ -184,11 +231,11 @@ def test_create_style_zip(geoserver: GeoServerCloud) -> None:
         assert code == 201
 
 
-def test_create_style_unsupported_format(geoserver: GeoServerCloud) -> None:
+def test_create_style_from_file_unsupported_format(geoserver: GeoServerCloud) -> None:
     with responses.RequestsMock():
         with pytest.raises(ValueError) as excinfo:
             geoserver.create_style_from_file(
-                style=STYLE,
+                style_name=STYLE,
                 file="resources/style.txt",
             )
         assert "Unsupported file extension" in str(excinfo.value)