diff --git a/docs/source/change_log.rst b/docs/source/change_log.rst index e68b944..aacfeb6 100644 --- a/docs/source/change_log.rst +++ b/docs/source/change_log.rst @@ -14,6 +14,9 @@ Change Log * Removed ``key_column`` parameter and added ``srid`` parameter (coordinate system of the layer, default is 4326) from ``publish_featurestore_sqlview`` function * Solved the Bug `#73 `_ and `#69 `_ * ``create_layergroup`` function added +* ``update_layergroup`` function added +* ``delete_layergroup`` function added +* Added layer and workspace checks to layergroup methods ``[V2.1.2 - 2021-10-14]`` diff --git a/geo/Geoserver.py b/geo/Geoserver.py index c8b47ca..8d0dc4f 100644 --- a/geo/Geoserver.py +++ b/geo/Geoserver.py @@ -12,7 +12,7 @@ from .supports import prepare_zip_file -# call back class for read the data +# call back class for reading the data class DataProvider(object): def __init__(self, data): self.data = data @@ -68,6 +68,8 @@ def _requests(self, method: str, url: str, **kwargs) -> requests.Response: return requests.get(url, auth=(self.username, self.password), **kwargs) elif method == "put": return requests.put(url, auth=(self.username, self.password), **kwargs) + elif method == "delete": + return requests.delete(url, auth=(self.username, self.password), **kwargs) # _______________________________________________________________________________________________ # @@ -94,7 +96,7 @@ def get_version(self): """ try: url = "{}/rest/about/version.json".format(self.service_url) - r = requests.get(url, auth=(self.username, self.password)) + r = self._requests("get", url) return r.json() except Exception as e: @@ -156,6 +158,124 @@ def reset(self): except Exception as e: return "reload error: {}".format(e) + # _______________________________________________________________________________________________ + # + # WORKSPACES + # _______________________________________________________________________________________________ + # + + def get_default_workspace(self): + """ + Returns the default workspace. + """ + try: + url = "{}/rest/workspaces/default".format(self.service_url) + r = requests.get(url, auth=(self.username, self.password)) + return r.json() + + except Exception as e: + return "get_default_workspace error: {}".format(e) + + def get_workspace(self, workspace): + """ + get name workspace if exist + Example: curl -v -u admin:admin -XGET -H "Accept: text/xml" http://localhost:8080/geoserver/rest/workspaces/acme.xml + """ + try: + payload = {"recurse": "true"} + url = "{}/rest/workspaces/{}.json".format(self.service_url, workspace) + r = requests.get(url, auth=(self.username, self.password), params=payload) + if r.status_code == 200: + return r.json() + else: + return None + + except Exception as e: + return "Error: {}".format(e) + + def get_workspaces(self): + """ + Returns all the workspaces. + """ + try: + url = "{}/rest/workspaces".format(self.service_url) + r = requests.get(url, auth=(self.username, self.password)) + return r.json() + + except Exception as e: + return "get_workspaces error: {}".format(e) + + def set_default_workspace(self, workspace: str): + """ + Set the default workspace. + """ + try: + url = "{}/rest/workspaces/default".format(self.service_url) + data = "{}".format(workspace) + print(url, data) + r = self._requests( + "put", + url, + data=data, + headers={"content-type": "text/xml"}, + ) + + if r.status_code == 200: + return "Status code: {}, default workspace {} set!".format( + r.status_code, workspace + ) + + except Exception as e: + return "reload error: {}".format(e) + + def create_workspace(self, workspace: str): + """ + Create a new workspace in geoserver. + + The geoserver workspace url will be same as the name of the workspace. + """ + try: + url = "{}/rest/workspaces".format(self.service_url) + data = "{}".format(workspace) + headers = {"content-type": "text/xml"} + r = self._requests("post", url, data=data, headers=headers) + + if r.status_code == 201: + return "{} Workspace {} created!".format(r.status_code, workspace) + + if r.status_code == 401: + raise Exception("The workspace already exist") + + else: + raise Exception("The workspace can not be created") + + except Exception as e: + return "Error: {}".format(e) + + def delete_workspace(self, workspace: str): + """ + + Parameters + ---------- + workspace : str + + """ + try: + payload = {"recurse": "true"} + url = "{}/rest/workspaces/{}".format(self.service_url, workspace) + r = requests.delete( + url, auth=(self.username, self.password), params=payload + ) + + if r.status_code == 200: + return "Status code: {}, delete workspace".format(r.status_code) + + else: + raise Exception("Error: {} {}".format(r.status_code, r.content)) + + except Exception as e: + return "Error: {}".format(e) + # _______________________________________________________________________________________________ # # DATASTORES @@ -177,7 +297,7 @@ def get_datastore(self, store_name: str, workspace: Optional[str] = None): self.service_url, workspace, store_name ) - r = requests.get(url, auth=(self.username, self.password)) + r = self._requests("get", url) return r.json() except Exception as e: @@ -194,7 +314,9 @@ def get_datastores(self, workspace: Optional[str] = None): if workspace is None: workspace = "default" - url = "{}/rest/workspaces/{}/datastores.json".format(self.service_url, workspace) + url = "{}/rest/workspaces/{}/datastores.json".format( + self.service_url, workspace + ) r = requests.get(url, auth=(self.username, self.password)) return r.json() @@ -220,7 +342,7 @@ def get_coveragestore( url = "{}/rest/workspaces/{}/coveragestores/{}.json".format( self.service_url, workspace, coveragestore_name ) - r = requests.get(url, auth=(self.username, self.password), params=payload) + r = self._requests(method="get", url=url, params=payload) # print("Status code: {}, Get coverage store".format(r.status_code)) return r.json() @@ -236,7 +358,9 @@ def get_coveragestores(self, workspace: str = None): if workspace is None: workspace = "default" - url = "{}/rest/workspaces/{}/coveragestores".format(self.service_url, workspace) + url = "{}/rest/workspaces/{}/coveragestores".format( + self.service_url, workspace + ) r = requests.get(url, auth=(self.username, self.password)) return r.json() @@ -291,14 +415,14 @@ def create_coveragestore( r = None try: with open(path, "rb") as f: - r = requests.put( - url, data=f, auth=(self.username, self.password), headers=headers - ) + r = self._requests(method="put", url=url, data=f, headers=headers) - if r.status_code != 201: - return "{}: The coveragestore can not be created!".format(r.status_code) - else: - return "The coveragestore has been created successfully!" + if r.status_code != 201: + return "{}: The coveragestore can not be created!".format( + r.status_code + ) + else: + return "The coveragestore has been created successfully!" except Exception as e: return "Error: {}".format(e) @@ -356,11 +480,8 @@ def publish_time_dimension_to_coveragestore( r = None try: - r = requests.put( - url, - data=time_dimension_data, - auth=(self.username, self.password), - headers=headers, + r = self._requests( + method="put", url=url, data=time_dimension_data, headers=headers ) if r.status_code not in [200, 201]: @@ -388,8 +509,11 @@ def get_layer(self, layer_name: str, workspace: Optional[str] = None): self.service_url, workspace, layer_name ) - r = requests.get(url, auth=(self.username, self.password)) - return r.json() + r = self._requests("get", url) + if r.status_code in [200, 201]: + return r.json() + else: + return None except Exception as e: return "get_layer error: {}".format(e) @@ -427,9 +551,7 @@ def delete_layer(self, layer_name: str, workspace: Optional[str] = None): if workspace is None: url = "{}/rest/layers/{}".format(self.service_url, layer_name) - r = requests.delete( - url, auth=(self.username, self.password), params=payload - ) + r = self._requests(method="delete", url=url, params=payload) if r.status_code == 200: return "Status code: {}, delete layer".format(r.status_code) @@ -457,7 +579,9 @@ def get_layergroups(self, workspace: Optional[str] = None): url = "{}/rest/layergroups".format(self.service_url) if workspace is not None: - url = "{}/rest/workspaces/{}/layergroups".format(self.service_url, workspace) + url = "{}/rest/workspaces/{}/layergroups".format( + self.service_url, workspace + ) r = requests.get(url, auth=(self.username, self.password)) return r.json() @@ -475,15 +599,18 @@ def get_layergroup(self, layer_name: str, workspace: Optional[str] = None): self.service_url, workspace, layer_name ) - r = requests.get(url, auth=(self.username, self.password)) - return r.json() + r = self._requests("get", url) + if r.status_code in [200, 201]: + return r.json() + else: + return None except Exception as e: return "get_layer error: {}".format(e) def create_layergroup( self, - name: str = "geoserver-rest layergroup", + name: str = "geoserver-rest-layergroup", mode: str = "single", title: str = "geoserver-rest layer group", abstract_text: str = "A new layergroup created with geoserver-rest python package", @@ -532,6 +659,9 @@ def create_layergroup( if workspace: assert isinstance(workspace, str), "Workspace must be of type String:''" + # check if the workspace is valid in Geoserver + if self.get_workspace(workspace) is None: + raise Exception("Workspace is not valid in Geoserver Instance") supported_modes: Set = { "single", @@ -553,19 +683,37 @@ def create_layergroup( raise Exception( f"Format not supported. Acceptable formats are : {supported_formats}" ) + # check if it already exist in Geoserver + if self.get_layergroup(name) is not None: + raise Exception(f"Layergroup: {name} already exist in Geoserver instance") + if len(layers) == 0: raise Exception("No layer provided!") + else: + for layer in layers: + # check if it is valid in geoserver + + if ( + self.get_layer( + layer_name=layer, + workspace=workspace if workspace is not None else None, + ) + is not None + ): + ... + else: + raise Exception( + f"Layer: {layer} is not a valid layer in the Geoserver instance" + ) skeleton = "" if workspace: - # TODO: check if it is valid in geoserver skeleton += f"{workspace}" - # metadata structure = [{about:"",content_url:""},{...}] metadata_xml_list = [] - if len(metadata) > 1: + if len(metadata) >= 1: for meta in metadata: metadata_about = meta.get("about") metadata_content_url = meta.get("content_url") @@ -582,8 +730,6 @@ def create_layergroup( metadata_xml = f"{''.join(['{}']*len(metadata_xml_list)).format(*metadata_xml_list)}" skeleton += metadata_xml - - # TODO: check if Layer exist in Geoserver layers_xml_list: List[str] = [] for layer in layers: @@ -599,7 +745,7 @@ def create_layergroup( layers_xml: str = f"{''.join(['{}']*len(layers)).format(*layers_xml_list)}" skeleton += layers_xml - if len(keywords) > 1: + if len(keywords) >= 1: keyword_xml_list: List[str] = [ f"{keyword}" for keyword in keywords @@ -624,11 +770,137 @@ def create_layergroup( ) if response.status_code in [200, 201]: - layergroup_url = f"{self.service_url}/layergroups/{name}.{formats}" + layergroup_url = f"{self.service_url}/rest/layergroups/{name}.{formats}" return f"layergroup created successfully! Layergroup link: {layergroup_url}" else: return "Error creating layergroup" + def update_layergroup( + self, + layergroup_name, + title: Optional[str] = None, + abstract_text: Optional[str] = None, + formats: str = "html", + metadata: List[dict] = [], + keywords: List[str] = [], + ) -> str: + """ + Updates a Layergroup. + + Parameters + ---------- + layergroup_name: str, required + mode : str, optional + title : str, optional + abstract_text : str, optional + formats : str, optional + metadata : list, optional + keywords : list, optional + + """ + + # check if layergroup is valid in Geoserver + + if self.get_layergroup(layer_name=layergroup_name) is None: + raise Exception( + f"Layer group: {layergroup_name} is not a valid layer group in the Geoserver instance" + ) + if title is not None: + assert isinstance(title, str), "Title must be of type String:''" + if abstract_text is not None: + assert isinstance( + abstract_text, str + ), "Abstract text must be of type String:''" + assert isinstance(formats, str), "Format must be of type String:''" + assert isinstance( + metadata, list + ), "Metadata must be of type List of dict:[{'about':'geoserver rest data metadata','content_url':'lint to content url'}]" + assert isinstance( + keywords, list + ), "Keywords must be of type List:['keyword1','keyword2'...]" + + supported_formats: Set = {"html", "json", "xml"} + + if formats.lower() != "html" and formats.lower() not in supported_formats: + + raise Exception( + f"Format not supported. Acceptable formats are : {supported_formats}" + ) + + skeleton = "" + + if title: + skeleton += f"{title}" + if abstract_text: + skeleton += f"{abstract_text}" + + metadata_xml_list = [] + + if len(metadata) >= 1: + for meta in metadata: + metadata_about = meta.get("about") + metadata_content_url = meta.get("content_url") + metadata_xml_list.append( + f""" + + text/plain + {metadata_about} + ISO19115:2003 + {metadata_content_url} + + """ + ) + + metadata_xml = f"{''.join(['{}']*len(metadata_xml_list)).format(*metadata_xml_list)}" + skeleton += metadata_xml + + if len(keywords) >= 1: + + keyword_xml_list: List[str] = [ + f"{keyword}" for keyword in keywords + ] + keywords_xml: str = f"{''.join(['{}']*len(keywords)).format(*keyword_xml_list)}" + skeleton += keywords_xml + + data = f""" + + {skeleton} + + """ + + url = f"{self.service_url}/rest/layergroups/{layergroup_name}" + + response = self._requests( + method="put", + url=url, + data=data, + headers={"content-type": "text/xml", "accept": "application/xml"}, + ) + if response.status_code in [200, 201]: + layergroup_url = ( + f"{self.service_url}/rest/layergroups/{layergroup_name}.{formats}" + ) + return f"layergroup updated successfully! Layergroup link: {layergroup_url}" + else: + return "Error updating layergroup" + + def delete_layergroup( + self, + layergroup_name: str, + ) -> str: + if self.get_layergroup(layer_name=layergroup_name) is None: + raise Exception( + f"Layer group: {layergroup_name} is not a valid layer group in the Geoserver instance" + ) + + url = f"{self.service_url}/rest/layergroups/{layergroup_name}" + + response = self._requests(url=url, method="delete") + if response.status_code in [200, 201]: + return "Layer group deleted successfully" + else: + return "Error deleting layer group" + # _______________________________________________________________________________________________ # # STYLES @@ -646,7 +918,7 @@ def get_style(self, style_name, workspace: Optional[str] = None): self.service_url, workspace, style_name ) - r = requests.get(url, auth=(self.username, self.password)) + r = self._requests("get", url) return r.json() except Exception as e: @@ -660,7 +932,9 @@ def get_styles(self, workspace: Optional[str] = None): url = "{}/rest/styles.json".format(self.service_url) if workspace is not None: - url = "{}/rest/workspaces/{}/styles.json".format(self.service_url, workspace) + url = "{}/rest/workspaces/{}/styles.json".format( + self.service_url, workspace + ) r = requests.get(url, auth=(self.username, self.password)) return r.json() @@ -716,13 +990,7 @@ def upload_style( r = None try: - r = requests.post( - url, - data=style_xml, - auth=(self.username, self.password), - headers=headers, - ) - + r = self._requests(method="post", url=url, data=style_xml, headers=headers) with open(path, "rb") as f: r_sld = requests.put( url + "/" + name, @@ -801,10 +1069,10 @@ def create_coveragestyle( r = None try: - r = requests.post( + r = self._requests( + "post", url, data=style_xml, - auth=(self.username, self.password), headers=headers, ) @@ -873,18 +1141,18 @@ def create_catagorized_featurestyle( r = None try: - r = requests.post( + r = self._requests( + "post", url, data=style_xml, - auth=(self.username, self.password), headers=headers, ) with open("style.sld", "rb") as f: - r_sld = requests.put( + r_sld = self._requests( + "put", url + "/" + style_name, data=f.read(), - auth=(self.username, self.password), headers=header_sld, ) if r_sld.status_code not in [200, 201]: @@ -940,18 +1208,18 @@ def create_outline_featurestyle( r = None try: - r = requests.post( + r = self._requests( + "post", url, data=style_xml, - auth=(self.username, self.password), headers=headers, ) with open("style.sld", "rb") as f: - r_sld = requests.put( + r_sld = self._requests( + "put", url + "/" + style_name, data=f.read(), - auth=(self.username, self.password), headers=header_sld, ) if r_sld.status_code not in [200, 201]: @@ -1015,18 +1283,18 @@ def create_classified_featurestyle( r = None try: - r = requests.post( + r = self._requests( + "post", url, data=style_xml, - auth=(self.username, self.password), headers=headers, ) with open("style.sld", "rb") as f: - r_sld = requests.put( + r_sld = self._requests( + "put", url + "/" + style_name, data=f.read(), - auth=(self.username, self.password), headers=header_sld, ) if r_sld.status_code not in [200, 201]: @@ -1073,10 +1341,10 @@ def publish_style( r = None try: - r = requests.put( + r = self._requests( + "put", url, data=style_xml, - auth=(self.username, self.password), headers=headers, ) @@ -1101,9 +1369,7 @@ def delete_style(self, style_name: str, workspace: Optional[str] = None): if workspace is None: url = "{}/rest/styles/{}".format(self.service_url, style_name) - r = requests.delete( - url, auth=(self.username, self.password), params=payload - ) + r = self._requests("delete", url, params=payload) if r.status_code == 200: return "Status code: {}, delete style".format(r.status_code) @@ -1114,126 +1380,6 @@ def delete_style(self, style_name: str, workspace: Optional[str] = None): except Exception as e: return "Error: {}".format(e) - # _______________________________________________________________________________________________ - # - # WORKSPACES - # _______________________________________________________________________________________________ - # - - def get_default_workspace(self): - """ - Returns the default workspace. - """ - try: - url = "{}/rest/workspaces/default".format(self.service_url) - r = requests.get(url, auth=(self.username, self.password)) - return r.json() - - except Exception as e: - return "get_default_workspace error: {}".format(e) - - def get_workspace(self, workspace): - """ - get name workspace if exist - Example: curl -v -u admin:admin -XGET -H "Accept: text/xml" http://localhost:8080/geoserver/rest/workspaces/acme.xml - """ - try: - payload = {"recurse": "true"} - url = "{}/rest/workspaces/{}.json".format(self.service_url, workspace) - r = requests.get(url, auth=(self.username, self.password), params=payload) - if r.status_code == 200: - return r.json() - else: - return None - - except Exception as e: - return "Error: {}".format(e) - - def get_workspaces(self): - """ - Returns all the workspaces. - """ - try: - url = "{}/rest/workspaces".format(self.service_url) - r = requests.get(url, auth=(self.username, self.password)) - return r.json() - - except Exception as e: - return "get_workspaces error: {}".format(e) - - def set_default_workspace(self, workspace: str): - """ - Set the default workspace. - """ - try: - url = "{}/rest/workspaces/default".format(self.service_url) - data = "{}".format(workspace) - print(url, data) - r = requests.put( - url, - data, - auth=(self.username, self.password), - headers={"content-type": "text/xml"}, - ) - - if r.status_code == 200: - return "Status code: {}, default workspace {} set!".format( - r.status_code, workspace - ) - - except Exception as e: - return "reload error: {}".format(e) - - def create_workspace(self, workspace: str): - """ - Create a new workspace in geoserver. - - The geoserver workspace url will be same as the name of the workspace. - """ - try: - url = "{}/rest/workspaces".format(self.service_url) - data = "{}".format(workspace) - headers = {"content-type": "text/xml"} - r = requests.post( - url, data, auth=(self.username, self.password), headers=headers - ) - - if r.status_code == 201: - return "{} Workspace {} created!".format(r.status_code, workspace) - - if r.status_code == 401: - raise Exception("The workspace already exist") - - else: - raise Exception("The workspace can not be created") - - except Exception as e: - return "Error: {}".format(e) - - def delete_workspace(self, workspace: str): - """ - - Parameters - ---------- - workspace : str - - """ - try: - payload = {"recurse": "true"} - url = "{}/rest/workspaces/{}".format(self.service_url, workspace) - r = requests.delete( - url, auth=(self.username, self.password), params=payload - ) - - if r.status_code == 200: - return "Status code: {}, delete workspace".format(r.status_code) - - else: - raise Exception("Error: {} {}".format(r.status_code, r.content)) - - except Exception as e: - return "Error: {}".format(e) - # _______________________________________________________________________________________________ # # FEATURES AND DATASTORES @@ -1388,10 +1534,10 @@ def create_featurestore( self.service_url, workspace, store_name ) - r = requests.put( + r = self._requests( + "put", url, data=database_connection, - auth=(self.username, self.password), headers=headers, ) @@ -1400,10 +1546,10 @@ def create_featurestore( r.status_code, r.content ) else: - r = requests.post( + r = self._requests( + "post", url, data=database_connection, - auth=(self.username, self.password), headers=headers, ) @@ -1461,12 +1607,12 @@ def create_datastore( url = "{}/rest/workspaces/{}/datastores/{}".format( self.service_url, workspace, name ) - r = requests.put( - url, data, auth=(self.username, self.password), headers=headers - ) + r = self._requests("put", url, data=data, headers=headers) else: - url = "{}/rest/workspaces/{}/datastores".format(self.service_url, workspace) + url = "{}/rest/workspaces/{}/datastores".format( + self.service_url, workspace + ) r = requests.post( url, data, auth=(self.username, self.password), headers=headers )