diff --git a/geoservercloud/geoservercloud.py b/geoservercloud/geoservercloud.py index a20ecab..fe3bf11 100644 --- a/geoservercloud/geoservercloud.py +++ b/geoservercloud/geoservercloud.py @@ -10,7 +10,24 @@ from requests import Response from geoservercloud import utils -from geoservercloud.restservice import RestService +from geoservercloud.models import ( + DataStores, + FeatureType, + FeatureTypes, + KeyDollarListDict, + PostGisDataStore, + Style, + Styles, + Workspace, + Workspaces, +) +from geoservercloud.services import ( + AclEndpoints, + GwcEndpoints, + OwsEndpoints, + RestEndpoints, + RestService, +) from geoservercloud.templates import Templates @@ -27,15 +44,15 @@ def __init__( self.password: str = password self.auth: tuple[str, str] = (user, password) self.rest_service: RestService = RestService(url, self.auth) + self.acl_endpoints: AclEndpoints = AclEndpoints() + self.gwc_endpoints: GwcEndpoints = GwcEndpoints() + self.ows_endpoints: OwsEndpoints = OwsEndpoints() + self.rest_endpoints: RestEndpoints = RestEndpoints() self.wms: WebMapService_1_3_0 | None = None self.wmts: WebMapTileService | None = None self.default_workspace: str | None = None self.default_datastore: str | None = None - @staticmethod - def workspace_wms_settings_path(workspace: str) -> str: - return f"/rest/services/wms/workspaces/{workspace}/settings.json" - @staticmethod def get_wmts_layer_bbox( url: str, layer_name: str @@ -48,9 +65,9 @@ def get_wmts_layer_bbox( def create_wms(self) -> None: if self.default_workspace: - path: str = f"/{self.default_workspace}/wms" + path: str = self.ows_endpoints.workspace_wms(self.default_workspace) else: - path = "/wms" + path = self.ows_endpoints.wms() self.wms = WebMapService_1_3_0( f"{self.url}{path}", username=self.user, @@ -59,7 +76,7 @@ def create_wms(self) -> None: ) def create_wmts(self) -> None: - path = "/gwc/service/wmts" + path = self.ows_endpoints.wmts() self.wmts = WebMapTileService( f"{self.url}{path}", version="1.0.0", @@ -67,9 +84,14 @@ def create_wmts(self) -> None: password=self.password, ) + def get_workspaces(self) -> Workspaces: + response: Response = self.get_request(self.rest_endpoints.workspaces()) + workspaces = Workspaces.from_response(response) + return workspaces + def create_workspace( self, - workspace: str, + workspace_name: str, isolated: bool = False, set_default_workspace: bool = False, ) -> Response: @@ -77,115 +99,150 @@ def create_workspace( Create a workspace in GeoServer, if it does not already exist. It if exists, update it """ - payload: dict[str, dict[str, Any]] = { - "workspace": { - "name": workspace, - "isolated": isolated, - } - } - response: Response = self.post_request("/rest/workspaces.json", json=payload) + response: Response = self.post_request( + self.rest_endpoints.workspaces(), + json=Workspace(workspace_name, isolated).post_payload(), + ) if response.status_code == 409: response = self.put_request( - f"/rest/workspaces/{workspace}.json", json=payload + self.rest_endpoints.workspace(workspace_name), + json=Workspace(workspace_name, isolated).put_payload(), ) if set_default_workspace: - self.default_workspace = workspace + self.default_workspace = workspace_name return response - def delete_workspace(self, workspace: str) -> Response: + def delete_workspace(self, workspace_name: str) -> Response: """ Delete a GeoServer workspace (recursively) """ - path: str = f"/rest/workspaces/{workspace}.json?recurse=true" - response: Response = self.delete_request(path) - if self.default_workspace == workspace: + response: Response = self.delete_request( + self.rest_endpoints.workspace(workspace_name), params={"recurse": "true"} + ) + if self.default_workspace == workspace_name: self.default_workspace = None self.wms = None self.wmts = None return response def recreate_workspace( - self, workspace: str, set_default_workspace: bool = False + self, workspace_name: str, set_default_workspace: bool = False ) -> Response: """ Create a workspace in GeoServer, and first delete it if it already exists. """ - self.delete_workspace(workspace) + self.delete_workspace(workspace_name) return self.create_workspace( - workspace, set_default_workspace=set_default_workspace + workspace_name, set_default_workspace=set_default_workspace ) - def publish_workspace(self, workspace) -> Response: + def publish_workspace(self, workspace_name) -> Response: """ Publish the WMS service for a given workspace """ - path: str = f"{self.workspace_wms_settings_path(workspace)}" - - data: dict[str, dict[str, Any]] = Templates.workspace_wms(workspace) - return self.put_request(path, json=data) + data: dict[str, dict[str, Any]] = Templates.workspace_wms(workspace_name) + return self.put_request( + self.rest_endpoints.workspace_wms_settings(workspace_name), json=data + ) def set_default_locale_for_service( - self, workspace: str, locale: str | None + self, workspace_name: str, locale: str | None ) -> Response: """ Set a default language for localized WMS requests """ - path: str = self.workspace_wms_settings_path(workspace) data: dict[str, dict[str, Any]] = { "wms": { "defaultLocale": locale, } } - return self.put_request(path, json=data) + return self.put_request( + self.rest_endpoints.workspace_wms_settings(workspace_name), json=data + ) - def unset_default_locale_for_service(self, workspace) -> None: + def unset_default_locale_for_service(self, workspace_name) -> None: """ Remove the default language for localized WMS requests """ - self.set_default_locale_for_service(workspace, None) + self.set_default_locale_for_service(workspace_name, None) + + def get_datastores(self, workspace_name: str) -> dict[str, Any]: + """ + Get all datastores for a given workspace + """ + response = self.get_request(self.rest_endpoints.datastores(workspace_name)) + return DataStores.from_response(response).datastores + + def get_postgis_datastore( + self, workspace_name: str, datastore_name: str + ) -> dict[str, Any]: + """ + Get a specific datastore + """ + response = self.get_request( + self.rest_endpoints.datastore(workspace_name, datastore_name) + ) + return PostGisDataStore.from_response(response) def create_pg_datastore( self, - workspace: str, - datastore: str, + workspace_name: str, + datastore_name: str, pg_host: str, pg_port: int, pg_db: str, pg_user: str, pg_password: str, pg_schema: str = "public", + description: str | None = None, set_default_datastore: bool = False, ) -> Response | None: """ Create a PostGIS datastore from the DB connection parameters, or update it if it already exist. """ response: None | Response = None - path = f"/rest/workspaces/{workspace}/datastores.json" - resource_path = f"/rest/workspaces/{workspace}/datastores/{datastore}.json" - payload: dict[str, dict[str, Any]] = Templates.postgis_data_store( - datastore=datastore, - pg_host=pg_host, - pg_port=pg_port, - pg_db=pg_db, - pg_user=pg_user, - pg_password=pg_password, - namespace=f"http://{workspace}", - pg_schema=pg_schema, + datastore = PostGisDataStore( + workspace_name, + datastore_name, + connection_parameters=KeyDollarListDict( + input_dict={ + "dbtype": "postgis", + "host": pg_host, + "port": pg_port, + "database": pg_db, + "user": pg_user, + "passwd": pg_password, + "schema": pg_schema, + "namespace": f"http://{workspace_name}", + "Expose primary keys": "true", + } + ), + data_store_type="PostGIS", + description=description, ) - if not self.resource_exists(resource_path): - response = self.post_request(path, json=payload) + payload = datastore.put_payload() + + if not self.resource_exists( + self.rest_endpoints.datastore(workspace_name, datastore_name) + ): + response = self.post_request( + self.rest_endpoints.datastores(workspace_name), json=payload + ) else: - response = self.put_request(resource_path, json=payload) + response = self.put_request( + self.rest_endpoints.datastore(workspace_name, datastore_name), + json=payload, + ) if set_default_datastore: - self.default_datastore = datastore + self.default_datastore = datastore_name return response def create_jndi_datastore( self, - workspace: str, - datastore: str, + workspace_name: str, + datastore_name: str, jndi_reference: str, pg_schema: str = "public", description: str | None = None, @@ -195,46 +252,88 @@ def create_jndi_datastore( Create a PostGIS datastore from JNDI resource, or update it if it already exist. """ response: None | Response = None - path = f"/rest/workspaces/{workspace}/datastores.json" - resource_path = f"/rest/workspaces/{workspace}/datastores/{datastore}.json" - payload: dict[str, dict[str, Any]] = Templates.postgis_jndi_data_store( - datastore=datastore, - jndi_reference=jndi_reference, - namespace=f"http://{workspace}", - pg_schema=pg_schema, + datastore = PostGisDataStore( + workspace_name, + datastore_name, + connection_parameters=KeyDollarListDict( + input_dict={ + "dbtype": "postgis", + "jndiReferenceName": jndi_reference, + "schema": pg_schema, + "namespace": f"http://{workspace_name}", + "Expose primary keys": "true", + } + ), + data_store_type="PostGIS (JNDI)", description=description, ) - if not self.resource_exists(resource_path): - response = self.post_request(path, json=payload) + payload = datastore.put_payload() + if not self.resource_exists( + self.rest_endpoints.datastore(workspace_name, datastore_name) + ): + response = self.post_request( + self.rest_endpoints.datastores(workspace_name), json=payload + ) else: - response = self.put_request(resource_path, json=payload) + response = self.put_request( + self.rest_endpoints.datastore(workspace_name, datastore_name), + json=payload, + ) if set_default_datastore: - self.default_datastore = datastore + self.default_datastore = datastore_name return response def create_wmts_store( self, - workspace: str, + workspace_name: str, name: str, capabilities: str, ) -> Response: """ Create a cascaded WMTS store, or update it if it already exist. """ - path = f"/rest/workspaces/{workspace}/wmtsstores.json" - resource_path = f"/rest/workspaces/{workspace}/wmtsstores/{name}.json" - payload = Templates.wmts_store(workspace, name, capabilities) - if not self.resource_exists(resource_path): - return self.post_request(path, json=payload) + payload = Templates.wmts_store(workspace_name, name, capabilities) + if not self.resource_exists( + self.rest_endpoints.wmtsstore(workspace_name, name) + ): + return self.post_request( + self.rest_endpoints.wmtsstores(workspace_name), json=payload + ) else: - return self.put_request(resource_path, json=payload) + return self.put_request( + self.rest_endpoints.wmtsstore(workspace_name, name), json=payload + ) + + def get_feature_types( + self, workspace_name: str, datastore_name: str + ) -> dict[str, Any]: + """ + Get all feature types for a given workspace and datastore + """ + featuretypes = FeatureTypes.from_response( + self.get_request( + self.rest_endpoints.featuretypes(workspace_name, datastore_name) + ) + ) + return featuretypes + + def get_feature_type( + self, workspace_name: str, datastore_name: str, feature_type_name: str + ) -> dict[str, Any]: + return FeatureType.from_response( + self.get_request( + self.rest_endpoints.featuretype( + workspace_name, datastore_name, feature_type_name + ) + ) + ) def create_feature_type( self, layer: str, - workspace: str | None = None, + workspace_name: str | None = None, datastore: str | None = None, title: str | dict = "Default title", abstract: str | dict = "Default abstract", @@ -244,21 +343,15 @@ def create_feature_type( """ Create a feature type or update it if it already exist. """ - workspace = workspace or self.default_workspace - if not workspace: + workspace_name = workspace_name or self.default_workspace + if not workspace_name: raise ValueError("Workspace not provided") datastore = datastore or self.default_datastore if not datastore: raise ValueError("Datastore not provided") - path: str = ( - f"/rest/workspaces/{workspace}/datastores/{datastore}/featuretypes.json" - ) - resource_path: str = ( - f"/rest/workspaces/{workspace}/datastores/{datastore}/featuretypes/{layer}.json" - ) payload: dict[str, dict[str, Any]] = Templates.feature_type( layer=layer, - workspace=workspace, + workspace=workspace_name, datastore=datastore, attributes=utils.convert_attributes(attributes), epsg=epsg, @@ -272,15 +365,23 @@ def create_feature_type( else: payload["featureType"]["abstract"] = abstract - if not self.resource_exists(resource_path): - return self.post_request(path, json=payload) + if not self.resource_exists( + self.rest_endpoints.featuretype(workspace_name, datastore, layer) + ): + return self.post_request( + self.rest_endpoints.featuretypes(workspace_name, datastore), + json=payload, + ) else: - return self.put_request(resource_path, json=payload) + return self.put_request( + self.rest_endpoints.featuretype(workspace_name, datastore, layer), + json=payload, + ) def create_layer_group( self, group: str, - workspace: str | None, + workspace_name: str | None, layers: list[str], title: str | dict, abstract: str | dict, @@ -290,28 +391,32 @@ def create_layer_group( """ Create a layer group if it does not already exist. """ - workspace = workspace or self.default_workspace - if not workspace: + workspace_name = workspace_name or self.default_workspace + if not workspace_name: raise ValueError("Workspace not provided") - path: str = f"/rest/workspaces/{workspace}/layergroups.json" - resource_path: str = f"/rest/workspaces/{workspace}/layergroups/{group}.json" payload: dict[str, dict[str, Any]] = Templates.layer_group( group=group, layers=layers, - workspace=workspace, + workspace=workspace_name, title=title, abstract=abstract, epsg=epsg, mode=mode, ) - if not self.resource_exists(resource_path): - return self.post_request(path, json=payload) + if not self.resource_exists( + self.rest_endpoints.layergroup(workspace_name, group) + ): + return self.post_request( + self.rest_endpoints.layergroups(workspace_name), json=payload + ) else: - return self.put_request(resource_path, json=payload) + return self.put_request( + self.rest_endpoints.layergroup(workspace_name, group), json=payload + ) def create_wmts_layer( self, - workspace: str, + workspace_name: str, wmts_store: str, native_layer: str, published_layer: str | None = None, @@ -324,19 +429,23 @@ def create_wmts_layer( """ if not published_layer: published_layer = native_layer - resource_path = f"/rest/workspaces/{workspace}/wmtsstores/{wmts_store}/layers/{published_layer}.json" - if self.resource_exists(resource_path): - self.delete_request(resource_path, params={"recurse": "true"}) - wmts_store_path = f"/rest/workspaces/{workspace}/wmtsstores/{wmts_store}.json" + if self.resource_exists( + self.rest_endpoints.wmtslayer(workspace_name, wmts_store, published_layer) + ): + self.delete_request( + self.rest_endpoints.wmtslayer( + workspace_name, wmts_store, published_layer + ), + params={"recurse": "true"}, + ) capabilities_url = ( - self.get_request(wmts_store_path) + self.get_request(self.rest_endpoints.wmtsstore(workspace_name, wmts_store)) .json() .get("wmtsStore") .get("capabilitiesURL") ) wgs84_bbox = self.get_wmts_layer_bbox(capabilities_url, native_layer) - path = f"/rest/workspaces/{workspace}/wmtsstores/{wmts_store}/layers.json" payload = Templates.wmts_layer( published_layer, native_layer, @@ -346,48 +455,77 @@ def create_wmts_layer( international_abstract=international_abstract, ) - return self.post_request(path, json=payload) + return self.post_request( + self.rest_endpoints.wmtslayers(workspace_name, wmts_store), json=payload + ) - def get_gwc_layer(self, workspace: str, layer: str) -> dict[str, Any] | None: - path = f"/gwc/rest/layers/{workspace}:{layer}.json" - response = self.get_request(path) + def get_gwc_layer(self, workspace_name: str, layer: str) -> dict[str, Any] | None: + response = self.get_request(self.gwc_endpoints.layer(workspace_name, layer)) if response.status_code == 404: return None return response.json() def publish_gwc_layer( - self, workspace: str, layer: str, epsg: int = 4326 + self, workspace_name: str, layer: str, epsg: int = 4326 ) -> Response | None: # Reload config to make sure GWC is aware of GeoServer layers self.post_request( - "/gwc/rest/reload", + self.gwc_endpoints.reload(), headers={"Content-Type": "application/json"}, data="reload_configuration=1", # type: ignore ) # Do not re-publish an existing layer - if self.get_gwc_layer(workspace, layer): + if self.get_gwc_layer(workspace_name, layer): return None - payload = Templates.gwc_layer(workspace, layer, f"EPSG:{epsg}") + payload = Templates.gwc_layer(workspace_name, layer, f"EPSG:{epsg}") return self.put_request( - f"/gwc/rest/layers/{workspace}:{layer}.json", + self.gwc_endpoints.layer(workspace_name, layer), json=payload, ) + def get_styles(self, workspace_name: str | None = None) -> dict[str, Any]: + """ + Get all styles for a given workspace + """ + path = ( + self.rest_endpoints.styles() + if not workspace_name + else self.rest_endpoints.workspace_styles(workspace_name) + ) + styles = Styles.from_response(self.get_request(path)).styles + return styles + + def get_style( + self, style: str, workspace_name: str | None = None + ) -> dict[str, Any]: + """ + Get a specific style + """ + path = ( + self.rest_endpoints.style(style) + if not workspace_name + else self.rest_endpoints.workspace_style(workspace_name, style) + ) + return Style.from_response(self.get_request(path)) + + # TODO: add a create_style method that takes a Style object as input def create_style_from_file( self, style: str, file: str, - workspace: str | None = None, + workspace_name: str | None = None, ) -> Response: """Create a style from a file, or update it if it already exists. Supported file extensions are .sld and .zip.""" path = ( - "/rest/styles" if not workspace else f"/rest/workspaces/{workspace}/styles" + self.rest_endpoints.styles() + if not workspace_name + else self.rest_endpoints.workspace_styles(workspace_name) ) resource_path = ( - f"/rest/styles/{style}.json" - if not workspace - else f"/rest/workspaces/{workspace}/styles/{style}.json" + self.rest_endpoints.style(style) + if not workspace_name + else self.rest_endpoints.workspace_style(workspace_name, style) ) file_ext = os.path.splitext(file)[1] @@ -407,16 +545,17 @@ def create_style_from_file( return self.put_request(resource_path, data=data, headers=headers) def set_default_layer_style( - self, layer: str, workspace: str, style: str + self, layer: str, workspace_name: str, style: str ) -> Response: - path = f"/rest/layers/{workspace}:{layer}.json" data = {"layer": {"defaultStyle": {"name": style}}} - return self.put_request(path, json=data) + return self.put_request( + self.rest_endpoints.workspace_layer(workspace_name, layer), json=data + ) def get_wms_capabilities( - self, workspace: str, accept_languages=None + self, workspace_name: str, accept_languages=None ) -> dict[str, Any]: - path: str = f"/{workspace}/wms" + path: str = self.ows_endpoints.workspace_wms(workspace_name) params: dict[str, str] = { "service": "WMS", "version": "1.3.0", @@ -428,28 +567,29 @@ def get_wms_capabilities( return xmltodict.parse(response.content) def get_wms_layers( - self, workspace: str, accept_languages: str | None = None + self, workspace_name: str, accept_languages: str | None = None ) -> Any | dict[str, Any]: capabilities: dict[str, Any] = self.get_wms_capabilities( - workspace, accept_languages + workspace_name, accept_languages ) try: return capabilities["WMS_Capabilities"]["Capability"]["Layer"] except KeyError: return capabilities - def get_wfs_capabilities(self, workspace: str) -> dict[str, Any]: - path: str = f"/{workspace}/wfs" + def get_wfs_capabilities(self, workspace_name: str) -> dict[str, Any]: params: dict[str, str] = { "service": "WFS", "version": "1.1.0", "request": "GetCapabilities", } - response: Response = self.get_request(path, params=params) + response: Response = self.get_request( + self.ows_endpoints.workspace_wfs(workspace_name), params=params + ) return xmltodict.parse(response.content) - def get_wfs_layers(self, workspace: str) -> Any | dict[str, Any]: - capabilities: dict[str, Any] = self.get_wfs_capabilities(workspace) + def get_wfs_layers(self, workspace_name: str) -> Any | dict[str, Any]: + capabilities: dict[str, Any] = self.get_wfs_capabilities(workspace_name) try: return capabilities["wfs:WFS_Capabilities"]["FeatureTypeList"] except KeyError: @@ -527,16 +667,16 @@ def get_legend_graphic( format: str = "image/png", language: str | None = None, style: str | None = None, - workspace: str | None = None, + workspace_name: str | None = None, ) -> Response: """ WMS GetLegendGraphic request """ path: str - if not workspace: - path = "/wms" + if not workspace_name: + path = self.ows_endpoints.wms() else: - path = f"/{workspace}/wms" + path = self.ows_endpoints.workspace_wms(workspace_name) params: dict[str, Any] = { "service": "WMS", "version": "1.3.0", @@ -572,7 +712,7 @@ def get_tile( def get_feature( self, - workspace: str, + workspace_name: str, type_name: str, feature_id: int | None = None, max_feature: int | None = None, @@ -581,7 +721,8 @@ def get_feature( """WFS GetFeature request Return the feature(s) as dict if found, otherwise return the raw response content as bytes """ - path = f"/{workspace}/wfs" + # FIXME: we should consider also the global wfs endpoint + path = self.ows_endpoints.workspace_wfs(workspace_name) params = { "service": "WFS", "version": "1.1.0", @@ -601,14 +742,17 @@ def get_feature( def describe_feature_type( self, - workspace: str, + workspace_name: str | None = None, type_name: str | None = None, format: str = "application/json", ) -> dict[str, Any] | bytes: """WFS DescribeFeatureType request Return the feature type(s) as dict if found, otherwise return the raw response content as bytes """ - path = f"/{workspace}/wfs" + if not workspace_name: + path = self.ows_endpoints.wfs() + else: + path = self.ows_endpoints.workspace_wfs(workspace_name) params = { "service": "WFS", "version": "1.1.0", @@ -625,7 +769,7 @@ def describe_feature_type( def get_property_value( self, - workspace: str, + workspace_name: str, type_name: str, property: str, ) -> dict | list | bytes: @@ -633,7 +777,8 @@ def get_property_value( Return the properties as dict (if one feature was found), a list (if multiple features were found) or an empty dict if no feature was found. Otherwise throw a requests.exceptions.HTTPError """ - path = f"/{workspace}/wfs" + # FIXME: we should consider also the global wfs endpoint + path = self.ows_endpoints.workspace_wfs(workspace_name) params = { "service": "WFS", "version": "2.0.0", @@ -661,7 +806,7 @@ def create_user(self, user: str, password: str, enabled: bool = True) -> Respons } } return self.post_request( - "/rest/security/usergroup/users", json=payload, headers=headers + self.rest_endpoints.users(), json=payload, headers=headers ) def update_user( @@ -677,26 +822,26 @@ def update_user( if enabled is not None: payload["user"]["enabled"] = enabled return self.post_request( - f"/rest/security/usergroup/user/{user}", json=payload, headers=headers + self.rest_endpoints.user(user), json=payload, headers=headers ) def delete_user(self, user: str) -> Response: """ Delete a GeoServer user """ - return self.delete_request(f"/rest/security/usergroup/user/{user}") + return self.delete_request(self.rest_endpoints.user(user)) def create_role(self, role_name: str) -> Response: """ Create a GeoServer role """ - return self.post_request(f"/rest/security/roles/role/{role_name}") + return self.post_request(self.rest_endpoints.role(role_name)) def delete_role(self, role_name: str) -> Response: """ Delete a GeoServer role """ - return self.delete_request(f"/rest/security/roles/role/{role_name}") + return self.delete_request(self.rest_endpoints.role(role_name)) def create_role_if_not_exists(self, role_name: str) -> Response | None: """ @@ -711,7 +856,7 @@ def role_exists(self, role_name: str) -> bool: Check if a GeoServer role exists """ response = self.get_request( - f"/rest/security/roles", headers={"Accept": "application/json"} + self.rest_endpoints.roles(), headers={"Accept": "application/json"} ) roles = response.json().get("roles", []) return role_name in roles @@ -720,7 +865,7 @@ def get_user_roles(self, user: str) -> list[str] | Response: """ Get all roles assigned to a GeoServer user """ - response = self.get_request(f"/rest/security/roles/user/{user}.json") + response = self.get_request(self.rest_endpoints.user_roles(user)) try: return response.json().get("roles") except JSONDecodeError: @@ -730,13 +875,13 @@ def assign_role_to_user(self, user: str, role: str) -> Response: """ Assign a role to a GeoServer user """ - return self.post_request(f"/rest/security/roles/role/{role}/user/{user}") + return self.post_request(self.rest_endpoints.role_user(role, user)) def remove_role_from_user(self, user: str, role: str) -> Response: """ Remove a role from a GeoServer user """ - return self.delete_request(f"/rest/security/roles/role/{role}/user/{user}") + return self.delete_request(self.rest_endpoints.role_user(role, user)) def create_acl_admin_rule( self, @@ -744,20 +889,19 @@ def create_acl_admin_rule( access: str = "ADMIN", role: str | None = None, user: str | None = None, - workspace: str | None = None, + workspace_name: str | None = None, ) -> Response: """ Create a GeoServer ACL admin rule """ - path = "/acl/api/adminrules" return self.post_request( - path, + self.acl_endpoints.adminrules(), json={ "priority": priority, "access": access, "role": role, "user": user, - "workspace": workspace, + "workspace": workspace_name, }, ) @@ -765,22 +909,19 @@ def delete_acl_admin_rule(self, id: int) -> Response: """ Delete a GeoServer ACL admin rule by id """ - path = f"/acl/api/adminrules/id/{id}" - return self.delete_request(path) + return self.delete_request(self.acl_endpoints.adminrule(id)) def delete_all_acl_admin_rules(self) -> Response: """ Delete all existing GeoServer ACL admin rules """ - path = "/acl/api/adminrules" - return self.delete_request(path) + return self.delete_request(self.acl_endpoints.adminrules()) def get_acl_rules(self) -> dict[str, Any]: """ Return all GeoServer ACL data rules """ - path = "/acl/api/rules" - response = self.get_request(path) + response = self.get_request(self.acl_endpoints.rules()) return response.json() def create_acl_rules_for_requests( @@ -790,7 +931,7 @@ def create_acl_rules_for_requests( access: str = "DENY", role: str | None = None, service: str | None = None, - workspace: str | None = None, + workspace_name: str | None = None, ) -> list[Response]: """ Create ACL rules for multiple type of OGC requests @@ -804,7 +945,7 @@ def create_acl_rules_for_requests( role=role, request=request, service=service, - workspace=workspace, + workspace_name=workspace_name, ) ) return responses @@ -817,12 +958,11 @@ def create_acl_rule( user: str | None = None, service: str | None = None, request: str | None = None, - workspace: str | None = None, + workspace_name: str | None = None, ) -> Response: """ Create a GeoServer ACL data rule """ - path = "/acl/api/rules" json = {"priority": priority, "access": access} if role: json["role"] = role @@ -832,33 +972,22 @@ def create_acl_rule( json["service"] = service if request: json["request"] = request - if workspace: - json["workspace"] = workspace - return self.post_request(path, json=json) + if workspace_name: + json["workspace"] = workspace_name + return self.post_request(self.acl_endpoints.rules(), json=json) def delete_all_acl_rules(self) -> Response: """ Delete all existing GeoServer ACL data rules """ - path = "/acl/api/rules" - return self.delete_request(path) - - def create_or_update_resource(self, path, resource_path, payload) -> Response: - """ - Create a GeoServer resource or update it if it already exists - """ - if not self.resource_exists(resource_path): - return self.post_request(path, json=payload) - else: - return self.put_request(resource_path, json=payload) + return self.delete_request(self.acl_endpoints.rules()) def create_gridset(self, epsg: int) -> Response | None: """ Create a gridset for GeoWebCache for a given projection Supported EPSG codes are 2056, 21781 and 3857 """ - resource_path: str = f"/gwc/rest/gridsets/EPSG:{epsg}.xml" - if self.resource_exists(resource_path): + if self.resource_exists(self.gwc_endpoints.gridset(epsg)): return None file_path: Path = Path(__file__).parent / "gridsets" / f"{epsg}.xml" headers: dict[str, str] = {"Content-Type": "application/xml"} @@ -866,7 +995,18 @@ def create_gridset(self, epsg: int) -> Response | None: data: bytes = file_path.read_bytes() except FileNotFoundError: raise ValueError(f"No gridset definition found for EPSG:{epsg}") - return self.put_request(resource_path, data=data, headers=headers) + return self.put_request( + self.gwc_endpoints.gridset(epsg), data=data, headers=headers + ) + + def create_or_update_resource(self, path, resource_path, payload) -> Response: + """ + Create a GeoServer resource or update it if it already exists + """ + if not self.resource_exists(resource_path): + return self.post_request(path, json=payload) + else: + return self.put_request(resource_path, json=payload) def resource_exists(self, path: str) -> bool: """ diff --git a/geoservercloud/models/__init__.py b/geoservercloud/models/__init__.py new file mode 100644 index 0000000..c28cea9 --- /dev/null +++ b/geoservercloud/models/__init__.py @@ -0,0 +1,22 @@ +from .common import I18N, KeyDollarListDict +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__ = [ + "DataStores", + "KeyDollarListDict", + "FeatureType", + "FeatureTypes", + "I18N", + "PostGisDataStore", + "Style", + "Styles", + "Workspaces", + "Workspace", +] diff --git a/geoservercloud/models/common.py b/geoservercloud/models/common.py new file mode 100644 index 0000000..f86f8bf --- /dev/null +++ b/geoservercloud/models/common.py @@ -0,0 +1,87 @@ +import json +import logging +from typing import Any + +log = logging.getLogger() + + +class KeyDollarListDict(dict): + def __init__( + self, + input_list: list | None = None, + input_dict: dict | None = None, + *args, + **kwargs + ): + super().__init__(*args, **kwargs) + self.key_prefix = "@key" + self.value_prefix = "$" + if input_list: + self.deserialize(input_list) + if input_dict: + self.update(input_dict) + log.debug(self) + + def deserialize(self, input_list: list): + for item in input_list: + key = item[self.key_prefix] + if self.value_prefix in item: + value = item[self.value_prefix] + else: + value = None + super().__setitem__(key, value) + + def serialize(self): + return [ + {self.key_prefix: key, self.value_prefix: value} + for key, value in self.items() + ] + + def __repr__(self) -> str: + return str(self.serialize()) + + def __str__(self): + return json.dumps(self.serialize()) + + def update(self, other: dict): # type: ignore + for key, value in other.items(): + super().__setitem__(key, value) + + +class I18N: + """ + Class to handle internationalization of strings + items like title, abstract, etc. that can be internationalized + become a dictionary with the key being the language code + and their key in the payload changes to internationalizedTitle, internationalizedAbstract, etc. + """ + + def __init__(self, keys: tuple[str, Any], value: str | dict) -> None: + self._str_key = keys[0] + self._i18n_key = keys[1] + self._value = value + if isinstance(value, str): + self._payload = (self.str_key, self._value) + elif isinstance(value, dict): + self._payload = (self._i18n_key, self._value) # type: ignore + else: + raise ValueError("Invalid value type") + + @property + def str_key(self): + return self._str_key + + @property + def i18n_key(self): + return self._i18n_key + + @property + def value(self): + return self._value + + @property + def payload_tuple(self): + return self._payload + + def __repr__(self): + return json.dumps({self._payload[0]: self._payload[1]}, indent=4) diff --git a/geoservercloud/models/datastore.py b/geoservercloud/models/datastore.py new file mode 100644 index 0000000..4d22a09 --- /dev/null +++ b/geoservercloud/models/datastore.py @@ -0,0 +1,77 @@ +import json +import logging + +from requests.models import Response + +from . import KeyDollarListDict + +log = logging.getLogger() + + +class PostGisDataStore: + + def __init__( + self, + workspace_name: str, + data_store_name: str, + connection_parameters: KeyDollarListDict, + data_store_type: str = "PostGIS", + enabled: bool = True, + description: str | None = None, + ) -> None: + self.workspace_name = workspace_name + self.data_store_name = data_store_name + self.connection_parameters = connection_parameters + self.data_store_type = data_store_type + self.description = description + self.enabled = enabled + + @property + def name(self): + return self.data_store_name + + def post_payload(self): + payload = { + "dataStore": { + "name": self.data_store_name, + "type": self.data_store_type, + "connectionParameters": { + "entry": self.connection_parameters.serialize() + }, + } + } + if self.description: + payload["dataStore"]["description"] = self.description + if self.enabled: + payload["dataStore"]["enabled"] = self.enabled + return payload + + def put_payload(self): + payload = self.post_payload() + return payload + + @classmethod + def from_response(cls, response: Response): + if response.status_code == 404: + return None + json_data = response.json() + connection_parameters = cls.parse_connection_parameters(json_data) + return cls( + json_data.get("dataStore", {}).get("workspace", {}).get("name", None), + json_data.get("dataStore", {}).get("name", None), + connection_parameters, + json_data.get("dataStore", {}).get("type", "PostGIS"), + json_data.get("dataStore", {}).get("enabled", True), + json_data.get("dataStore", {}).get("description", None), + ) + + @classmethod + def parse_connection_parameters(cls, json_data): + return KeyDollarListDict( + json_data.get("dataStore", {}) + .get("connectionParameters", {}) + .get("entry", []) + ) + + def __repr__(self): + return json.dumps(self.put_payload(), indent=4) diff --git a/geoservercloud/models/datastores.py b/geoservercloud/models/datastores.py new file mode 100644 index 0000000..19b476d --- /dev/null +++ b/geoservercloud/models/datastores.py @@ -0,0 +1,32 @@ +import logging + +from requests.models import Response + +log = logging.getLogger() + + +class DataStores: + + def __init__(self, workspace_name: str, datastores: list[str] = []) -> None: + self.workspace_name = workspace_name + self._datastores = datastores + + @property + def datastores(self): + return self._datastores + + @classmethod + def from_response(cls, response: Response): + json_data = response.json() + datastores = [] + workspace_name = ( + json_data.get("dataStores", {}).get("workspace", {}).get("name", None) + ) + for store in json_data.get("dataStores", {}).get("dataStore", []): + datastores.append(store["name"]) + for data_store_name in datastores: + log.debug(f"Name: {data_store_name}") + return cls(workspace_name, datastores) + + def __repr__(self): + return str(self.datastores) diff --git a/geoservercloud/models/featuretype.py b/geoservercloud/models/featuretype.py new file mode 100644 index 0000000..3ca163a --- /dev/null +++ b/geoservercloud/models/featuretype.py @@ -0,0 +1,134 @@ +import json + +from requests.models import Response + +from geoservercloud.models import I18N + + +# TODO: import more default values from Templates +class FeatureType: + def __init__( + self, + namespace_name: str, + name: str, + native_name: str, + srs: str = "EPSG:4326", + title: str | dict = "New Layer", + abstract: str | dict = "New Layer", + keywords={}, + metadata_url=None, + metadata_type="TC211", + metadata_format="text/xml", + attributes: dict | None = None, + ) -> None: + self._namespace_name = namespace_name + self._name = name + self._title = I18N(("title", "internationalTitle"), title) + self._abstract = I18N(("abstract", "internationalAbstract"), abstract) + self._native_name = native_name + self._srs = srs + self._keywords = keywords + self._attributes = attributes + self.create_metadata_link(metadata_url, metadata_type, metadata_format) + + @property + def namespace_name(self): + return self._namespace_name + + @property + def name(self): + return self._name + + @property + def title(self): + return self._title + + @property + def abstract(self): + return self._abstract + + @property + def native_name(self): + return self._native_name + + @property + def srs(self): + return self._srs + + @property + def keywords(self): + return self._keywords + + @property + def metadataLink(self): + return self._metadataLink + + @property + def attributes(self): + return self._attributes + + def post_payload(self): + payload = { + "featureType": { + "name": self.name, + "nativeName": self.native_name, + self._title.payload_tuple[0]: self._title.payload_tuple[1], + self._abstract.payload_tuple[0]: self._abstract.payload_tuple[1], + "srs": self.srs, + "keywords": self.keywords, + } + } + if self.metadataLink != {}: + payload["featureType"]["metadataLinks"] = self.metadataLink + if self.attributes: + payload["featureType"]["attributes"] = self.attributes + return payload + + def create_metadata_link( + self, metadata_url=None, metadata_type="TC211", metadata_format="text/xml" + ): + self._metadataLink = {} + if metadata_url: + self._metadataLink["metadataLink"] = { + "type": metadata_format, + "metadataType": metadata_type, + "content": metadata_url, + } + + @classmethod + def from_response(cls, response: Response): + json_data = response.json() + try: + abstract = json_data["featureType"]["abstract"] + except KeyError: + abstract = json_data["featureType"]["internationalAbstract"] + try: + title = json_data["featureType"]["title"] + except KeyError: + title = json_data["featureType"]["internationalTitle"] + + return cls( + namespace_name=json_data["featureType"]["namespace"]["name"], + name=json_data["featureType"]["name"], + native_name=json_data["featureType"]["nativeName"], + title=title, + abstract=abstract, + srs=json_data["featureType"]["srs"], + keywords=json_data["featureType"]["keywords"], + attributes=json_data["featureType"].get("attributes", None), + metadata_url=json_data["featureType"] + .get("metadataLinks", {}) + .get("metadataLink", {}) + .get("content"), + metadata_type=json_data["featureType"] + .get("metadataLinks", {}) + .get("metadataLink", {}) + .get("metadataType"), + metadata_format=json_data["featureType"] + .get("metadataLinks", {}) + .get("metadataLink", {}) + .get("type"), + ) + + def __repr__(self): + return json.dumps(self.post_payload(), indent=4) diff --git a/geoservercloud/models/featuretypes.py b/geoservercloud/models/featuretypes.py new file mode 100644 index 0000000..85eef01 --- /dev/null +++ b/geoservercloud/models/featuretypes.py @@ -0,0 +1,24 @@ +import json + +from requests.models import Response + + +class FeatureTypes: + + def __init__(self, featuretypes: list = []) -> None: + self._featuretypes = featuretypes + + @property + def featuretypes(self): + return self._featuretypes + + @classmethod + def from_response(cls, response: Response): + featuretypes = [] + json_data = response.json() + for featuretype in json_data.get("featureTypes", {}).get("featureType", []): + featuretypes.append(featuretype["name"]) + return cls(featuretypes) + + def __repr__(self): + return json.dumps(self._featuretypes, indent=4) diff --git a/geoservercloud/models/layer.py b/geoservercloud/models/layer.py new file mode 100644 index 0000000..593588a --- /dev/null +++ b/geoservercloud/models/layer.py @@ -0,0 +1,113 @@ +import logging + +import jsonschema +import requests + +log = logging.getLogger() + + +class Layer: + _responseSchema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "layer": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "type": {"type": "string"}, + "defaultStyle": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "href": {"type": "string", "format": "uri"}, + }, + "required": ["name", "href"], + }, + "resource": { + "type": "object", + "properties": { + "@class": {"type": "string", "const": "featureType"}, + "name": {"type": "string"}, + "href": {"type": "string", "format": "uri"}, + }, + "required": ["@class", "name", "href"], + }, + "attribution": { + "type": "object", + "properties": { + "logoWidth": {"type": "integer"}, + "logoHeight": {"type": "integer"}, + }, + "required": ["logoWidth", "logoHeight"], + }, + "dateCreated": {"type": "string", "format": "date-time"}, + }, + "required": [ + "name", + "type", + "defaultStyle", + "resource", + "attribution", + "dateCreated", + ], + } + }, + "required": ["layer"], + } + + def __init__( + self, + workspace_name, + layer_name, + srs="EPSG:4326", + title="", + abstract="", + keywords={"string": []}, + ) -> None: + self.workspace = workspace_name + self.layer_name = layer_name + self.title = title + self.srs = srs + self.abstract = abstract + self.keywords = keywords + + @property + def responseSchema(self): + return self._responseSchema + + def endpoint_url(self): + return f"/workspaces/{self.workspace}/layers/{self.layer_name}.json" + + def toListItem(self): + return { + "@type": "layer", + "name": f"{self.workspace}:{self.layer_name}", + "href": self.endpoint_url(), + } + + def put_payload_style(self, style_name, authorityURLs, identifier): + return { + "layer": { + "name": self.layer_name, + "defaultStyle": {"name": style_name}, + "authorityURLs": {"AuthorityURL": authorityURLs}, + "identifiers": {"Identifier": identifier}, + } + } + + def validate(self, response): + try: + jsonschema.validate(response, self.responseSchema) + except jsonschema.exceptions.ValidationError as err: + print(err) + return False + return True + + def parseResponse(self, response): + if response.status_code == 200: + # Parse the JSON response + self.layer = response.json() + self.validate(self.layer) + else: + log.error(f"Error: {response.status_code}") diff --git a/geoservercloud/models/layergroup.py b/geoservercloud/models/layergroup.py new file mode 100644 index 0000000..5364eaa --- /dev/null +++ b/geoservercloud/models/layergroup.py @@ -0,0 +1,197 @@ +import logging + +import jsonschema +import requests + +from .Layer import Layer +from .Style import Style + +log = logging.getLogger() + + +class LayerGroup: + _responseSchema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "layerGroup": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "mode": {"type": "string"}, + "title": {"type": "string"}, + "abstractTxt": {"type": "string"}, + "workspace": { + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"], + }, + "internationalTitle": {"type": "string"}, + "internationalAbstract": {"type": "string"}, + "publishables": { + "type": "object", + "properties": { + "published": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@type": {"type": "string", "const": "layer"}, + "name": {"type": "string"}, + "href": {"type": "string", "format": "uri"}, + }, + "required": ["@type", "name", "href"], + }, + } + }, + "required": ["published"], + }, + "styles": { + "type": "object", + "properties": { + "style": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "href": {"type": "string", "format": "uri"}, + }, + "required": ["name", "href"], + }, + } + }, + "required": ["style"], + }, + "bounds": { + "type": "object", + "properties": { + "minx": {"type": "number"}, + "maxx": {"type": "number"}, + "miny": {"type": "number"}, + "maxy": {"type": "number"}, + "crs": { + "type": "object", + "properties": { + "@class": {"type": "string", "const": "projected"}, + "$": {"type": "string", "const": "EPSG:25833"}, + }, + "required": ["@class", "$"], + }, + }, + "required": ["minx", "maxx", "miny", "maxy", "crs"], + }, + "dateCreated": {"type": "string", "format": "date-time"}, + }, + "required": [ + "name", + "mode", + "title", + "abstractTxt", + "workspace", + "publishables", + "styles", + "bounds", + "dateCreated", + ], + } + }, + "required": ["layerGroup"], + } + + def __init__( + self, + workspace_name, + layer_group_name, + layers=[], + styles=[], + crs={"@class": "projected", "$": "EPSG:25833"}, + bbox={ + "minx": 269387.6943774796, + "maxx": 731380.9792889762, + "miny": 5138491.809334871, + "maxy": 9330126.130139956, + }, + internationalTitle={"de-DE": "change-me - title"}, + internationalAbstract={"de-DE": "change-me - abstract"}, + metadataLinksIdentifier="change-me - metadataLinksIdentifier", + keywords={}, + ) -> None: + self.workspace = workspace_name + self.name = layer_group_name + log.debug(f"LayerGroup: {self.name}") + log.debug(f"layers = {layers}") + log.debug(f"layers: {layers[0].toListItem()}") + self.layers_list_items = self.layerList2layerListItem(layers) + self.styles_list_items = self.stylesList2styleListItem(styles) + self.metadataLinksIdentifier = metadataLinksIdentifier + self.crs = crs + self.bounds = { + "minx": bbox["minx"], + "maxx": bbox["maxx"], + "miny": bbox["miny"], + "maxy": bbox["maxy"], + "crs": self.crs, + } + + self.keywords = keywords + self.internationalTitle = internationalTitle + self.internationalAbstract = internationalAbstract + + @property + def responseSchema(self): + return self._responseSchema + + def stylesList2styleListItem(self, stylesList): + styleListItems = [] + for style in stylesList: + styleListItems.append(style.toListItem()) + return styleListItems + + def layerList2layerListItem(self, layerList): + layerListItems = [] + for layer in layerList: + layerListItems.append(layer.toListItem()) + return layerListItems + + def endpoint_url(self): + return f"/workspaces/{self.workspace}/layergroups/{self.layerGroupName}.json" + + def post_payload(self): + payload = { + "layerGroup": { + "name": self.name, + "mode": "SINGLE", + "workspace": {"name": self.workspace}, + "internationalTitle": self.internationalTitle, + "internationalAbstract": self.internationalAbstract, + "publishables": {"published": self.layers_list_items}, + "styles": {"style": self.styles_list_items}, + "metadataLinks": { + "metadataLink": { + "type": "text/xml", + "metadataType": "ISO19115:2003", + "content": self.metadataLinksIdentifier, + } + }, + "bounds": self.bounds, + } + } + + return payload + + def validate(self, response): + try: + jsonschema.validate(response, self.responseSchema) + except jsonschema.exceptions.ValidationError as err: + print(err) + return False + return True + + def parseResponse(self, response): + if response.status_code == 200: + # Parse the JSON response + self.layerGroup = response.json() + self.validate(self.layerGroup) + else: + log.error(f"Error: {response.status_code}") diff --git a/geoservercloud/models/layergroups.py b/geoservercloud/models/layergroups.py new file mode 100644 index 0000000..89c33c1 --- /dev/null +++ b/geoservercloud/models/layergroups.py @@ -0,0 +1,78 @@ +import logging + +import jsonschema +import requests + +log = logging.getLogger() + + +class LayerGroups: + + def __init__(self, workspace_name, layerGroups={}) -> None: + self.workspace_name = workspace_name + self.layerGroups = layerGroups + + def endpoint_url(self): + return f"/workspaces/{self.workspace_name}/layergroups.json" + + _response_schema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "layerGroups": { + "type": "object", + "properties": { + "layerGroup": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "href": {"type": "string", "format": "uri"}, + }, + "required": ["name", "href"], + }, + } + }, + "required": ["layerGroup"], + } + }, + "required": ["layerGroups"], + } + + @property + def response_schema(self): + return self._response_schema + + def validate(self, response): + try: + log.debug("validate: response = " + str(response)) + if not response["layerGroups"] == "": + jsonschema.validate(response, self.response_schema) + except jsonschema.exceptions.ValidationError as err: + print(err) + return False + return True + + def parseResponse(self, response): + # Check if the request was successful (status code 200) + if response.status_code == 200: + # Parse the JSON response + json_data = response.json() + if not self.validate(json_data): + raise Exception("Invalid from layerGroups") + + # Map the response to a list of FeatureType instances + try: + for layergroup in json_data.get("layerGroups", {}).get( + "layerGroup", [] + ): + self.layerGroups[layergroup["name"]] = layergroup["href"] + except AttributeError: + self.layerGroups = {} + + # Now 'layerGroups' is a list of FeatureType instances + for layergroup_type_name, layergroup_type_href in self.layerGroups.items(): + log.debug(f"Name: {layergroup_type_name}, Href: {layergroup_type_href}") + else: + log.error(f"Error: {response.status_code}") diff --git a/geoservercloud/models/layers.py b/geoservercloud/models/layers.py new file mode 100644 index 0000000..54b687d --- /dev/null +++ b/geoservercloud/models/layers.py @@ -0,0 +1,76 @@ +import logging + +import jsonschema +import requests + +log = logging.getLogger() + + +class Layers: + + def __init__(self, workspace_name, layers={}) -> None: + self.workspace_name = workspace_name + self.layers = layers + + def endpoint_url(self): + return f"/workspaces/{self.workspace_name}/layers.json" + + _response_schema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "layers": { + "type": "object", + "properties": { + "layer": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "href": {"type": "string", "format": "uri"}, + }, + "required": ["name", "href"], + }, + } + }, + "required": ["layer"], + } + }, + "required": ["layers"], + } + + @property + def response_schema(self): + return self._response_schema + + def validate(self, response): + try: + log.debug("validate: response = " + str(response)) + if not response["layers"] == "": + jsonschema.validate(response, self.response_schema) + except jsonschema.exceptions.ValidationError as err: + print(err) + return False + return True + + def parseResponse(self, response): + # Check if the request was successful (status code 200) + if response.status_code == 200: + # Parse the JSON response + json_data = response.json() + if not self.validate(json_data): + raise Exception("Invalid from layers") + + # Map the response to a list of FeatureType instances + try: + for feature in json_data.get("layers", {}).get("layer", []): + self.layers[feature["name"]] = feature["href"] + except AttributeError: + self.layers = {} + + # Now 'layers' is a list of FeatureType instances + for layer_name, layer_href in self.layers.items(): + log.debug(f"Name: {layer_name}, Href: {layer_href}") + else: + log.error(f"Error: {response.status_code}") diff --git a/geoservercloud/models/namespace.py b/geoservercloud/models/namespace.py new file mode 100644 index 0000000..054c9b0 --- /dev/null +++ b/geoservercloud/models/namespace.py @@ -0,0 +1,61 @@ +import logging + +import jsonschema +import requests + +log = logging.getLogger() + + +class Namespace: + _responseSchema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "namespace": { + "type": "object", + "properties": { + "prefix": {"type": "string"}, + "uri": {"type": "string", "format": "uri"}, + "isolated": {"type": "boolean"}, + }, + "required": ["prefix", "uri", "isolated"], + } + }, + "required": ["namespace"], + } + + def __init__(self, workspace) -> None: + self.workspace = workspace + self.namespace = None + + def put_payload(self, uri=None, isolated="true"): + payload = { + "namespace": {"prefix": self.workspace, "uri": uri, "isolated": isolated} + } + payload["namespace"] = { + k: v for k, v in payload["namespace"].items() if v is not None + } + return payload + + @property + def responseSchema(self): + return self._responseSchema + + def endpoint_url(self): + return f"/namespaces/{self.workspace}.json" + + def validate(self, response): + try: + jsonschema.validate(response, self.responseSchema) + except jsonschema.exceptions.ValidationError as err: + print(err) + return False + return True + + def parseResponse(self, response): + if response.status_code == 200: + # Parse the JSON response + self.namespace = response.json() + self.validate(self.settingsWFS) + else: + log.error(f"Error: {response.status_code}") diff --git a/geoservercloud/models/settingsWFS.py b/geoservercloud/models/settingsWFS.py new file mode 100644 index 0000000..78ec19e --- /dev/null +++ b/geoservercloud/models/settingsWFS.py @@ -0,0 +1,139 @@ +import logging + +import jsonschema +import requests + +log = logging.getLogger() + + +class SettingsWFS: + _responseSchema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "wfs": { + "type": "object", + "properties": { + "workspace": { + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"], + }, + "enabled": {"type": "boolean"}, + "name": {"type": "string"}, + "title": {"type": "string"}, + "maintainer": {"type": "string"}, + "abstract": {"type": "string"}, + "accessConstraints": {"type": "string"}, + "fees": {"type": "string"}, + "versions": { + "type": "object", + "properties": { + "org.geotools.util.Version": { + "type": "array", + "items": { + "type": "object", + "properties": {"version": {"type": "string"}}, + "required": ["version"], + }, + } + }, + "required": ["org.geotools.util.Version"], + }, + "keywords": { + "type": "object", + "properties": { + "string": {"type": "array", "items": {"type": "string"}} + }, + "required": ["string"], + }, + "citeCompliant": {"type": "boolean"}, + "onlineResource": {"type": "string"}, + "schemaBaseURL": {"type": "string"}, + "verbose": {"type": "boolean"}, + "gml": { + "type": "object", + "properties": { + "entry": { + "type": "array", + "items": { + "type": "object", + "properties": { + "version": {"type": "string"}, + "gml": { + "type": "object", + "properties": { + "srsNameStyle": { + "type": "array", + "items": {"type": "string"}, + }, + "overrideGMLAttributes": { + "type": "boolean" + }, + }, + "required": [ + "srsNameStyle", + "overrideGMLAttributes", + ], + }, + }, + "required": ["version", "gml"], + }, + } + }, + "required": ["entry"], + }, + "serviceLevel": {"type": "string"}, + "maxFeatures": {"type": "integer"}, + "featureBounding": {"type": "boolean"}, + "canonicalSchemaLocation": {"type": "boolean"}, + "encodeFeatureMember": {"type": "boolean"}, + "hitsIgnoreMaxFeatures": {"type": "boolean"}, + "includeWFSRequestDumpFile": {"type": "boolean"}, + "allowGlobalQueries": {"type": "boolean"}, + "simpleConversionEnabled": {"type": "boolean"}, + }, + "required": ["workspace", "enabled", "name"], + } + }, + "required": ["wfs"], + } + + def __init__(self, workspace) -> None: + self.workspace = workspace + self.wfs = None + + def put_payload(self, enabled=None, title=None, keywords=None): + payload = { + "wfs": { + "workspace": {"name": self.workspace}, + "enabled": enabled, + "title": title, + "keywords": {"string": keywords} if keywords else None, + } + } + payload["wfs"] = {k: v for k, v in payload["wfs"].items() if v is not None} + return payload + + @property + def responseSchema(self): + return self._responseSchema + + def endpoint_url(self): + return f"/services/wfs/workspaces/{self.workspace}/settings.json" + + def validate(self, response): + try: + jsonschema.validate(response, self.responseSchema) + except jsonschema.exceptions.ValidationError as err: + print(err) + return False + return True + + def parseResponse(self, response): + if response.status_code == 200: + # Parse the JSON response + self.wfs = response.json() + self.validate(self.settingsWFS) + else: + log.error(f"Error: {response.status_code}") diff --git a/geoservercloud/models/settingsWMS.py b/geoservercloud/models/settingsWMS.py new file mode 100644 index 0000000..1f8fbf3 --- /dev/null +++ b/geoservercloud/models/settingsWMS.py @@ -0,0 +1,149 @@ +import logging + +import jsonschema +import requests + +log = logging.getLogger() + + +class SettingsWMS: + _responseSchema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "wms": { + "type": "object", + "properties": { + "workspace": { + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"], + }, + "enabled": {"type": "boolean"}, + "name": {"type": "string"}, + "title": {"type": "string"}, + "maintainer": {"type": "string"}, + "abstract": {"type": "string"}, + "accessConstraints": {"type": "string"}, + "fees": {"type": "string"}, + "versions": { + "type": "object", + "properties": { + "org.geotools.util.Version": { + "type": "array", + "items": { + "type": "object", + "properties": {"version": {"type": "string"}}, + "required": ["version"], + }, + } + }, + "required": ["org.geotools.util.Version"], + }, + "keywords": { + "type": "object", + "properties": { + "string": {"type": "array", "items": {"type": "string"}} + }, + "required": ["string"], + }, + "citeCompliant": {"type": "boolean"}, + "onlineResource": {"type": "string"}, + "schemaBaseURL": {"type": "string"}, + "verbose": {"type": "boolean"}, + "metadata": { + "type": "object", + "properties": { + "entry": { + "type": "array", + "items": { + "type": "object", + "properties": { + "@key": {"type": "string"}, + "$": {"type": "string"}, + }, + }, + } + }, + "required": ["entry"], + }, + "bboxForEachCRS": {"type": "boolean"}, + "watermark": { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "position": {"type": "string"}, + "transparency": {"type": "integer"}, + }, + "required": ["enabled", "position", "transparency"], + }, + "interpolation": {"type": "string"}, + "getFeatureInfoMimeTypeCheckingEnabled": {"type": "boolean"}, + "getMapMimeTypeCheckingEnabled": {"type": "boolean"}, + "dynamicStylingDisabled": {"type": "boolean"}, + "featuresReprojectionDisabled": {"type": "boolean"}, + "maxBuffer": {"type": "integer"}, + "maxRequestMemory": {"type": "integer"}, + "maxRenderingTime": {"type": "integer"}, + "maxRenderingErrors": {"type": "integer"}, + "maxRequestedDimensionValues": {"type": "integer"}, + "cacheConfiguration": { + "type": "object", + "properties": { + "enabled": {"type": "boolean"}, + "maxEntries": {"type": "integer"}, + "maxEntrySize": {"type": "integer"}, + }, + "required": ["enabled", "maxEntries", "maxEntrySize"], + }, + "remoteStyleMaxRequestTime": {"type": "integer"}, + "remoteStyleTimeout": {"type": "integer"}, + "defaultGroupStyleEnabled": {"type": "boolean"}, + }, + "required": [], + } + }, + "required": ["wms"], + } + + def __init__(self, workspace="default") -> None: + self.workspace = workspace + self.wms = None + + def put_payload(self, enabled=None, title=None, keywords=None): + payload = { + "wms": { + "workspace": {"name": self.workspace}, + "enabled": enabled, + "title": title, + "keywords": {"string": keywords} if keywords else None, + } + } + payload["wms"] = {k: v for k, v in payload["wms"].items() if v is not None} + return payload + + @property + def responseSchema(self): + return self._responseSchema + + def endpoint_url(self): + return f"/services/wms/workspaces/{self.workspace}/settings.json" + + def endpoint_url_default_service(self): + return f"/services/wms/settings.json" + + def validate(self, response): + try: + jsonschema.validate(response, self.responseSchema) + except jsonschema.exceptions.ValidationError as err: + print(err) + return False + return True + + def parseResponse(self, response): + if response.status_code == 200: + # Parse the JSON response + self.wms = response.json() + self.validate(self.wms) + else: + log.error(f"Error: {response.status_code}") diff --git a/geoservercloud/models/style.py b/geoservercloud/models/style.py new file mode 100644 index 0000000..ebc9734 --- /dev/null +++ b/geoservercloud/models/style.py @@ -0,0 +1,129 @@ +import json + +import xmltodict +from requests.models import Response + + +class Style: + + def __init__( + self, + name: str, + workspace: str | None = None, + format: str | None = "sld", + language_version: dict | None = {"version": "1.0.0"}, + filename: str | None = None, + date_created: str | None = None, + date_modified: str | None = None, + legend_url: str | None = None, + legend_format: str | None = None, + legend_width: int | None = None, + legend_height: int | None = None, + ) -> None: + self._workspace = workspace + self._name = name + self._format = format + self._language_version = language_version + self._filename = filename + self._date_created = date_created + self._date_modified = date_modified + self._legend = self.create_legend( + legend_url, legend_format, legend_width, legend_height + ) + + # create one property for each attribute + @property + def workspace(self): + return self._workspace + + @property + def name(self): + return self._name + + @property + def format(self): + return self._format + + @property + def language_version(self): + return self._language_version + + @property + def filename(self): + return self._filename + + @property + def date_created(self): + return self._date_created + + @property + def date_modified(self): + return self._date_modified + + @property + def legend(self): + return self._legend + + def create_legend( + self, + url: str | None, + image_format: str | None, + width: int | None, + height: int | None, + ): + if any([url, image_format, width, height]): + legend: dict = {} + if url: + legend["onlineResource"] = url + if image_format: + legend["format"] = image_format + if width: + legend["width"] = width + if height: + legend["height"] = height + else: + legend = None # type: ignore + return legend + + def put_payload(self): + payload = { + "style": { + "name": self.name, + "format": self.format, + "languageVersion": self.language_version, + "filename": self.filename, + } + } + if self.legend: + payload["style"]["legend"] = self.legend + return payload + + def post_payload(self): + return self.put_payload() + + @classmethod + def from_response(cls, response: Response): + json_data = response.json() + style_data = json_data.get("style", {}) + return cls( + workspace=style_data.get("workspace"), + name=style_data.get("name"), + format=style_data.get("format"), + language_version=style_data.get("languageVersion", None), + filename=style_data.get("filename"), + date_created=style_data.get("dateCreated"), + date_modified=style_data.get("dateModified"), + legend_url=style_data.get("legend", {}).get("onlineResource"), + legend_format=style_data.get("legend", {}).get("format"), + legend_width=style_data.get("legend", {}).get("width"), + legend_height=style_data.get("legend", {}).get("height"), + ) + + def xml_post_payload(self): + return xmltodict.unparse(self.post_payload()).split("\n", 1)[1] + + def xml_put_payload(self): + return xmltodict.unparse(self.put_payload()).split("\n", 1)[1] + + def __repr__(self): + return json.dumps(self.put_payload(), indent=4) diff --git a/geoservercloud/models/styles.py b/geoservercloud/models/styles.py new file mode 100644 index 0000000..f8589e6 --- /dev/null +++ b/geoservercloud/models/styles.py @@ -0,0 +1,31 @@ +from requests.models import Response + + +class Styles: + + def __init__(self, styles: list[str], workspace: str | None = None) -> None: + self._workspace = workspace + self._styles = styles + + @property + def workspace(self): + return self._workspace + + @property + def styles(self): + return self._styles + + @classmethod + def from_response(cls, response: Response): + json_data = response.json() + styles = [] + try: + workspace = json_data["styles"]["workspace"] + except KeyError: + workspace = None + try: + for style in json_data.get("styles", {}).get("style", []): + styles.append(style["name"]) + except AttributeError: + styles = [] + return cls(styles, workspace) diff --git a/geoservercloud/models/workspace.py b/geoservercloud/models/workspace.py new file mode 100644 index 0000000..eff4789 --- /dev/null +++ b/geoservercloud/models/workspace.py @@ -0,0 +1,34 @@ +import json +import logging + +from requests.models import Response + +log = logging.getLogger() + + +class Workspace: + + def __init__(self, name: str, isolated: bool = False) -> None: + self.name = name + self.isolated = isolated + + def put_payload(self): + payload = {"workspace": {"name": self.name}} + if self.isolated: + payload["workspace"]["isolated"] = self.isolated + return payload + + def post_payload(self): + return self.put_payload() + + @classmethod + def from_response(cls, response: Response): + json_data = response.json() + return cls( + json_data.get("workspace", {}).get("name", None), + json_data.get("workspace", {}).get("isolated", False), + ) + return cls(json_data.get("workspace", {}).get("name", None)) + + def __repr__(self): + return json.dumps(self.put_payload(), indent=4) diff --git a/geoservercloud/models/workspaces.py b/geoservercloud/models/workspaces.py new file mode 100644 index 0000000..b27dc3e --- /dev/null +++ b/geoservercloud/models/workspaces.py @@ -0,0 +1,35 @@ +import logging + +from requests.models import Response + +log = logging.getLogger() + + +class Workspaces: + + def __init__(self, workspaces: list = []) -> None: + self._workspaces = workspaces + + def find(self, workspace_name: str): + return self.workspaces.get(workspace_name, None) + + @property + def workspaces(self): + return self._workspaces + + @classmethod + def from_response(cls, response: Response): + # Parse the JSON response + json_data = response.json() + + workspaces = [] + # Map the response to a list of Workspace instances + for ws in json_data.get("workspaces", {}).get("workspace", []): + workspaces.append(ws["name"]) + + # Now 'workspaces' is a list of Workspace instances + log.debug("Parsed Workspaces:") + for workspace in workspaces: + log.debug(f"Name: {workspace}") + + return cls(workspaces) diff --git a/geoservercloud/services/__init__.py b/geoservercloud/services/__init__.py new file mode 100644 index 0000000..d885ea3 --- /dev/null +++ b/geoservercloud/services/__init__.py @@ -0,0 +1,10 @@ +from .endpoints import AclEndpoints, GwcEndpoints, OwsEndpoints, RestEndpoints +from .restservice import RestService + +__all__ = [ + "RestService", + "AclEndpoints", + "OwsEndpoints", + "GwcEndpoints", + "RestEndpoints", +] diff --git a/geoservercloud/services/endpoints.py b/geoservercloud/services/endpoints.py new file mode 100644 index 0000000..6a7f557 --- /dev/null +++ b/geoservercloud/services/endpoints.py @@ -0,0 +1,174 @@ +class AclEndpoints: + def __init__(self, base_url: str = "/acl") -> None: + self.base_url: str = base_url + + def adminrules(self) -> str: + return f"{self.base_url}/api/adminrules" + + def adminrule(self, id: int) -> str: + return f"{self.base_url}/api/adminrules/id/{id}" + + def rules(self) -> str: + return f"{self.base_url}/api/rules" + + +class GwcEndpoints: + def __init__(self, base_url: str = "/gwc/rest") -> None: + self.base_url: str = base_url + + def reload(self) -> str: + return f"{self.base_url}/reload" + + def layers(self, workspace_name: str) -> str: + return f"{self.base_url}/layers.json" + + def layer(self, workspace_name: str, layer_name: str) -> str: + return f"{self.base_url}/layers/{workspace_name}:{layer_name}.json" + + def gridsets(self) -> str: + return f"{self.base_url}/gridsets.json" + + def gridset(self, epsg: int) -> str: + return f"{self.base_url}/gridsets/EPSG:{str(epsg)}.xml" + + +class OwsEndpoints: + def __init__(self, base_url: str = "") -> None: + self.base_url: str = base_url + + def ows(self) -> str: + return f"{self.base_url}/ows" + + def wms(self) -> str: + return f"{self.base_url}/wms" + + def wfs(self) -> str: + return f"{self.base_url}/wfs" + + def wcs(self) -> str: + return f"{self.base_url}/wcs" + + def wmts(self) -> str: + return f"{self.base_url}/gwc/service/wmts" + + def workspace_ows(self, workspace_name: str) -> str: + return f"{self.base_url}/{workspace_name}/ows" + + def workspace_wms(self, workspace_name: str) -> str: + return f"{self.base_url}/{workspace_name}/wms" + + def workspace_wfs(self, workspace_name: str) -> str: + return f"{self.base_url}/{workspace_name}/wfs" + + def workspace_wcs(self, workspace_name: str) -> str: + return f"{self.base_url}/{workspace_name}/wcs" + + +class RestEndpoints: + 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 workspaces(self) -> str: + return f"{self.base_url}/workspaces.json" + + 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" + + def workspace_wms_settings(self, workspace_name: str) -> str: + return f"{self.base_url}/services/wms/workspaces/{workspace_name}/settings.json" + + def workspace_wfs_settings(self, workspace_name: str) -> str: + return f"{self.base_url}/services/wfs/workspaces/{workspace_name}/settings.json" + + def datastores(self, workspace_name: str) -> str: + return f"{self.base_url}/workspaces/{workspace_name}/datastores.json" + + def datastore(self, workspace_name: str, datastore_name: str) -> str: + return f"{self.base_url}/workspaces/{workspace_name}/datastores/{datastore_name}.json" + + def featuretypes(self, workspace_name: str, datastore_name: str) -> str: + return f"{self.base_url}/workspaces/{workspace_name}/datastores/{datastore_name}/featuretypes.json" + + def featuretype( + self, workspace_name: str, datastore_name: str, featuretype_name: str + ) -> str: + return f"{self.base_url}/workspaces/{workspace_name}/datastores/{datastore_name}/featuretypes/{featuretype_name}.json" + + def layergroup(self, workspace_name: str, layergroup_name: str) -> str: + return f"{self.base_url}/workspaces/{workspace_name}/layergroups/{layergroup_name}.json" + + def layergroups(self, workspace_name: str) -> str: + return f"{self.base_url}/workspaces/{workspace_name}/layergroups.json" + + def coveragestores(self, workspace_name: str) -> str: + return f"{self.base_url}/workspaces/{workspace_name}/coveragestores.json" + + def coveragestore(self, workspace_name: str, coveragestore_name: str) -> str: + return f"{self.base_url}/workspaces/{workspace_name}/coveragestores/{coveragestore_name}.json" + + def coverages(self, workspace_name: str, coveragestore_name: str) -> str: + return f"{self.base_url}/workspaces/{workspace_name}/coveragestores/{coveragestore_name}/coverages.json" + + def coverage( + self, workspace_name: str, coveragestore_name: str, coverage_name: str + ) -> str: + return f"{self.base_url}/workspaces/{workspace_name}/coveragestores/{coveragestore_name}/coverages/{coverage_name}.json" + + def wmsstores(self, workspace_name: str) -> str: + return f"{self.base_url}/workspaces/{workspace_name}/wmsstores.json" + + def wmsstore(self, workspace_name: str, wmsstore_name: str) -> str: + return f"{self.base_url}/workspaces/{workspace_name}/wmsstores/{wmsstore_name}.json" + + def wmtsstores(self, workspace_name: str) -> str: + return f"{self.base_url}/workspaces/{workspace_name}/wmtsstores.json" + + def wmtsstore(self, workspace_name: str, wmtsstore_name: str) -> str: + return f"{self.base_url}/workspaces/{workspace_name}/wmtsstores/{wmtsstore_name}.json" + + def wmtslayers(self, workspace_name: str, wmtsstore_name: str) -> str: + return f"{self.base_url}/workspaces/{workspace_name}/wmtsstores/{wmtsstore_name}/layers.json" + + def wmtslayer( + self, workspace_name: str, wmtsstore_name: str, wmtslayer_name: str + ) -> str: + return f"{self.base_url}/workspaces/{workspace_name}/wmtsstores/{wmtsstore_name}/layers/{wmtslayer_name}.json" + + def namespaces(self) -> str: + return f"{self.base_url}/namespaces.json" + + def namespace(self, namespace_name: str) -> str: + return f"{self.base_url}/namespaces/{namespace_name}.json" + + def users(self) -> str: + return f"{self.base_url}/security/usergroup/users.json" + + def user(self, username: str) -> str: + return f"{self.base_url}/security/usergroup/user/{username}.json" + + def roles(self) -> str: + return f"{self.base_url}/security/roles.json" + + def user_roles(self, username: str) -> str: + return f"{self.base_url}/security/roles/user/{username}.json" + + def role(self, role_name: str) -> str: + return f"{self.base_url}/security/roles/role/{role_name}.json" + + def role_user(self, role_name: str, username: str) -> str: + return f"{self.base_url}/security/roles/role/{role_name}/user/{username}.json" diff --git a/geoservercloud/restservice.py b/geoservercloud/services/restservice.py similarity index 100% rename from geoservercloud/restservice.py rename to geoservercloud/services/restservice.py diff --git a/poetry.lock b/poetry.lock index bf332e9..f247065 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,28 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "attrs" +version = "24.2.0" +description = "Classes Without Boilerplate" +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" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -14,109 +32,122 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, ] [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -128,7 +159,6 @@ files = [ name = "coverage" version = "7.6.1" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -213,7 +243,6 @@ toml = ["tomli"] name = "exceptiongroup" version = "1.2.2" description = "Backport of PEP 654 (exception groups)" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -228,7 +257,6 @@ test = ["pytest (>=6)"] name = "idna" version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -243,7 +271,6 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -251,11 +278,45 @@ 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" +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" +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" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -410,7 +471,6 @@ source = ["Cython (>=3.0.11)"] name = "owslib" version = "0.31.0" description = "OGC Web Service utility library" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -429,7 +489,6 @@ requests = ">=1.0" name = "packaging" version = "24.1" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -441,7 +500,6 @@ files = [ name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -457,7 +515,6 @@ testing = ["pytest", "pytest-benchmark"] name = "pytest" version = "8.3.3" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -476,11 +533,27 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-mock" +version = "3.14.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -495,7 +568,6 @@ six = ">=1.5" name = "pytz" version = "2024.2" description = "World timezone definitions, modern and historical" -category = "main" optional = false python-versions = "*" files = [ @@ -507,7 +579,6 @@ files = [ name = "pyyaml" version = "6.0.2" description = "YAML parser and emitter for Python" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -566,11 +637,25 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "referencing" +version = "0.35.1" +description = "JSON Referencing + Python" +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" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -592,7 +677,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "responses" version = "0.25.3" description = "A utility library for mocking out the `requests` Python library." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -608,11 +692,122 @@ 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.0" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2"}, + {file = "rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94"}, + {file = "rpds_py-0.20.0-cp310-none-win32.whl", hash = "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee"}, + {file = "rpds_py-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58"}, + {file = "rpds_py-0.20.0-cp311-none-win32.whl", hash = "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0"}, + {file = "rpds_py-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174"}, + {file = "rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139"}, + {file = "rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57"}, + {file = "rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a"}, + {file = "rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2"}, + {file = "rpds_py-0.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24"}, + {file = "rpds_py-0.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a"}, + {file = "rpds_py-0.20.0-cp38-none-win32.whl", hash = "sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5"}, + {file = "rpds_py-0.20.0-cp38-none-win_amd64.whl", hash = "sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b"}, + {file = "rpds_py-0.20.0-cp39-none-win32.whl", hash = "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7"}, + {file = "rpds_py-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8"}, + {file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"}, +] + [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -622,21 +817,19 @@ files = [ [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.2" description = "A lil' TOML parser" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] [[package]] name = "types-requests" version = "2.32.0.20240914" description = "Typing stubs for requests" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -651,7 +844,6 @@ urllib3 = ">=2" name = "types-xmltodict" version = "0.13.0.3" description = "Typing stubs for xmltodict" -category = "main" optional = false python-versions = "*" files = [ @@ -663,7 +855,6 @@ files = [ name = "urllib3" version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -681,7 +872,6 @@ zstd = ["zstandard (>=0.18.0)"] name = "xmltodict" version = "0.13.0" description = "Makes working with XML feel like you are working with JSON" -category = "main" optional = false python-versions = ">=3.4" files = [ @@ -692,4 +882,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0" -content-hash = "a1eb4e9fac7ac03ff7b7726b22760c073dc3e289f6906c72280b17cea2e39cac" +content-hash = "a0ca853b5fb61ea80c7a6fce776f2b09640814c05efa4529b73c2a2e4116f12c" diff --git a/pyproject.toml b/pyproject.toml index 1a817eb..712fc39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,8 @@ OWSLib = "0.31.0" xmltodict = "0.13.0" types-requests = "2.32.0.20240914" types-xmltodict = "0.13.0.3" +jsonschema = "^4.23.0" +pytest-mock = "^3.14.0" [tool.poetry.group.dev.dependencies] pytest = "8.3.3" diff --git a/tests/models/__init__.py b/tests/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/models/test_common.py b/tests/models/test_common.py new file mode 100644 index 0000000..73f3bde --- /dev/null +++ b/tests/models/test_common.py @@ -0,0 +1,148 @@ +import json + +import pytest + +from geoservercloud.models import ( # Adjust import based on your module location + I18N, + KeyDollarListDict, +) + + +# Test initialization with input_list (deserialization) +def test_keydollarlistdict_initialization_with_input_list(): + input_list = [{"@key": "host", "$": "localhost"}, {"@key": "port", "$": "5432"}] + + kdl_dict = KeyDollarListDict(input_list) + + assert kdl_dict["host"] == "localhost" + assert kdl_dict["port"] == "5432" + + +# Test initialization without input_list +def test_keydollarlistdict_initialization_without_input_list(): + kdl_dict = KeyDollarListDict() + + assert len(kdl_dict) == 0 # Should be an empty dictionary + + +# Test deserialization of input_list +def test_keydollarlistdict_deserialization(): + input_list = [ + {"@key": "username", "$": "admin"}, + {"@key": "password", "$": "secret"}, + ] + + kdl_dict = KeyDollarListDict(input_list) + + assert kdl_dict["username"] == "admin" + assert kdl_dict["password"] == "secret" + + +# Test serialization method +def test_keydollarlistdict_serialization(): + kdl_dict = KeyDollarListDict() + kdl_dict["host"] = "localhost" + kdl_dict["port"] = "5432" + + expected_output = [ + {"@key": "host", "$": "localhost"}, + {"@key": "port", "$": "5432"}, + ] + + assert kdl_dict.serialize() == expected_output + + +# Test __repr__ method +def test_keydollarlistdict_repr(): + kdl_dict = KeyDollarListDict([{"@key": "db", "$": "postgres"}]) + + expected_repr = "[{'@key': 'db', '$': 'postgres'}]" + + assert repr(kdl_dict) == expected_repr + + +# Test __str__ method +def test_keydollarlistdict_str(): + kdl_dict = KeyDollarListDict([{"@key": "db", "$": "postgres"}]) + + expected_str = json.dumps([{"@key": "db", "$": "postgres"}]) + + assert str(kdl_dict) == expected_str + + +# Test initialization with a string value +def test_i18n_initialization_string(): + keys = ("title", "internationalizedTitle") + value = "Test Title" + + i18n_instance = I18N(keys, value) + + assert i18n_instance.str_key == "title" + assert i18n_instance.i18n_key == "internationalizedTitle" + assert i18n_instance.value == value + assert i18n_instance._payload == ("title", value) + + +# Test initialization with a dictionary value (internationalized) +def test_i18n_initialization_dict(): + keys = ("abstract", "internationalizedAbstract") + value = {"en": "Test Abstract", "fr": "Résumé Test"} + + i18n_instance = I18N(keys, value) + + assert i18n_instance.str_key == "abstract" + assert i18n_instance.i18n_key == "internationalizedAbstract" + assert i18n_instance.value == value + assert i18n_instance._payload == ("internationalizedAbstract", value) + + +# Test invalid value type (neither string nor dictionary) +def test_i18n_invalid_value_type(): + keys = ("title", "internationalizedTitle") + value = 123 # Invalid value type + + with pytest.raises(ValueError, match="Invalid value type"): + I18N(keys, value) + + +# Test str_key property +def test_i18n_str_key_property(): + keys = ("title", "internationalizedTitle") + value = "Test Title" + i18n_instance = I18N(keys, value) + + assert i18n_instance.str_key == "title" + + +# Test i18n_key property +def test_i18n_i18n_key_property(): + keys = ("abstract", "internationalizedAbstract") + value = {"en": "Test Abstract", "fr": "Résumé Test"} + i18n_instance = I18N(keys, value) + + assert i18n_instance.i18n_key == "internationalizedAbstract" + + +# Test value property +def test_i18n_value_property(): + keys = ("title", "internationalizedTitle") + value = "Test Title" + i18n_instance = I18N(keys, value) + + assert i18n_instance.value == "Test Title" + + +def test_i18n_payload_tuple_property(): + keys = ("title", "internationalizedTitle") + value = "Test Title" + i18n_instance = I18N(keys, value) + + assert i18n_instance.payload_tuple == ("title", "Test Title") + + +def test_i18n_repr(): + keys = ("title", "internationalizedTitle") + value = "Test Title" + i18n_instance = I18N(keys, value) + + assert repr(i18n_instance) == json.dumps({"title": "Test Title"}, indent=4) diff --git a/tests/models/test_datastore.py b/tests/models/test_datastore.py new file mode 100644 index 0000000..b03b101 --- /dev/null +++ b/tests/models/test_datastore.py @@ -0,0 +1,110 @@ +from unittest.mock import Mock + +import pytest + +from geoservercloud.models import ( # Adjust based on your actual module name + KeyDollarListDict, + PostGisDataStore, +) + + +# Test initialization of the PostGisDataStore class +def test_postgisdatastore_initialization(): + connection_parameters = KeyDollarListDict( + [{"@key": "host", "$": "localhost"}, {"@key": "port", "$": "5432"}] + ) + + datastore = PostGisDataStore( + "test_workspace", "test_datastore", connection_parameters + ) + + assert datastore.workspace_name == "test_workspace" + assert datastore.data_store_name == "test_datastore" + assert datastore.connection_parameters == connection_parameters + assert datastore.data_store_type == "PostGIS" + + +# Test put_payload method +def test_postgisdatastore_put_payload(): + connection_parameters = KeyDollarListDict( + [{"@key": "host", "$": "localhost"}, {"@key": "port", "$": "5432"}] + ) + + datastore = PostGisDataStore( + "test_workspace", "test_datastore", connection_parameters + ) + + expected_payload = { + "dataStore": { + "name": "test_datastore", + "type": "PostGIS", + "enabled": True, + "connectionParameters": { + "entry": [ + {"@key": "host", "$": "localhost"}, + {"@key": "port", "$": "5432"}, + ] + }, + } + } + + assert datastore.put_payload() == expected_payload + + +# Test post_payload method (should return the same as put_payload) +def test_postgisdatastore_post_payload(): + connection_parameters = KeyDollarListDict( + [{"@key": "host", "$": "localhost"}, {"@key": "port", "$": "5432"}] + ) + + datastore = PostGisDataStore( + "test_workspace", "test_datastore", connection_parameters + ) + + assert datastore.post_payload() == datastore.put_payload() + + +# Test from_response class method +def test_postgisdatastore_from_response(mocker): + mock_response = Mock() + mock_response.json.return_value = { + "dataStore": { + "name": "test_datastore", + "type": "PostGIS", + "connectionParameters": { + "entry": [ + {"@key": "host", "$": "localhost"}, + {"@key": "port", "$": "5432"}, + ] + }, + } + } + + datastore = PostGisDataStore.from_response(mock_response) + + assert datastore.data_store_name == "test_datastore" + assert datastore.data_store_type == "PostGIS" + + # Check that connection parameters were correctly parsed into a KeyDollarListDict + assert datastore.connection_parameters["host"] == "localhost" + assert datastore.connection_parameters["port"] == "5432" + + +# Test parse_connection_parameters method +def test_postgisdatastore_parse_connection_parameters(): + json_data = { + "dataStore": { + "connectionParameters": { + "entry": [ + {"@key": "host", "$": "localhost"}, + {"@key": "port", "$": "5432"}, + ] + } + } + } + + connection_params = PostGisDataStore.parse_connection_parameters(json_data) + + assert isinstance(connection_params, KeyDollarListDict) + assert connection_params["host"] == "localhost" + assert connection_params["port"] == "5432" diff --git a/tests/models/test_datastores.py b/tests/models/test_datastores.py new file mode 100644 index 0000000..b8f061d --- /dev/null +++ b/tests/models/test_datastores.py @@ -0,0 +1,54 @@ +from unittest.mock import Mock + +import pytest + +from geoservercloud.models import DataStores # Replace with the actual module name + + +# Test initialization of DataStores class +def test_datastores_initialization(): + workspace_name = "test_workspace" + datastores = ["store1", "store2"] + + ds = DataStores(workspace_name, datastores) + + assert ds.workspace_name == "test_workspace" + assert ds.datastores == datastores + + +# Test the from_response class method with a valid response +def test_datastores_from_response(mocker): + mock_response = Mock() + mock_response.json.return_value = { + "dataStores": { + "workspace": {"name": "test_workspace"}, + "dataStore": [{"name": "store1"}, {"name": "store2"}], + } + } + + ds = DataStores.from_response(mock_response) + + assert ds.workspace_name == "test_workspace" + assert ds.datastores == ["store1", "store2"] + + +# Test from_response with an empty response +def test_datastores_from_response_empty(): + mock_response = Mock() + mock_response.json.return_value = { + "dataStores": {"workspace": {"name": "empty_workspace"}, "dataStore": []} + } + + ds = DataStores.from_response(mock_response) + + assert ds.workspace_name == "empty_workspace" + assert ds.datastores == [] + + +# Test the __repr__ method +def test_datastores_repr(): + ds = DataStores("test_workspace", ["store1", "store2"]) + + expected_repr = "['store1', 'store2']" + + assert repr(ds) == expected_repr diff --git a/tests/models/test_featuretype.py b/tests/models/test_featuretype.py new file mode 100644 index 0000000..fa0fab3 --- /dev/null +++ b/tests/models/test_featuretype.py @@ -0,0 +1,137 @@ +import json +from unittest.mock import Mock + +from geoservercloud.models import I18N, FeatureType + + +# Test initialization of FeatureType class +def test_featuretype_initialization(): + feature_type = FeatureType( + namespace_name="test_namespace", + name="test_name", + native_name="test_native_name", + srs="EPSG:4326", + title="Test Title", + abstract="Test Abstract", + keywords={"keyword1": "test_keyword"}, + attributes={"attribute1": "value1"}, + ) + + assert feature_type.namespace_name == "test_namespace" + assert feature_type.name == "test_name" + assert feature_type.native_name == "test_native_name" + assert feature_type.srs == "EPSG:4326" + assert feature_type.title.payload_tuple[1] == "Test Title" + assert feature_type.abstract.payload_tuple[1] == "Test Abstract" + assert feature_type.keywords == {"keyword1": "test_keyword"} + assert feature_type.attributes == {"attribute1": "value1"} + + +# Test post_payload method +def test_featuretype_post_payload(): + feature_type = FeatureType( + namespace_name="test_namespace", + name="test_name", + native_name="test_native_name", + srs="EPSG:4326", + title={"de": "Test Title"}, + abstract={"de": "Test Abstract"}, + keywords={"keyword1": "test_keyword"}, + attributes={"attribute1": "value1"}, + ) + + expected_payload = { + "featureType": { + "name": "test_name", + "nativeName": "test_native_name", + "internationalTitle": {"de": "Test Title"}, + "internationalAbstract": {"de": "Test Abstract"}, + "srs": "EPSG:4326", + "keywords": {"keyword1": "test_keyword"}, + "attributes": {"attribute1": "value1"}, + } + } + + assert feature_type.post_payload() == expected_payload + + +# Test create_metadata_link method +def test_featuretype_create_metadata_link(): + feature_type = FeatureType( + namespace_name="test_namespace", + name="test_name", + native_name="test_native_name", + metadata_url="http://example.com/metadata.xml", + metadata_type="TC211", + metadata_format="text/xml", + ) + + expected_metadata_link = { + "metadataLink": { + "type": "text/xml", + "metadataType": "TC211", + "content": "http://example.com/metadata.xml", + } + } + + assert feature_type.metadataLink == expected_metadata_link + + +# Test from_response method +def test_featuretype_from_response(): + mock_response = Mock() + mock_response.json.return_value = { + "featureType": { + "namespace": {"name": "test_namespace"}, + "name": "test_name", + "nativeName": "test_native_name", + "srs": "EPSG:4326", + "title": "Test Title", + "abstract": "Test Abstract", + "keywords": {"keyword1": "test_keyword"}, + "attributes": {"attribute1": "value1"}, + "metadataLinks": { + "metadataLink": { + "type": "text/xml", + "metadataType": "TC211", + "content": "http://example.com/metadata.xml", + } + }, + } + } + + feature_type = FeatureType.from_response(mock_response) + + assert feature_type.namespace_name == "test_namespace" + assert feature_type.name == "test_name" + assert feature_type.native_name == "test_native_name" + assert feature_type.srs == "EPSG:4326" + assert feature_type.title.payload_tuple[1] == "Test Title" + assert feature_type.abstract.payload_tuple[1] == "Test Abstract" + assert feature_type.keywords == {"keyword1": "test_keyword"} + assert feature_type.attributes == {"attribute1": "value1"} + assert feature_type.metadataLink == { + "metadataLink": { + "type": "text/xml", + "metadataType": "TC211", + "content": "http://example.com/metadata.xml", + } + } + + +# Test __repr__ method +def test_featuretype_repr(): + feature_type = FeatureType( + namespace_name="test_namespace", + name="test_name", + native_name="test_native_name", + srs="EPSG:4326", + title="Test Title", + abstract="Test Abstract", + keywords={"keyword1": "test_keyword"}, + attributes={"attribute1": "value1"}, + ) + + expected_repr = json.dumps(feature_type.post_payload(), indent=4) + + assert repr(feature_type) == expected_repr diff --git a/tests/models/test_featuretypes.py b/tests/models/test_featuretypes.py new file mode 100644 index 0000000..4a623ff --- /dev/null +++ b/tests/models/test_featuretypes.py @@ -0,0 +1,37 @@ +import json +from unittest.mock import Mock + +import pytest + +from geoservercloud.models import FeatureTypes # Replace with the correct module path + + +# Test initialization of FeatureTypes class +def test_featuretypes_initialization(): + featuretypes = ["feature1", "feature2"] + feature_types_instance = FeatureTypes(featuretypes) + + assert feature_types_instance.featuretypes == featuretypes + + +# Test from_response method with a valid response +def test_featuretypes_from_response_valid(): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "featureTypes": {"featureType": [{"name": "feature1"}, {"name": "feature2"}]} + } + + feature_types_instance = FeatureTypes.from_response(mock_response) + + assert feature_types_instance.featuretypes == ["feature1", "feature2"] + + +# Test __repr__ method +def test_featuretypes_repr(): + featuretypes = ["feature1", "feature2"] + feature_types_instance = FeatureTypes(featuretypes) + + expected_repr = json.dumps(featuretypes, indent=4) + + assert repr(feature_types_instance) == expected_repr diff --git a/tests/models/test_style.py b/tests/models/test_style.py new file mode 100644 index 0000000..e3ec48a --- /dev/null +++ b/tests/models/test_style.py @@ -0,0 +1,173 @@ +from unittest.mock import Mock + +import pytest + +from geoservercloud.models import Style # Adjust based on the actual import path + + +# Test initialization of Style class +def test_style_initialization(): + style = Style( + name="test_style", + workspace="test_workspace", + format="sld", + language_version={"version": "1.0.0"}, + filename="style.sld", + date_created="2023-10-01", + date_modified="2023-10-02", + legend_url="http://example.com/legend.png", + legend_format="image/png", + legend_width="100", + legend_height="100", + ) + + assert style.name == "test_style" + assert style.workspace == "test_workspace" + assert style.format == "sld" + assert style.language_version == {"version": "1.0.0"} + assert style.filename == "style.sld" + assert style.date_created == "2023-10-01" + assert style.date_modified == "2023-10-02" + assert style.legend == { + "onlineResource": "http://example.com/legend.png", + "format": "image/png", + "width": "100", + "height": "100", + } + + +# Test initialization without a legend +def test_style_initialization_without_legend(): + style = Style( + name="test_style", + workspace="test_workspace", + format="sld", + language_version={"version": "1.0.0"}, + filename="style.sld", + ) + + assert style.legend is None + + +# Test create_legend method +def test_style_create_legend(): + style = Style( + name="test_style", + workspace="test_workspace", + legend_url="http://example.com/legend.png", + legend_format="image/png", + legend_width="100", + legend_height="100", + ) + + assert style.legend == { + "onlineResource": "http://example.com/legend.png", + "format": "image/png", + "width": "100", + "height": "100", + } + + +# Test put_payload method with legend +def test_style_put_payload_with_legend(mocker): + style = Style( + name="test_style", + workspace="test_workspace", + legend_url="http://example.com/legend.png", + legend_format="image/png", + legend_width="100", + legend_height="100", + ) + + expected_payload = { + "style": { + "name": "test_style", + "format": "sld", + "languageVersion": {"version": "1.0.0"}, + "filename": None, + "legend": { + "onlineResource": "http://example.com/legend.png", + "format": "image/png", + "width": "100", + "height": "100", + }, + } + } + + payload = style.put_payload() + assert payload == expected_payload + + +# Test put_payload method without legend +def test_style_put_payload_without_legend(mocker): + style = Style( + name="test_style", + workspace="test_workspace", + ) + + expected_payload = { + "style": { + "name": "test_style", + "format": "sld", + "languageVersion": {"version": "1.0.0"}, + "filename": None, + } + } + + payload = style.put_payload() + assert payload == expected_payload + + +# Test post_payload method +def test_style_post_payload(mocker): + style = Style( + name="test_style", + workspace="test_workspace", + ) + + mock_put_payload = mocker.patch.object( + style, "put_payload", return_value={"style": {}} + ) + + payload = style.post_payload() + + assert payload == {"style": {}} + mock_put_payload.assert_called_once() + + +# Test from_response method +def test_style_from_response(): + mock_response = Mock() + mock_response.json.return_value = { + "style": { + "workspace": "test_workspace", + "name": "test_style", + "format": "sld", + "languageVersion": {"version": "1.0.0"}, + "filename": "style.sld", + "dateCreated": "2023-10-01", + "dateModified": "2023-10-02", + "legend": { + "onlineResource": "http://example.com/legend.png", + "format": "image/png", + "width": "100", + "height": "100", + }, + } + } + + style = Style.from_response(mock_response) + + assert style.name == "test_style" + assert style.workspace == "test_workspace" + assert style.format == "sld" + assert style.language_version == {"version": "1.0.0"} + assert style.filename == "style.sld" + assert style.date_created == "2023-10-01" + assert style.date_modified == "2023-10-02" + assert style.legend == { + "onlineResource": "http://example.com/legend.png", + "format": "image/png", + "width": "100", + "height": "100", + } diff --git a/tests/models/test_styles.py b/tests/models/test_styles.py new file mode 100644 index 0000000..9a8eb4a --- /dev/null +++ b/tests/models/test_styles.py @@ -0,0 +1,69 @@ +from unittest.mock import Mock + +import pytest + +from geoservercloud.models import Styles # Replace with the correct import path + + +# Test initialization of Styles class +def test_styles_initialization(): + workspace = "test_workspace" + styles = ["style1", "style2"] + + styles_instance = Styles(styles, workspace) + + assert styles_instance.workspace == workspace + assert styles_instance.styles == styles + + +# Test the from_response method with a valid response +def test_styles_from_response_valid(): + mock_response = Mock() + mock_response.json.return_value = { + "styles": { + "workspace": "test_workspace", + "style": [{"name": "style1"}, {"name": "style2"}], + } + } + + styles_instance = Styles.from_response(mock_response) + + assert styles_instance.workspace == "test_workspace" + assert styles_instance.styles == ["style1", "style2"] + + +# Test the from_response method when no workspace is provided +def test_styles_from_response_no_workspace(): + mock_response = Mock() + mock_response.json.return_value = { + "styles": {"style": [{"name": "style1"}, {"name": "style2"}]} + } + + styles_instance = Styles.from_response(mock_response) + + assert styles_instance.workspace is None + assert styles_instance.styles == ["style1", "style2"] + + +# Test the from_response method with empty styles list +def test_styles_from_response_empty_styles(): + mock_response = Mock() + mock_response.json.return_value = { + "styles": {"workspace": "test_workspace", "style": []} + } + + styles_instance = Styles.from_response(mock_response) + + assert styles_instance.workspace == "test_workspace" + assert styles_instance.styles == [] + + +# Test the from_response method with no styles section +def test_styles_from_response_no_styles_section(): + mock_response = Mock() + mock_response.json.return_value = {} + + styles_instance = Styles.from_response(mock_response) + + assert styles_instance.workspace is None + assert styles_instance.styles == [] diff --git a/tests/models/test_workspace.py b/tests/models/test_workspace.py new file mode 100644 index 0000000..39df4e2 --- /dev/null +++ b/tests/models/test_workspace.py @@ -0,0 +1,75 @@ +from unittest.mock import Mock + +import pytest + +from geoservercloud.models import Workspace + + +# Test normal initialization of the Workspace class +def test_workspace_initialization(): + workspace = Workspace("test_workspace", isolated=True) + + assert workspace.name == "test_workspace" + assert workspace.isolated is True + + +# Test the put_payload method with isolated=True +def test_workspace_put_payload_isolated(): + workspace = Workspace("test_workspace", isolated=True) + + expected_payload = {"workspace": {"name": "test_workspace", "isolated": True}} + + assert workspace.put_payload() == expected_payload + + +# Test the put_payload method with isolated=False +def test_workspace_put_payload_not_isolated(): + workspace = Workspace("test_workspace", isolated=False) + + expected_payload = {"workspace": {"name": "test_workspace"}} + + assert workspace.put_payload() == expected_payload + + +# Test the post_payload method (should be the same as put_payload) +def test_workspace_post_payload(): + workspace = Workspace("test_workspace", isolated=True) + + expected_payload = workspace.put_payload() + + assert workspace.post_payload() == expected_payload + + +# Test the from_response class method with isolated=True in response +def test_workspace_from_response_isolated(): + mock_response = Mock() + mock_response.json.return_value = { + "workspace": {"name": "test_workspace", "isolated": True} + } + + workspace = Workspace.from_response(mock_response) + + assert workspace.name == "test_workspace" + assert workspace.isolated is True + + +# Test the from_response class method with isolated=False (not present) in response +def test_workspace_from_response_not_isolated(): + mock_response = Mock() + mock_response.json.return_value = {"workspace": {"name": "test_workspace"}} + + workspace = Workspace.from_response(mock_response) + + assert workspace.name == "test_workspace" + assert workspace.isolated is False + + +# Test the from_response class method with missing workspace name +def test_workspace_from_response_missing_name(): + mock_response = Mock() + mock_response.json.return_value = {"workspace": {}} + + workspace = Workspace.from_response(mock_response) + + assert workspace.name is None + assert workspace.isolated is False diff --git a/tests/models/test_workspaces.py b/tests/models/test_workspaces.py new file mode 100644 index 0000000..a461377 --- /dev/null +++ b/tests/models/test_workspaces.py @@ -0,0 +1,39 @@ +from unittest.mock import Mock + +import jsonschema +import pytest + +from geoservercloud.models import Workspaces + + +# Test initialization of the Workspaces class +def test_workspaces_initialization(): + initial_workspaces = {"Workspace1": "http://example.com/ws1"} + workspaces = Workspaces(initial_workspaces) + + assert workspaces.workspaces == initial_workspaces + + +# Test the find method to ensure it finds existing workspaces +def test_workspaces_find_existing(): + initial_workspaces = {"Workspace1": "http://example.com/ws1"} + workspaces = Workspaces(initial_workspaces) + + assert workspaces.find("Workspace1") == "http://example.com/ws1" + + +# Test the find method to ensure it returns None for non-existing workspaces +def test_workspaces_find_non_existing(): + workspaces = Workspaces({"Workspace1": "http://example.com/ws1"}) + + assert workspaces.find("NonExistingWorkspace") is None + + +# Test the from_response method with an empty response +def test_workspaces_from_response_empty(): + mock_response = Mock() + mock_response.json.return_value = {"workspaces": {}} + + workspaces = Workspaces.from_response(mock_response) + + assert len(workspaces.workspaces) == 0 diff --git a/tests/test_acl.py b/tests/test_acl.py index b886923..63d309f 100644 --- a/tests/test_acl.py +++ b/tests/test_acl.py @@ -27,7 +27,7 @@ def test_create_acl_admin_rule(geoserver: GeoServerCloud) -> None: access="ADMIN", role="TEST_ROLE", user="TEST_USER", - workspace="TEST_WORKSPACE", + workspace_name="TEST_WORKSPACE", ) assert response.status_code == 201 @@ -54,7 +54,7 @@ def test_create_acl_rule(geoserver: GeoServerCloud) -> None: priority=1, access="ALLOW", role="TEST_ROLE", - workspace="TEST_WORKSPACE", + workspace_name="TEST_WORKSPACE", service="WMS", ) assert response.status_code == 201 @@ -99,7 +99,7 @@ def test_create_acl_rule_for_requests(geoserver: GeoServerCloud) -> None: priority=1, access="ALLOW", role="TEST_ROLE", - workspace="TEST_WORKSPACE", + workspace_name="TEST_WORKSPACE", service="WMS", requests=["GetCapabilities", "GetMap"], ) diff --git a/tests/test_cascaded_wmts.py b/tests/test_cascaded_wmts.py index bcc6e40..a5d1a04 100644 --- a/tests/test_cascaded_wmts.py +++ b/tests/test_cascaded_wmts.py @@ -83,7 +83,7 @@ def test_create_wmts_store( status=201, ) response = geoserver.create_wmts_store( - workspace=WORKSPACE, + workspace_name=WORKSPACE, name=STORE, capabilities=CAPABILITIES_URL, ) @@ -105,7 +105,7 @@ def test_update_wmts_store( status=200, ) response = geoserver.create_wmts_store( - workspace=WORKSPACE, + workspace_name=WORKSPACE, name=STORE, capabilities=CAPABILITIES_URL, ) @@ -138,7 +138,7 @@ def test_create_wmts_layer( status=201, ) response = geoserver.create_wmts_layer( - workspace=WORKSPACE, + workspace_name=WORKSPACE, wmts_store=STORE, native_layer=NATIVE_LAYER, published_layer=LAYER, @@ -178,7 +178,7 @@ def test_create_wmts_layer_already_exists( status=201, ) response = geoserver.create_wmts_layer( - workspace=WORKSPACE, + workspace_name=WORKSPACE, wmts_store=STORE, native_layer=NATIVE_LAYER, published_layer=LAYER, @@ -214,7 +214,7 @@ def test_create_wmts_layer_international_title( status=201, ) response = geoserver.create_wmts_layer( - workspace=WORKSPACE, + workspace_name=WORKSPACE, wmts_store=STORE, native_layer=NATIVE_LAYER, published_layer=LAYER, diff --git a/tests/test_datastore.py b/tests/test_datastore.py index 780fdee..e622e4e 100644 --- a/tests/test_datastore.py +++ b/tests/test_datastore.py @@ -1,3 +1,4 @@ +import json from collections.abc import Generator from typing import Any @@ -6,6 +7,7 @@ from responses import matchers from geoservercloud.geoservercloud import GeoServerCloud +from geoservercloud.models import PostGisDataStore # Ensure this import is correct from tests.conftest import GEOSERVER_URL WORKSPACE = "test_workspace" @@ -25,6 +27,8 @@ def pg_payload() -> Generator[dict[str, dict[str, Any]], Any, None]: yield { "dataStore": { "name": STORE, + "type": "PostGIS", + "enabled": True, "connectionParameters": { "entry": [ {"@key": "dbtype", "$": "postgis"}, @@ -51,6 +55,8 @@ def jndi_payload() -> Generator[dict[str, dict[str, Any]], Any, None]: "dataStore": { "name": STORE, "description": DESCRIPTION, + "type": "PostGIS (JNDI)", + "enabled": True, "connectionParameters": { "entry": [ {"@key": "dbtype", "$": "postgis"}, @@ -73,6 +79,68 @@ def jndi_payload() -> Generator[dict[str, dict[str, Any]], Any, None]: } +@pytest.fixture(scope="module") +def datastores_response() -> Generator[dict[str, Any], Any, None]: + yield { + "dataStores": { + "dataStore": [ + { + "name": STORE, + "href": f"{GEOSERVER_URL}/rest/workspaces/{WORKSPACE}/datastores/{STORE}.json", + } + ], + } + } + + +def test_get_datastores( + geoserver: GeoServerCloud, datastores_response: dict[str, Any] +) -> None: + with responses.RequestsMock() as rsps: + rsps.get( + url=f"{GEOSERVER_URL}/rest/workspaces/{WORKSPACE}/datastores.json", + status=200, + json=datastores_response, + ) + + datastores = geoserver.get_datastores(workspace_name=WORKSPACE) + assert datastores == ["test_store"] + + +# Test the get_postgis_datastore method with a valid response +def test_get_postgis_datastore_valid( + geoserver: GeoServerCloud, pg_payload: dict[str, dict[str, Any]] +) -> None: + with responses.RequestsMock() as rsps: + rsps.get( + url=f"{geoserver.url}/rest/workspaces/{WORKSPACE}/datastores/{STORE}.json", + json=pg_payload, + status=200, + ) + result = geoserver.get_postgis_datastore(WORKSPACE, STORE) + assert len(rsps.calls) == 1 + # FIXME: I think the geoserver rest endpoint is wrong, might be a problem with the conftest.py stuff. + # assert rsps.calls[0].request.url == geoserver.rest_endpoints.datastore(WORKSPACE, STORE) + assert json.loads(str(result)) == pg_payload + + +# Test the get_postgis_datastore method with a 404 error +def test_get_postgis_datastore_not_found(geoserver: GeoServerCloud) -> None: + datastore_name = "non_existing_datastore" + with responses.RequestsMock() as rsps: + rsps.get( + url=f"{GEOSERVER_URL}/rest/workspaces/{WORKSPACE}/datastores/{datastore_name}.json", + json={"error": "Datastore not found"}, + status=404, + ) + + not_existing_datastore = geoserver.get_postgis_datastore( + WORKSPACE, datastore_name + ) + assert len(rsps.calls) == 1 + assert not_existing_datastore is None + + def test_create_pg_datastore( geoserver: GeoServerCloud, pg_payload: dict[str, dict[str, Any]] ) -> None: @@ -89,8 +157,8 @@ def test_create_pg_datastore( ) response = geoserver.create_pg_datastore( - workspace=WORKSPACE, - datastore=STORE, + workspace_name=WORKSPACE, + datastore_name=STORE, pg_host=HOST, pg_port=PORT, pg_db=DATABASE, @@ -118,8 +186,8 @@ def test_update_pg_datastore( ) response = geoserver.create_pg_datastore( - workspace=WORKSPACE, - datastore=STORE, + workspace_name=WORKSPACE, + datastore_name=STORE, pg_host=HOST, pg_port=PORT, pg_db=DATABASE, @@ -147,8 +215,8 @@ def test_create_jndi_datastore( ) response = geoserver.create_jndi_datastore( - workspace=WORKSPACE, - datastore=STORE, + workspace_name=WORKSPACE, + datastore_name=STORE, jndi_reference=JNDI, pg_schema=SCHEMA, description=DESCRIPTION, @@ -173,8 +241,8 @@ def test_update_jndi_datastore( ) response = geoserver.create_jndi_datastore( - workspace=WORKSPACE, - datastore=STORE, + workspace_name=WORKSPACE, + datastore_name=STORE, jndi_reference=JNDI, pg_schema=SCHEMA, description=DESCRIPTION, diff --git a/tests/test_feature_type.py b/tests/test_feature_type.py index 77823ab..4ddf20e 100644 --- a/tests/test_feature_type.py +++ b/tests/test_feature_type.py @@ -5,6 +5,12 @@ from geoservercloud.geoservercloud import GeoServerCloud +# TODO: add tests for +# - geoservercloud.get_featuretypes() +# - geoservercloud.get_featuretype() +# for the moment just import them as import tests +from geoservercloud.models import FeatureType, FeatureTypes + LAYER = "test_layer" WORKSPACE = "test_workspace" STORE = "test_store" @@ -54,53 +60,3 @@ def feature_type_payload() -> dict[str, dict[str, Any]]: }, } } - - -def test_create_feature_type( - geoserver: GeoServerCloud, feature_type_payload: dict[str, dict[str, Any]] -) -> None: - with responses.RequestsMock() as rsps: - rsps.get( - f"{geoserver.url}/rest/workspaces/{WORKSPACE}/datastores/{STORE}/featuretypes/{LAYER}.json", - status=404, - ) - rsps.post( - f"{geoserver.url}/rest/workspaces/{WORKSPACE}/datastores/{STORE}/featuretypes.json", - match=[responses.matchers.json_params_matcher(feature_type_payload)], - status=201, - ) - response = geoserver.create_feature_type( - workspace=WORKSPACE, - datastore=STORE, - layer=LAYER, - title={"en": "English"}, - abstract={"en": "English"}, - ) - - assert response - assert response.status_code == 201 - - -def test_update_feature_type( - geoserver: GeoServerCloud, feature_type_payload: dict[str, dict[str, Any]] -) -> None: - with responses.RequestsMock() as rsps: - rsps.get( - f"{geoserver.url}/rest/workspaces/{WORKSPACE}/datastores/{STORE}/featuretypes/{LAYER}.json", - status=200, - ) - rsps.put( - f"{geoserver.url}/rest/workspaces/{WORKSPACE}/datastores/{STORE}/featuretypes/{LAYER}.json", - match=[responses.matchers.json_params_matcher(feature_type_payload)], - status=200, - ) - response = geoserver.create_feature_type( - workspace=WORKSPACE, - datastore=STORE, - layer=LAYER, - title={"en": "English"}, - abstract={"en": "English"}, - ) - - assert response - assert response.status_code == 200 diff --git a/tests/test_layer_group.py b/tests/test_layer_group.py index 83d38a5..81fb0ac 100644 --- a/tests/test_layer_group.py +++ b/tests/test_layer_group.py @@ -58,7 +58,7 @@ def test_create_layer_group( response = geoserver.create_layer_group( group=LAYER_GROUP, - workspace=WORKSPACE, + workspace_name=WORKSPACE, layers=LAYERS, title=TITLE, abstract=ABSTRACT, @@ -83,7 +83,7 @@ def test_update_layer_group( response = geoserver.create_layer_group( group=LAYER_GROUP, - workspace=WORKSPACE, + workspace_name=WORKSPACE, layers=LAYERS, title=TITLE, abstract=ABSTRACT, diff --git a/tests/test_role.py b/tests/test_role.py index 06c1808..9633655 100644 --- a/tests/test_role.py +++ b/tests/test_role.py @@ -7,7 +7,7 @@ def test_create_role(geoserver: GeoServerCloud) -> None: role = "test_role" with responses.RequestsMock() as rsps: rsps.post( - url=f"{geoserver.url}/rest/security/roles/role/{role}", + url=f"{geoserver.url}/rest/security/roles/role/{role}.json", status=201, ) @@ -20,10 +20,12 @@ def test_create_role_if_not_exists_case_true(geoserver: GeoServerCloud) -> None: role = "test_role" with responses.RequestsMock() as rsps: rsps.get( - url=f"{geoserver.url}/rest/security/roles", status=200, json={"roles": []} + url=f"{geoserver.url}/rest/security/roles.json", + status=200, + json={"roles": []}, ) rsps.post( - url=f"{geoserver.url}/rest/security/roles/role/{role}", + url=f"{geoserver.url}/rest/security/roles/role/{role}.json", status=201, ) @@ -36,7 +38,7 @@ def test_create_role_if_not_exists_case_false(geoserver: GeoServerCloud) -> None role = "test_role" with responses.RequestsMock() as rsps: rsps.get( - url=f"{geoserver.url}/rest/security/roles", + url=f"{geoserver.url}/rest/security/roles.json", status=200, json={"roles": [role]}, ) @@ -49,7 +51,7 @@ def test_delete_role(geoserver: GeoServerCloud) -> None: role = "test_role" with responses.RequestsMock() as rsps: rsps.delete( - url=f"{geoserver.url}/rest/security/roles/role/{role}", + url=f"{geoserver.url}/rest/security/roles/role/{role}.json", status=200, ) @@ -78,7 +80,7 @@ def test_assign_role_to_user(geoserver: GeoServerCloud) -> None: role = "test_role" with responses.RequestsMock() as rsps: rsps.post( - url=f"{geoserver.url}/rest/security/roles/role/{role}/user/{user}", + url=f"{geoserver.url}/rest/security/roles/role/{role}/user/{user}.json", status=200, ) @@ -92,7 +94,7 @@ def test_remove_role_from_user(geoserver: GeoServerCloud) -> None: role = "test_role" with responses.RequestsMock() as rsps: rsps.delete( - url=f"{geoserver.url}/rest/security/roles/role/{role}/user/{user}", + url=f"{geoserver.url}/rest/security/roles/role/{role}/user/{user}.json", status=200, ) diff --git a/tests/test_style.py b/tests/test_style.py index 482faf3..d844416 100644 --- a/tests/test_style.py +++ b/tests/test_style.py @@ -4,11 +4,79 @@ import responses from geoservercloud.geoservercloud import GeoServerCloud +from geoservercloud.models import Styles from tests.conftest import GEOSERVER_URL STYLE = "test_style" +def test_get_styles_no_workspace(geoserver: GeoServerCloud): + # Mock the self.rest_endpoints.styles() URL + with responses.RequestsMock() as rsps: + rsps.get( + url=f"{GEOSERVER_URL}/rest/styles.json", + status=200, + json={ + "styles": { + "style": [ + { + "name": "style1", + "href": f"{GEOSERVER_URL}/rest/styles/style1.json", + }, + { + "name": "style2", + "href": f"{GEOSERVER_URL}/rest/styles/style2.json", + }, + ] + } + }, + ) + result = geoserver.get_styles() + + assert result == ["style1", "style2"] + + +@responses.activate +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", + status=200, + json={ + "styles": { + "style": [ + { + "name": "style3", + "href": f"{GEOSERVER_URL}/rest/workspaces/{workspace_name}/styles/style3.json", + }, + { + "name": "style4", + "href": f"{GEOSERVER_URL}/rest/workspaces/{workspace_name}/styles/style4.json", + }, + ] + } + }, + ) + result = geoserver.get_styles(workspace_name) + + assert result == ["style3", "style4"] + + +def test_get_style_no_workspace(geoserver: GeoServerCloud) -> None: + with responses.RequestsMock() as rsps: + rsps.get( + url=f"{GEOSERVER_URL}/rest/styles/{STYLE}.json", + status=200, + json={"style": {"name": STYLE}}, + ) + + style = geoserver.get_style(STYLE) + + assert style.name == STYLE # type: ignore + assert style.workspace is None # type: ignore + + def test_create_style(geoserver: GeoServerCloud) -> None: file_path = (Path(__file__).parent / "resources/style.sld").resolve() with responses.RequestsMock() as rsps: @@ -17,7 +85,7 @@ def test_create_style(geoserver: GeoServerCloud) -> None: status=404, ) rsps.post( - url=f"{GEOSERVER_URL}/rest/styles", + url=f"{GEOSERVER_URL}/rest/styles.json", status=201, ) @@ -57,7 +125,7 @@ def test_create_style_zip(geoserver: GeoServerCloud) -> None: status=404, ) rsps.post( - url=f"{GEOSERVER_URL}/rest/styles", + url=f"{GEOSERVER_URL}/rest/styles.json", status=201, ) diff --git a/tests/test_user.py b/tests/test_user.py index 1d833da..c7b12ab 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -9,7 +9,7 @@ def test_create_user(geoserver: GeoServerCloud) -> None: with responses.RequestsMock() as rsps: rsps.post( - url=f"{geoserver.url}/rest/security/usergroup/users", + url=f"{geoserver.url}/rest/security/usergroup/users.json", status=201, match=[ responses.matchers.json_params_matcher( @@ -30,7 +30,7 @@ def test_create_user(geoserver: GeoServerCloud) -> None: def test_update_user_password(geoserver: GeoServerCloud) -> None: with responses.RequestsMock() as rsps: rsps.post( - url=f"{geoserver.url}/rest/security/usergroup/user/{TEST_USER}", + url=f"{geoserver.url}/rest/security/usergroup/user/{TEST_USER}.json", status=200, match=[ responses.matchers.json_params_matcher( @@ -49,7 +49,7 @@ def test_update_user_password(geoserver: GeoServerCloud) -> None: def test_update_user_enabled(geoserver: GeoServerCloud) -> None: with responses.RequestsMock() as rsps: rsps.post( - url=f"{geoserver.url}/rest/security/usergroup/user/{TEST_USER}", + url=f"{geoserver.url}/rest/security/usergroup/user/{TEST_USER}.json", status=200, match=[ responses.matchers.json_params_matcher( @@ -68,7 +68,8 @@ def test_update_user_enabled(geoserver: GeoServerCloud) -> None: def test_delete_user(geoserver: GeoServerCloud) -> None: with responses.RequestsMock() as rsps: rsps.delete( - url=f"{geoserver.url}/rest/security/usergroup/user/{TEST_USER}", status=200 + url=f"{geoserver.url}/rest/security/usergroup/user/{TEST_USER}.json", + status=200, ) response = geoserver.delete_user(TEST_USER) assert response.status_code == 200 diff --git a/tests/test_workspace.py b/tests/test_workspace.py index 34f5ea9..41de135 100644 --- a/tests/test_workspace.py +++ b/tests/test_workspace.py @@ -5,6 +5,26 @@ from tests.conftest import GEOSERVER_URL +def test_list_workspaces(geoserver: GeoServerCloud) -> None: + with responses.RequestsMock() as rsps: + rsps.get( + url=f"{GEOSERVER_URL}/rest/workspaces.json", + status=200, + json={ + "workspaces": { + "workspace": [ + { + "name": "test_workspace", + "href": "http://localhost:8080/geoserver/rest/workspaces/test_workspace.json", + } + ] + } + }, + ) + workspaces = geoserver.get_workspaces() + assert workspaces.workspaces == ["test_workspace"] + + def test_create_workspace(geoserver: GeoServerCloud) -> None: workspace = "test_workspace" isolated = True