Geoserver.py 68 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871
  1. # -*- coding: utf-8 -*-
  2. __author__ = 'wanger'
  3. __date__ = '2024-08-20'
  4. __copyright__ = '(C) 2024 by siwei'
  5. __revision__ = '1.0'
  6. # inbuilt libraries
  7. import os
  8. from typing import List, Optional, Set, Union, Dict, Iterable, Any
  9. from pathlib import Path
  10. # third-party libraries
  11. import requests
  12. from xmltodict import parse, unparse
  13. # custom functions
  14. from processing.tools.GeoServer.Calculation_gdal import raster_value
  15. from processing.tools.GeoServer.Style import catagorize_xml, classified_xml, coverage_style_xml, outline_only_xml
  16. from processing.tools.GeoServer.supports import prepare_zip_file, is_valid_xml, is_surrounded_by_quotes
  17. default_gridset_name = "WebMercatorQuadx2" # 默认切片方案
  18. default_seed_type = "seed" # 默认切片请求类型 Type can be seed (add tiles), reseed (replace tiles), or truncate (remove tiles)
  19. default_cache_start = 0 # 默认切片始终级别
  20. default_cache_stop = 18
  21. def _parse_request_options(request_options: Dict[str, Any]):
  22. return request_options if request_options is not None else {}
  23. # Custom exceptions.
  24. class GeoserverException(Exception):
  25. def __init__(self, status, message):
  26. self.status = status
  27. self.message = message
  28. super().__init__(f"Status : {self.status} - {self.message}")
  29. # call back class for reading the data
  30. class DataProvider:
  31. def __init__(self, data):
  32. self.data = data
  33. self.finished = False
  34. def read_cb(self, size):
  35. assert len(self.data) <= size
  36. if not self.finished:
  37. self.finished = True
  38. return self.data
  39. else:
  40. # Nothing more to read
  41. return ""
  42. # callback class for reading the files
  43. class FileReader:
  44. def __init__(self, fp):
  45. self.fp = fp
  46. def read_callback(self, size):
  47. return self.fp.read(size)
  48. class Geoserver:
  49. def __init__(
  50. self,
  51. service_url: str = "http://localhost:28085/geoserver", # default deployment url during installation
  52. username: str = "admin", # default username during geoserver installation
  53. password: str = "geoserver", # default password during geoserver installation
  54. request_options: Dict[str, Any] = None # additional parameters to be sent with each request
  55. ):
  56. self.service_url = service_url
  57. self.username = username
  58. self.password = password
  59. self.request_options = request_options if request_options is not None else {}
  60. def _requests(self,
  61. method: str,
  62. url: str,
  63. **kwargs) -> requests.Response:
  64. if method.lower() == "post":
  65. return requests.post(url, auth=(self.username, self.password), **kwargs, **self.request_options)
  66. elif method.lower() == "get":
  67. return requests.get(url, auth=(self.username, self.password), **kwargs, **self.request_options)
  68. elif method.lower() == "put":
  69. return requests.put(url, auth=(self.username, self.password), **kwargs, **self.request_options)
  70. elif method.lower() == "delete":
  71. return requests.delete(url, auth=(self.username, self.password), **kwargs, **self.request_options)
  72. def get_manifest(self):
  73. url = "{}/rest/about/manifest.json".format(self.service_url)
  74. r = self._requests("get", url)
  75. if r.status_code == 200:
  76. return r.json()
  77. else:
  78. raise GeoserverException(r.status_code, r.content)
  79. def get_version(self):
  80. url = "{}/rest/about/version.json".format(self.service_url)
  81. r = self._requests("get", url)
  82. if r.status_code == 200:
  83. return r.json()
  84. else:
  85. raise GeoserverException(r.status_code, r.content)
  86. def get_status(self):
  87. url = "{}/rest/about/status.json".format(self.service_url)
  88. r = self._requests("get", url)
  89. if r.status_code == 200:
  90. return r.json()
  91. else:
  92. raise GeoserverException(r.status_code, r.content)
  93. def get_system_status(self):
  94. url = "{}/rest/about/system-status.json".format(self.service_url)
  95. r = self._requests("get", url)
  96. if r.status_code == 200:
  97. return r.json()
  98. else:
  99. raise GeoserverException(r.status_code, r.content)
  100. def reload(self):
  101. url = "{}/rest/reload".format(self.service_url)
  102. r = self._requests("post", url)
  103. if r.status_code == 200:
  104. return "Status code: {}".format(r.status_code)
  105. else:
  106. raise GeoserverException(r.status_code, r.content)
  107. def reset(self):
  108. url = "{}/rest/reset".format(self.service_url)
  109. r = self._requests("post", url)
  110. if r.status_code == 200:
  111. return "Status code: {}".format(r.status_code)
  112. else:
  113. raise GeoserverException(r.status_code, r.content)
  114. # _______________________________________________________________________________________________
  115. #
  116. # WORKSPACES
  117. # _______________________________________________________________________________________________
  118. #
  119. def get_default_workspace(self):
  120. url = "{}/rest/workspaces/default".format(self.service_url)
  121. r = self._requests("get", url)
  122. if r.status_code == 200:
  123. return r.json()
  124. else:
  125. raise GeoserverException(r.status_code, r.content)
  126. def get_workspace(self, workspace):
  127. url = "{}/rest/workspaces/{}.json".format(self.service_url, workspace)
  128. r = self._requests("get", url, params={"recurse": "true"})
  129. if r.status_code == 200:
  130. return r.json()
  131. else:
  132. raise GeoserverException(r.status_code, r.content)
  133. def get_workspaces(self):
  134. url = "{}/rest/workspaces".format(self.service_url)
  135. r = self._requests("get", url)
  136. if r.status_code == 200:
  137. return r.json()
  138. else:
  139. raise GeoserverException(r.status_code, r.content)
  140. def set_default_workspace(self, workspace: str):
  141. url = "{}/rest/workspaces/default".format(self.service_url)
  142. data = "<workspace><name>{}</name></workspace>".format(workspace)
  143. r = self._requests(
  144. "put",
  145. url,
  146. data=data,
  147. headers={"content-type": "text/xml"}
  148. )
  149. if r.status_code == 200:
  150. return "Status code: {}, default workspace {} set!".format(
  151. r.status_code, workspace
  152. )
  153. else:
  154. raise GeoserverException(r.status_code, r.content)
  155. def create_workspace(self, workspace: str):
  156. url = "{}/rest/workspaces".format(self.service_url)
  157. data = "<workspace><name>{}</name></workspace>".format(workspace)
  158. headers = {"content-type": "text/xml"}
  159. r = self._requests("post", url, data=data, headers=headers)
  160. if r.status_code == 201:
  161. return "{} Workspace {} created!".format(r.status_code, workspace)
  162. else:
  163. return "{} Workspace {} already exists!".format(r.status_code, workspace)
  164. # raise GeoserverException(r.status_code, r.content)
  165. def delete_workspace(self, workspace: str):
  166. payload = {"recurse": "true"}
  167. url = "{}/rest/workspaces/{}".format(self.service_url, workspace)
  168. r = self._requests("delete", url, params=payload)
  169. if r.status_code == 200:
  170. return "Status code: {}, delete workspace".format(r.status_code)
  171. else:
  172. raise GeoserverException(r.status_code, r.content)
  173. def get_datastore(self, store_name: str, workspace: Optional[str] = None):
  174. if workspace is None:
  175. workspace = "default"
  176. url = "{}/rest/workspaces/{}/datastores/{}".format(
  177. self.service_url, workspace, store_name
  178. )
  179. r = self._requests("get", url)
  180. if r.status_code == 200:
  181. return r.json()
  182. else:
  183. raise GeoserverException(r.status_code, r.content)
  184. def get_datastores(self, workspace: Optional[str] = None):
  185. if workspace is None:
  186. workspace = "default"
  187. url = "{}/rest/workspaces/{}/datastores.json".format(
  188. self.service_url, workspace
  189. )
  190. r = self._requests("get", url)
  191. if r.status_code == 200:
  192. return r.json()
  193. else:
  194. raise GeoserverException(r.status_code, r.content)
  195. def get_coveragestore(
  196. self, coveragestore_name: str, workspace: Optional[str] = None
  197. ):
  198. payload = {"recurse": "true"}
  199. if workspace is None:
  200. workspace = "default"
  201. url = "{}/rest/workspaces/{}/coveragestores/{}.json".format(
  202. self.service_url, workspace, coveragestore_name
  203. )
  204. r = self._requests(method="get", url=url, params=payload)
  205. if r.status_code == 200:
  206. return r.json()
  207. else:
  208. raise GeoserverException(r.status_code, r.content)
  209. def get_coveragestores(self, workspace: str = None):
  210. if workspace is None:
  211. workspace = "default"
  212. url = "{}/rest/workspaces/{}/coveragestores".format(self.service_url, workspace)
  213. r = self._requests("get", url)
  214. if r.status_code == 200:
  215. return r.json()
  216. else:
  217. raise GeoserverException(r.status_code, r.content)
  218. def create_coveragestore(
  219. self,
  220. path,
  221. workspace: Optional[str] = None,
  222. layer_name: Optional[str] = None,
  223. file_type: str = "GeoTIFF",
  224. content_type: str = "image/tiff",
  225. ):
  226. if path is None:
  227. raise Exception("You must provide the full path to the raster")
  228. if workspace is None:
  229. workspace = "default"
  230. if layer_name is None:
  231. layer_name = os.path.basename(path)
  232. f = layer_name.split(".")
  233. if len(f) > 0:
  234. layer_name = f[0]
  235. file_type = file_type.lower()
  236. url = "{0}/rest/workspaces/{1}/coveragestores/{2}/file.{3}?coverageName={2}".format(
  237. self.service_url, workspace, layer_name, file_type
  238. )
  239. headers = {"content-type": content_type, "Accept": "application/json"}
  240. r = None
  241. with open(path, "rb") as f:
  242. r = self._requests(method="put", url=url, data=f, headers=headers)
  243. if r.status_code == 201:
  244. return r.json()
  245. else:
  246. raise GeoserverException(r.status_code, r.content)
  247. def publish_time_dimension_to_coveragestore(
  248. self,
  249. store_name: Optional[str] = None,
  250. workspace: Optional[str] = None,
  251. presentation: Optional[str] = "LIST",
  252. units: Optional[str] = "ISO8601",
  253. default_value: Optional[str] = "MINIMUM",
  254. content_type: str = "application/xml; charset=UTF-8",
  255. ):
  256. url = "{0}/rest/workspaces/{1}/coveragestores/{2}/coverages/{2}".format(
  257. self.service_url, workspace, store_name
  258. )
  259. headers = {"content-type": content_type}
  260. time_dimension_data = (
  261. "<coverage>"
  262. "<enabled>true</enabled>"
  263. "<metadata>"
  264. "<entry key='time'>"
  265. "<dimensionInfo>"
  266. "<enabled>true</enabled>"
  267. "<presentation>{}</presentation>"
  268. "<units>{}</units>"
  269. "<defaultValue>"
  270. "<strategy>{}</strategy>"
  271. "</defaultValue>"
  272. "</dimensionInfo>"
  273. "</entry>"
  274. "</metadata>"
  275. "</coverage>".format(presentation, units, default_value)
  276. )
  277. r = self._requests(
  278. method="put", url=url, data=time_dimension_data, headers=headers
  279. )
  280. if r.status_code in [200, 201]:
  281. return r.json()
  282. else:
  283. raise GeoserverException(r.status_code, r.content)
  284. def get_layer(self, layer_name: str, workspace: Optional[str] = None):
  285. url = "{}/rest/layers/{}".format(self.service_url, layer_name)
  286. if workspace is not None:
  287. url = "{}/rest/workspaces/{}/layers/{}".format(
  288. self.service_url, workspace, layer_name
  289. )
  290. r = self._requests("get", url)
  291. if r.status_code == 200:
  292. return r.json()
  293. else:
  294. raise GeoserverException(r.status_code, r.content)
  295. def get_layers(self, workspace: Optional[str] = None):
  296. url = "{}/rest/layers".format(self.service_url)
  297. if workspace is not None:
  298. url = "{}/rest/workspaces/{}/layers".format(self.service_url, workspace)
  299. r = self._requests("get", url)
  300. if r.status_code == 200:
  301. return r.json()
  302. else:
  303. raise GeoserverException(r.status_code, r.content)
  304. def delete_layer(self, layer_name: str, workspace: Optional[str] = None):
  305. payload = {"recurse": "true"}
  306. url = "{}/rest/workspaces/{}/layers/{}".format(
  307. self.service_url, workspace, layer_name
  308. )
  309. if workspace is None:
  310. url = "{}/rest/layers/{}".format(self.service_url, layer_name)
  311. r = self._requests(method="delete", url=url, params=payload)
  312. if r.status_code == 200:
  313. return "Status code: {}, delete layer".format(r.status_code)
  314. else:
  315. raise GeoserverException(r.status_code, r.content)
  316. def get_layergroups(self, workspace: Optional[str] = None):
  317. url = "{}/rest/layergroups".format(self.service_url)
  318. if workspace is not None:
  319. url = "{}/rest/workspaces/{}/layergroups".format(
  320. self.service_url, workspace
  321. )
  322. r = self._requests("get", url)
  323. if r.status_code == 200:
  324. return r.json()
  325. else:
  326. raise GeoserverException(r.status_code, r.content)
  327. def get_layergroup(self, layer_name: str, workspace: Optional[str] = None):
  328. url = "{}/rest/layergroups/{}".format(self.service_url, layer_name)
  329. if workspace is not None:
  330. url = "{}/rest/workspaces/{}/layergroups/{}".format(
  331. self.service_url, workspace, layer_name
  332. )
  333. r = self._requests("get", url)
  334. if r.status_code == 200:
  335. return r.json()
  336. else:
  337. return None
  338. #raise GeoserverException(r.status_code, r.content)
  339. def create_layergroup(
  340. self,
  341. name: str = "geoserver-rest-layergroup",
  342. mode: str = "single",
  343. title: str = "geoserver-rest layer group",
  344. abstract_text: str = "A new layergroup created with geoserver-rest python package",
  345. layers: List[str] = [],
  346. workspace: Optional[str] = None,
  347. formats: str = "html",
  348. metadata: List[dict] = [],
  349. keywords: List[str] = [],
  350. ) -> str:
  351. assert isinstance(name, str), "Name must be of type String:''"
  352. assert isinstance(mode, str), "Mode must be of type String:''"
  353. assert isinstance(title, str), "Title must be of type String:''"
  354. assert isinstance(abstract_text, str), "Abstract text must be of type String:''"
  355. assert isinstance(formats, str), "Format must be of type String:''"
  356. assert isinstance(
  357. metadata, list
  358. ), "Metadata must be of type List of dict:[{'about':'geoserver rest data metadata','content_url':'link to content url'}]"
  359. assert isinstance(
  360. keywords, list
  361. ), "Keywords must be of type List:['keyword1','keyword2'...]"
  362. assert isinstance(
  363. layers, list
  364. ), "Layers must be of type List:['layer1','layer2'...]"
  365. if workspace:
  366. assert isinstance(workspace, str), "Workspace must be of type String:''"
  367. # check if the workspace is valid in GeoServer
  368. if self.get_workspace(workspace) is None:
  369. raise Exception("Workspace is not valid in GeoServer Instance")
  370. supported_modes: Set = {
  371. "single",
  372. "opaque",
  373. "named",
  374. "container",
  375. "eo",
  376. }
  377. supported_formats: Set = {"html", "json", "xml"}
  378. if mode.lower() != "single" and mode.lower() not in supported_modes:
  379. raise Exception(
  380. f"Mode not supported. Acceptable modes are : {supported_modes}"
  381. )
  382. if formats.lower() != "html" and formats.lower() not in supported_formats:
  383. raise Exception(
  384. f"Format not supported. Acceptable formats are : {supported_formats}"
  385. )
  386. # check if it already exist in GeoServer
  387. try:
  388. existing_layergroup = self.get_layergroup(name, workspace=workspace)
  389. except GeoserverException:
  390. existing_layergroup = None
  391. if existing_layergroup is not None:
  392. raise Exception(f"Layergroup: {name} already exist in GeoServer instance")
  393. if len(layers) == 0:
  394. raise Exception("No layer provided!")
  395. else:
  396. for layer in layers:
  397. # check if it is valid in geoserver
  398. try:
  399. # Layer check
  400. self.get_layer(
  401. layer_name=layer,
  402. workspace=workspace if workspace is not None else None,
  403. )
  404. except GeoserverException:
  405. try:
  406. # Layer group check
  407. self.get_layergroup(
  408. layer_name=layer,
  409. workspace=workspace if workspace is not None else None,
  410. )
  411. except GeoserverException:
  412. raise Exception(
  413. f"Layer: {layer} is not a valid layer in the GeoServer instance"
  414. )
  415. skeleton = ""
  416. if workspace:
  417. skeleton += f"<workspace><name>{workspace}</name></workspace>"
  418. metadata_xml_list = []
  419. if len(metadata) >= 1:
  420. for meta in metadata:
  421. metadata_about = meta.get("about")
  422. metadata_content_url = meta.get("content_url")
  423. metadata_xml_list.append(
  424. f"""
  425. <metadataLink>
  426. <type>text/plain</type>
  427. <about>{metadata_about}</about>
  428. <metadataType>ISO19115:2003</metadataType>
  429. <content>{metadata_content_url}</content>
  430. </metadataLink>
  431. """
  432. )
  433. metadata_xml = f"<metadataLinks>{''.join(['{}'] * len(metadata_xml_list)).format(*metadata_xml_list)}</metadataLinks>"
  434. skeleton += metadata_xml
  435. layers_xml_list: List[str] = []
  436. for layer in layers:
  437. published_type = "layer"
  438. try:
  439. # Layer check
  440. self.get_layer(
  441. layer_name=layer,
  442. workspace=workspace if workspace is not None else None,
  443. )
  444. except GeoserverException: # It's a layer group
  445. published_type = "layerGroup"
  446. layers_xml_list.append(
  447. f"""<published type="{published_type}">
  448. <name>{layer}</name>
  449. <link>{self.service_url}/layers/{layer}.xml</link>
  450. </published>
  451. """
  452. )
  453. layers_xml: str = f"<publishables>{''.join(['{}'] * len(layers)).format(*layers_xml_list)}</publishables>"
  454. skeleton += layers_xml
  455. if len(keywords) >= 1:
  456. keyword_xml_list: List[str] = [
  457. f"<keyword>{keyword}</keyword>" for keyword in keywords
  458. ]
  459. keywords_xml: str = f"<keywords>{''.join(['{}'] * len(keywords)).format(*keyword_xml_list)}</keywords>"
  460. skeleton += keywords_xml
  461. data = f"""
  462. <layerGroup>
  463. <name>{name}</name>
  464. <mode>{mode}</mode>
  465. <title>{title}</title>
  466. <abstractTxt>{abstract_text}</abstractTxt>
  467. {skeleton}
  468. </layerGroup>
  469. """
  470. url = f"{self.service_url}/rest/layergroups/"
  471. r = self._requests(
  472. method="post", url=url, data=data, headers={"content-type": "text/xml"}
  473. )
  474. if r.status_code == 201:
  475. layergroup_url = f"{self.service_url}/rest/layergroups/{name}.{formats}"
  476. return f"layergroup created successfully! Layergroup link: {layergroup_url}"
  477. else:
  478. raise GeoserverException(r.status_code, r.content)
  479. def update_layergroup(
  480. self,
  481. layergroup_name,
  482. title: Optional[str] = None,
  483. abstract_text: Optional[str] = None,
  484. formats: str = "html",
  485. metadata: List[dict] = [],
  486. keywords: List[str] = [],
  487. ) -> str:
  488. if self.get_layergroup(layer_name=layergroup_name) is None:
  489. raise Exception(
  490. f"Layer group: {layergroup_name} is not a valid layer group in the Geoserver instance"
  491. )
  492. if title is not None:
  493. assert isinstance(title, str), "Title must be of type String:''"
  494. if abstract_text is not None:
  495. assert isinstance(
  496. abstract_text, str
  497. ), "Abstract text must be of type String:''"
  498. assert isinstance(formats, str), "Format must be of type String:''"
  499. assert isinstance(
  500. metadata, list
  501. ), "Metadata must be of type List of dict:[{'about':'geoserver rest data metadata','content_url':'lint to content url'}]"
  502. assert isinstance(
  503. keywords, list
  504. ), "Keywords must be of type List:['keyword1','keyword2'...]"
  505. supported_formats: Set = {"html", "json", "xml"}
  506. if formats.lower() != "html" and formats.lower() not in supported_formats:
  507. raise Exception(
  508. f"Format not supported. Acceptable formats are : {supported_formats}"
  509. )
  510. skeleton = ""
  511. if title:
  512. skeleton += f"<title>{title}</title>"
  513. if abstract_text:
  514. skeleton += f"<abstractTxt>{abstract_text}</abstractTxt>"
  515. metadata_xml_list = []
  516. if len(metadata) >= 1:
  517. for meta in metadata:
  518. metadata_about = meta.get("about")
  519. metadata_content_url = meta.get("content_url")
  520. metadata_xml_list.append(
  521. f"""
  522. <metadataLink>
  523. <type>text/plain</type>
  524. <about>{metadata_about}</about>
  525. <metadataType>ISO19115:2003</metadataType>
  526. <content>{metadata_content_url}</content>
  527. </metadataLink>
  528. """
  529. )
  530. metadata_xml = f"<metadataLinks>{''.join(['{}'] * len(metadata_xml_list)).format(*metadata_xml_list)}</metadataLinks>"
  531. skeleton += metadata_xml
  532. if len(keywords) >= 1:
  533. keyword_xml_list: List[str] = [
  534. f"<keyword>{keyword}</keyword>" for keyword in keywords
  535. ]
  536. keywords_xml: str = f"<keywords>{''.join(['{}'] * len(keyword_xml_list)).format(*keyword_xml_list)}</keywords>"
  537. skeleton += keywords_xml
  538. data = f"""
  539. <layerGroup>
  540. {skeleton}
  541. </layerGroup>
  542. """
  543. url = f"{self.service_url}/rest/layergroups/{layergroup_name}"
  544. r = self._requests(
  545. method="put",
  546. url=url,
  547. data=data,
  548. headers={"content-type": "text/xml", "accept": "application/xml"},
  549. )
  550. if r.status_code == 200:
  551. layergroup_url = (
  552. f"{self.service_url}/rest/layergroups/{layergroup_name}.{formats}"
  553. )
  554. return f"layergroup updated successfully! Layergroup link: {layergroup_url}"
  555. else:
  556. raise GeoserverException(r.status_code, r.content)
  557. def delete_layergroup(
  558. self, layergroup_name: str, workspace: Optional[str] = None
  559. ) -> str:
  560. # raises an exception in case the layer group doesn't exist
  561. self.get_layergroup(layer_name=layergroup_name, workspace=workspace)
  562. if workspace is None:
  563. url = f"{self.service_url}/rest/layergroups/{layergroup_name}"
  564. else:
  565. url = f"{self.service_url}/rest/workspaces/{workspace}/layergroups/{layergroup_name}"
  566. r = self._requests(url=url, method="delete")
  567. if r.status_code == 200:
  568. return "Layer group deleted successfully"
  569. else:
  570. return "Layer group deleted successfully"
  571. #raise GeoserverException(r.status_code, r.content)
  572. def add_layer_to_layergroup(
  573. self,
  574. layer_name: str,
  575. layer_workspace: str,
  576. layergroup_name: str,
  577. layergroup_workspace: str = None,
  578. ) -> None:
  579. layergroup_info = self.get_layergroup(
  580. layer_name=layergroup_name, workspace=layergroup_workspace
  581. )
  582. layer_info = self.get_layer(layer_name=layer_name, workspace=layer_workspace)
  583. # build list of existing publishables & styles
  584. publishables = layergroup_info["layerGroup"]["publishables"]["published"]
  585. if not isinstance(publishables, list): # only 1 layer up to now
  586. publishables = [publishables]
  587. styles = layergroup_info["layerGroup"]["styles"]["style"]
  588. if not isinstance(styles, list): # only 1 layer up to now
  589. styles = [styles]
  590. # add publishable & style for the new layer
  591. new_pub = {
  592. "name": f"{layer_workspace}:{layer_name}",
  593. "href": f"{self.service_url}/rest/workspaces/{layer_workspace}/layers/{layer_name}.json",
  594. }
  595. publishables.append(new_pub)
  596. new_style = layer_info["layer"]["defaultStyle"]
  597. styles.append(new_style)
  598. data = self._layergroup_definition_from_layers_and_styles(
  599. publishables=publishables, styles=styles
  600. )
  601. if layergroup_workspace is None:
  602. url = f"{self.service_url}/rest/layergroups/{layergroup_name}"
  603. else:
  604. url = f"{self.service_url}/rest/workspaces/{layergroup_workspace}/layergroups/{layergroup_name}"
  605. r = self._requests(
  606. method="put",
  607. url=url,
  608. data=data,
  609. headers={"content-type": "text/xml", "accept": "application/xml"},
  610. )
  611. if r.status_code == 200:
  612. return
  613. else:
  614. raise GeoserverException(r.status_code, r.content)
  615. def remove_layer_from_layergroup(
  616. self,
  617. layer_name: str,
  618. layer_workspace: str,
  619. layergroup_name: str,
  620. layergroup_workspace: str = None,
  621. ) -> None:
  622. layergroup_info = self.get_layergroup(
  623. layer_name=layergroup_name, workspace=layergroup_workspace
  624. )
  625. # build list of existing publishables & styles
  626. publishables = layergroup_info["layerGroup"]["publishables"]["published"]
  627. if not isinstance(publishables, list): # only 1 layer up to now
  628. publishables = [publishables]
  629. styles = layergroup_info["layerGroup"]["styles"]["style"]
  630. if not isinstance(styles, list): # only 1 layer up to now
  631. styles = [styles]
  632. layer_to_remove = f"{layer_workspace}:{layer_name}"
  633. revised_set_of_publishables_and_styles = [
  634. (pub, style)
  635. for (pub, style) in zip(
  636. layergroup_info["layerGroup"]["publishables"]["published"],
  637. layergroup_info["layerGroup"]["styles"]["style"],
  638. )
  639. if pub["name"] != layer_to_remove
  640. ]
  641. revised_set_of_publishables = list(
  642. map(list, zip(*revised_set_of_publishables_and_styles))
  643. )[0]
  644. revised_set_of_styles = list(
  645. map(list, zip(*revised_set_of_publishables_and_styles))
  646. )[1]
  647. xml_payload = self._layergroup_definition_from_layers_and_styles(
  648. publishables=revised_set_of_publishables, styles=revised_set_of_styles
  649. )
  650. if layergroup_workspace is None:
  651. url = f"{self.service_url}/rest/layergroups/{layergroup_name}"
  652. else:
  653. url = f"{self.service_url}/rest/workspaces/{layergroup_workspace}/layergroups/{layergroup_name}"
  654. r = self._requests(
  655. method="put",
  656. url=url,
  657. data=xml_payload,
  658. headers={"content-type": "text/xml", "accept": "application/xml"},
  659. )
  660. if r.status_code == 200:
  661. return
  662. else:
  663. raise GeoserverException(r.status_code, r.content)
  664. def _layergroup_definition_from_layers_and_styles(
  665. self, publishables: list, styles: list
  666. ) -> str:
  667. # the get_layergroup method may return an empty string for style;
  668. # so we get the default styles for each layer with no style information in the layergroup
  669. if len(styles) == 1:
  670. index = [0]
  671. else:
  672. index = range(len(styles))
  673. for ix, this_style, this_layer in zip(index, styles, publishables):
  674. if this_style == "":
  675. this_layer_info = self.get_layer(
  676. layer_name=this_layer["name"].split(":")[1],
  677. workspace=this_layer["name"].split(":")[0],
  678. )
  679. styles[ix] = {
  680. "name": this_layer_info["layer"]["defaultStyle"]["name"],
  681. "href": this_layer_info["layer"]["defaultStyle"]["href"],
  682. }
  683. # build xml structure
  684. layer_skeleton = ""
  685. style_skeleton = ""
  686. for publishable in publishables:
  687. layer_str = f"""
  688. <published type="layer">
  689. <name>{publishable['name']}</name>
  690. <link>{publishable['href']}</link>
  691. </published>
  692. """
  693. layer_skeleton += layer_str
  694. for style in styles:
  695. style_str = f"""
  696. <style>
  697. <name>{style['name']}</name>
  698. <link>{style['href']}</link>
  699. </style>
  700. """
  701. style_skeleton += style_str
  702. data = f"""
  703. <layerGroup>
  704. <publishables>
  705. {layer_skeleton}
  706. </publishables>
  707. <styles>
  708. {style_skeleton}
  709. </styles>
  710. </layerGroup>
  711. """
  712. return data
  713. def get_style(self, style_name, workspace: Optional[str] = None):
  714. url = "{}/rest/styles/{}.json".format(self.service_url, style_name)
  715. if workspace is not None:
  716. url = "{}/rest/workspaces/{}/styles/{}.json".format(
  717. self.service_url, workspace, style_name
  718. )
  719. r = self._requests("get", url)
  720. if r.status_code == 200:
  721. return r.json()
  722. else:
  723. raise GeoserverException(r.status_code, r.content)
  724. def get_styles(self, workspace: Optional[str] = None):
  725. url = "{}/rest/styles.json".format(self.service_url)
  726. if workspace is not None:
  727. url = "{}/rest/workspaces/{}/styles.json".format(
  728. self.service_url, workspace
  729. )
  730. r = self._requests("get", url)
  731. if r.status_code == 200:
  732. return r.json()
  733. else:
  734. raise GeoserverException(r.status_code, r.content)
  735. def upload_style(
  736. self,
  737. path: str,
  738. name: Optional[str] = None,
  739. workspace: Optional[str] = None,
  740. sld_version: str = "1.1.0",
  741. ):
  742. if name is None:
  743. name = os.path.basename(path)
  744. f = name.split(".")
  745. if len(f) > 0:
  746. name = f[0]
  747. if is_valid_xml(path):
  748. xml = path
  749. elif Path(path).exists():
  750. with open(path, "r", encoding='utf-8') as f:
  751. xml = f.read()
  752. xml = xml.encode("gbk")
  753. else:
  754. raise ValueError("`path` must be either a path to a style file, or a valid XML string.")
  755. headers = {"content-type": "text/xml"}
  756. url = "{}/rest/workspaces/{}/styles".format(self.service_url, workspace)
  757. sld_content_type = "application/vnd.ogc.sld+xml"
  758. if sld_version == "1.1.0" or sld_version == "1.1":
  759. sld_content_type = "application/vnd.ogc.se+xml"
  760. header_sld = {"content-type": sld_content_type}
  761. if workspace is None:
  762. url = "{}/rest/styles".format(self.service_url)
  763. style_xml = "<style><name>{}</name><filename>{}</filename></style>".format(
  764. name, name + ".sld"
  765. )
  766. r = self._requests(method="post", url=url, data=style_xml, headers=headers)
  767. if r.status_code == 201:
  768. r_sld = self._requests(method="put", url=url + "/" + name, data=xml, headers=header_sld)
  769. if r_sld.status_code == 200:
  770. return r_sld.status_code
  771. else:
  772. return r_sld.status_code
  773. # raise GeoserverException(r_sld.status_code, r_sld.content)
  774. else:
  775. return r.status_code
  776. # raise GeoserverException(r.status_code, r.content)
  777. def create_coveragestyle(
  778. self,
  779. raster_path: str,
  780. style_name: Optional[str] = None,
  781. workspace: str = None,
  782. color_ramp: str = "RdYlGn_r",
  783. cmap_type: str = "ramp",
  784. number_of_classes: int = 5,
  785. opacity: float = 1,
  786. ):
  787. raster = raster_value(raster_path)
  788. min_value = raster["min"]
  789. max_value = raster["max"]
  790. if style_name is None:
  791. style_name = raster["file_name"]
  792. coverage_style_xml(
  793. color_ramp,
  794. style_name,
  795. cmap_type,
  796. min_value,
  797. max_value,
  798. number_of_classes,
  799. opacity,
  800. )
  801. style_xml = "<style><name>{}</name><filename>{}</filename></style>".format(
  802. style_name, style_name + ".sld"
  803. )
  804. if style_name is None:
  805. style_name = os.path.basename(raster_path)
  806. f = style_name.split(".")
  807. if len(f) > 0:
  808. style_name = f[0]
  809. headers = {"content-type": "text/xml"}
  810. url = "{}/rest/workspaces/{}/styles".format(self.service_url, workspace)
  811. sld_content_type = "application/vnd.ogc.sld+xml"
  812. header_sld = {"content-type": sld_content_type}
  813. if workspace is None:
  814. url = "{}/rest/styles".format(self.service_url)
  815. r = self._requests(
  816. "post",
  817. url,
  818. data=style_xml,
  819. headers=headers,
  820. )
  821. if r.status_code == 201:
  822. with open("style.sld", "rb") as f:
  823. r_sld = self._requests(method="put", url=url + "/" + style_name, data=f.read(), headers=header_sld)
  824. os.remove("style.sld")
  825. if r_sld.status_code == 200:
  826. return r_sld.status_code
  827. else:
  828. raise GeoserverException(r_sld.status_code, r_sld.content)
  829. else:
  830. raise GeoserverException(r.status_code, r.content)
  831. def create_catagorized_featurestyle(
  832. self,
  833. style_name: str,
  834. column_name: str,
  835. column_distinct_values,
  836. workspace: str = None,
  837. color_ramp: str = "tab20",
  838. geom_type: str = "polygon",
  839. ):
  840. catagorize_xml(column_name, column_distinct_values, color_ramp, geom_type)
  841. style_xml = "<style><name>{}</name><filename>{}</filename></style>".format(
  842. style_name, style_name + ".sld"
  843. )
  844. headers = {"content-type": "text/xml"}
  845. url = "{}/rest/workspaces/{}/styles".format(self.service_url, workspace)
  846. sld_content_type = "application/vnd.ogc.sld+xml"
  847. header_sld = {"content-type": sld_content_type}
  848. if workspace is None:
  849. url = "{}/rest/styles".format(self.service_url)
  850. r = self._requests(
  851. "post",
  852. url,
  853. data=style_xml,
  854. headers=headers,
  855. )
  856. if r.status_code == 201:
  857. with open("style.sld", "rb") as f:
  858. r_sld = self._requests(
  859. "put",
  860. url + "/" + style_name,
  861. data=f.read(),
  862. headers=header_sld,
  863. )
  864. os.remove("style.sld")
  865. if r_sld.status_code == 200:
  866. return r_sld.status_code
  867. else:
  868. raise GeoserverException(r_sld.status_code, r_sld.content)
  869. else:
  870. raise GeoserverException(r.status_code, r.content)
  871. def create_outline_featurestyle(
  872. self,
  873. style_name: str,
  874. color: str = "#3579b1",
  875. width: str = "2",
  876. geom_type: str = "polygon",
  877. workspace: Optional[str] = None,
  878. ):
  879. outline_only_xml(color, width, geom_type)
  880. style_xml = "<style><name>{}</name><filename>{}</filename></style>".format(
  881. style_name, style_name + ".sld"
  882. )
  883. headers = {"content-type": "text/xml"}
  884. url = "{}/rest/workspaces/{}/styles".format(self.service_url, workspace)
  885. sld_content_type = "application/vnd.ogc.sld+xml"
  886. header_sld = {"content-type": sld_content_type}
  887. if workspace is None:
  888. url = "{}/rest/styles".format(self.service_url)
  889. r = self._requests(
  890. "post",
  891. url,
  892. data=style_xml,
  893. headers=headers,
  894. )
  895. if r.status_code == 201:
  896. with open("style.sld", "rb") as f:
  897. r_sld = self._requests(
  898. "put",
  899. url + "/" + style_name,
  900. data=f.read(),
  901. headers=header_sld,
  902. )
  903. os.remove("style.sld")
  904. if r_sld.status_code == 200:
  905. return r_sld.status_code
  906. else:
  907. raise GeoserverException(r_sld.status_code, r_sld.content)
  908. else:
  909. raise GeoserverException(r.status_code, r.content)
  910. def create_classified_featurestyle(
  911. self,
  912. style_name: str,
  913. column_name: str,
  914. column_distinct_values,
  915. workspace: Optional[str] = None,
  916. color_ramp: str = "tab20",
  917. geom_type: str = "polygon",
  918. # outline_color: str = "#3579b1",
  919. ):
  920. classified_xml(
  921. style_name,
  922. column_name,
  923. column_distinct_values,
  924. color_ramp,
  925. geom_type,
  926. )
  927. style_xml = "<style><name>{}</name><filename>{}</filename></style>".format(
  928. column_name, column_name + ".sld"
  929. )
  930. headers = {"content-type": "text/xml"}
  931. url = "{}/rest/workspaces/{}/styles".format(self.service_url, workspace)
  932. sld_content_type = "application/vnd.ogc.sld+xml"
  933. header_sld = {"content-type": sld_content_type}
  934. if workspace is None:
  935. url = "{}/rest/styles".format(self.service_url)
  936. r = self._requests(
  937. "post",
  938. url,
  939. data=style_xml,
  940. headers=headers,
  941. )
  942. if r.status_code == 201:
  943. with open("style.sld", "rb") as f:
  944. r_sld = self._requests(
  945. "put",
  946. url + "/" + style_name,
  947. data=f.read(),
  948. headers=header_sld,
  949. )
  950. os.remove("style.sld")
  951. if r_sld.status_code == 200:
  952. return r_sld.status_code
  953. else:
  954. raise GeoserverException(r_sld.status_code, r_sld.content)
  955. else:
  956. raise GeoserverException(r.status_code, r.content)
  957. def publish_style(
  958. self,
  959. layer_name: str,
  960. style_name: str,
  961. workspace: str,
  962. ):
  963. headers = {"content-type": "text/xml"}
  964. url = "{}/rest/layers/{}:{}".format(self.service_url, workspace, layer_name)
  965. style_xml = (
  966. "<layer><defaultStyle><name>{}</name></defaultStyle></layer>".format(
  967. style_name
  968. )
  969. )
  970. r = self._requests(
  971. "put",
  972. url,
  973. data=style_xml,
  974. headers=headers,
  975. )
  976. if r.status_code == 200:
  977. return r.status_code
  978. else:
  979. raise GeoserverException(r.status_code, r.content)
  980. def delete_style(self, style_name: str, workspace: Optional[str] = None):
  981. payload = {"recurse": "true"}
  982. url = "{}/rest/workspaces/{}/styles/{}".format(
  983. self.service_url, workspace, style_name
  984. )
  985. if workspace is None:
  986. url = "{}/rest/styles/{}".format(self.service_url, style_name)
  987. r = self._requests("delete", url, params=payload)
  988. if r.status_code == 200:
  989. return "Status code: {}, delete style".format(r.status_code)
  990. else:
  991. return "Status code: {}, delete style".format(r.status_code)
  992. # raise GeoserverException(r.status_code, r.content)
  993. def create_featurestore(
  994. self,
  995. store_name: str,
  996. workspace: Optional[str] = None,
  997. db: str = "postgres",
  998. host: str = "localhost",
  999. port: int = 5432,
  1000. schema: str = "public",
  1001. pg_user: str = "postgres",
  1002. pg_password: str = "admin",
  1003. overwrite: bool = False,
  1004. expose_primary_keys: str = "false",
  1005. description: Optional[str] = None,
  1006. evictor_run_periodicity: Optional[int] = 300,
  1007. max_open_prepared_statements: Optional[int] = 50,
  1008. encode_functions: Optional[str] = "false",
  1009. primary_key_metadata_table: Optional[str] = None,
  1010. batch_insert_size: Optional[int] = 1,
  1011. preparedstatements: Optional[str] = "false",
  1012. loose_bbox: Optional[str] = "true",
  1013. estimated_extends: Optional[str] = "true",
  1014. fetch_size: Optional[int] = 1000,
  1015. validate_connections: Optional[str] = "true",
  1016. support_on_the_fly_geometry_simplification: Optional[str] = "true",
  1017. connection_timeout: Optional[int] = 20,
  1018. create_database: Optional[str] = "false",
  1019. min_connections: Optional[int] = 1,
  1020. max_connections: Optional[int] = 10,
  1021. evictor_tests_per_run: Optional[int] = 3,
  1022. test_while_idle: Optional[str] = "true",
  1023. max_connection_idle_time: Optional[int] = 300,
  1024. ):
  1025. url = "{}/rest/workspaces/{}/datastores".format(self.service_url, workspace)
  1026. headers = {"content-type": "text/xml"}
  1027. database_connection = """
  1028. <dataStore>
  1029. <name>{}</name>
  1030. <description>{}</description>
  1031. <connectionParameters>
  1032. <entry key="Expose primary keys">{}</entry>
  1033. <entry key="host">{}</entry>
  1034. <entry key="port">{}</entry>
  1035. <entry key="user">{}</entry>
  1036. <entry key="passwd">{}</entry>
  1037. <entry key="dbtype">postgis</entry>
  1038. <entry key="schema">{}</entry>
  1039. <entry key="database">{}</entry>
  1040. <entry key="Evictor run periodicity">{}</entry>
  1041. <entry key="Max open prepared statements">{}</entry>
  1042. <entry key="encode functions">{}</entry>
  1043. <entry key="Primary key metadata table">{}</entry>
  1044. <entry key="Batch insert size">{}</entry>
  1045. <entry key="preparedStatements">{}</entry>
  1046. <entry key="Estimated extends">{}</entry>
  1047. <entry key="fetch size">{}</entry>
  1048. <entry key="validate connections">{}</entry>
  1049. <entry key="Support on the fly geometry simplification">{}</entry>
  1050. <entry key="Connection timeout">{}</entry>
  1051. <entry key="create database">{}</entry>
  1052. <entry key="min connections">{}</entry>
  1053. <entry key="max connections">{}</entry>
  1054. <entry key="Evictor tests per run">{}</entry>
  1055. <entry key="Test while idle">{}</entry>
  1056. <entry key="Max connection idle time">{}</entry>
  1057. <entry key="Loose bbox">{}</entry>
  1058. </connectionParameters>
  1059. </dataStore>
  1060. """.format(
  1061. store_name,
  1062. description,
  1063. expose_primary_keys,
  1064. host,
  1065. port,
  1066. pg_user,
  1067. pg_password,
  1068. schema,
  1069. db,
  1070. evictor_run_periodicity,
  1071. max_open_prepared_statements,
  1072. encode_functions,
  1073. primary_key_metadata_table,
  1074. batch_insert_size,
  1075. preparedstatements,
  1076. estimated_extends,
  1077. fetch_size,
  1078. validate_connections,
  1079. support_on_the_fly_geometry_simplification,
  1080. connection_timeout,
  1081. create_database,
  1082. min_connections,
  1083. max_connections,
  1084. evictor_tests_per_run,
  1085. test_while_idle,
  1086. max_connection_idle_time,
  1087. loose_bbox,
  1088. )
  1089. if overwrite:
  1090. url = "{}/rest/workspaces/{}/datastores/{}".format(
  1091. self.service_url, workspace, store_name
  1092. )
  1093. r = self._requests(
  1094. "put",
  1095. url,
  1096. data=database_connection,
  1097. headers=headers,
  1098. )
  1099. else:
  1100. r = self._requests(
  1101. "post",
  1102. url,
  1103. data=database_connection,
  1104. headers=headers,
  1105. )
  1106. if r.status_code in [200, 201]:
  1107. return "Featurestore created/updated successfully"
  1108. else:
  1109. raise GeoserverException(r.status_code, r.content)
  1110. def create_datastore(
  1111. self,
  1112. name: str,
  1113. path: str,
  1114. workspace: Optional[str] = None,
  1115. overwrite: bool = False,
  1116. ):
  1117. if workspace is None:
  1118. workspace = "default"
  1119. if path is None:
  1120. raise Exception("You must provide a full path to the data")
  1121. data_url = "<url>file:{}</url>".format(path)
  1122. if "http://" in path:
  1123. data_url = "<GET_CAPABILITIES_URL>{}</GET_CAPABILITIES_URL>".format(path)
  1124. data = "<dataStore><name>{}</name><connectionParameters>{}</connectionParameters></dataStore>".format(
  1125. name, data_url
  1126. )
  1127. headers = {"content-type": "text/xml"}
  1128. if overwrite:
  1129. url = "{}/rest/workspaces/{}/datastores/{}".format(
  1130. self.service_url, workspace, name
  1131. )
  1132. r = self._requests("put", url, data=data, headers=headers)
  1133. else:
  1134. url = "{}/rest/workspaces/{}/datastores".format(self.service_url, workspace)
  1135. r = self._requests(method="post", url=url, data=data, headers=headers)
  1136. if r.status_code in [200, 201]:
  1137. return "Data store created/updated successfully"
  1138. else:
  1139. raise GeoserverException(r.status_code, r.content)
  1140. def create_shp_datastore(
  1141. self,
  1142. path: str,
  1143. store_name: Optional[str] = None,
  1144. workspace: Optional[str] = None,
  1145. file_extension: str = "shp",
  1146. ):
  1147. if path is None:
  1148. raise Exception("You must provide a full path to shapefile")
  1149. if workspace is None:
  1150. workspace = "default"
  1151. if store_name is None:
  1152. store_name = os.path.basename(path)
  1153. f = store_name.split(".")
  1154. if len(f) > 0:
  1155. store_name = f[0]
  1156. headers = {
  1157. "Content-type": "application/zip",
  1158. "Accept": "application/xml",
  1159. }
  1160. if isinstance(path, dict):
  1161. path = prepare_zip_file(store_name, path)
  1162. url = "{0}/rest/workspaces/{1}/datastores/{2}/file.{3}?filename={2}&update=overwrite".format(
  1163. self.service_url, workspace, store_name, file_extension
  1164. )
  1165. with open(path, "rb") as f:
  1166. r = self._requests("put", url, data=f.read(), headers=headers)
  1167. if r.status_code in [200, 201, 202]:
  1168. return "The shapefile datastore created successfully!"
  1169. else:
  1170. raise GeoserverException(r.status_code, r.content)
  1171. def create_gpkg_datastore(
  1172. self,
  1173. path: str,
  1174. store_name: Optional[str] = None,
  1175. workspace: Optional[str] = None,
  1176. file_extension: str = "gpkg",
  1177. ):
  1178. if path is None:
  1179. raise Exception("You must provide a full path to shapefile")
  1180. if workspace is None:
  1181. workspace = "default"
  1182. if store_name is None:
  1183. store_name = os.path.basename(path)
  1184. f = store_name.split(".")
  1185. if len(f) > 0:
  1186. store_name = f[0]
  1187. headers = {
  1188. "Content-type": "application/x-sqlite3",
  1189. "Accept": "application/json",
  1190. }
  1191. url = "{0}/rest/workspaces/{1}/datastores/{2}/file.{3}?filename={2}".format(
  1192. self.service_url, workspace, store_name, file_extension
  1193. )
  1194. with open(path, "rb") as f:
  1195. r = self._requests("put", url, data=f.read(), headers=headers)
  1196. if r.status_code in [200, 201, 202]:
  1197. return "The geopackage datastore created successfully!"
  1198. else:
  1199. raise GeoserverException(r.status_code, r.content)
  1200. def publish_featurestore(
  1201. self,
  1202. store_name: str,
  1203. pg_table: str,
  1204. workspace: Optional[str] = None,
  1205. title: Optional[str] = None,
  1206. advertised: Optional[bool] = True,
  1207. abstract: Optional[str] = None,
  1208. keywords: Optional[List[str]] = None,
  1209. cqlfilter: Optional[str] = None
  1210. ) -> int:
  1211. if workspace is None:
  1212. workspace = "default"
  1213. if title is None:
  1214. title = pg_table
  1215. url = "{}/rest/workspaces/{}/datastores/{}/featuretypes/".format(
  1216. self.service_url, workspace, store_name
  1217. )
  1218. abstract_xml = f"<abstract>{abstract}</abstract>" if abstract else ""
  1219. keywords_xml = ""
  1220. if keywords:
  1221. keywords_xml = "<keywords>"
  1222. for keyword in keywords:
  1223. keywords_xml += f"<string>{keyword}</string>"
  1224. keywords_xml += "</keywords>"
  1225. cqlfilter_xml = f"<cqlFilter>{cqlfilter}</cqlFilter>" if cqlfilter else ""
  1226. layer_xml = f"""<featureType>
  1227. <name>{pg_table}</name>
  1228. <title>{title}</title>
  1229. <advertised>{advertised}</advertised>
  1230. {abstract_xml}
  1231. {keywords_xml}
  1232. {cqlfilter_xml}
  1233. </featureType>"""
  1234. headers = {"content-type": "text/xml;charset=utf-8"}
  1235. print(url)
  1236. print(layer_xml)
  1237. r = self._requests("post", url, data=layer_xml.encode("utf-8"), headers=headers)
  1238. if r.status_code == 201 or r.status_code == 200:
  1239. return r.status_code
  1240. else:
  1241. raise GeoserverException(r.status_code, r.content)
  1242. def edit_featuretype(
  1243. self,
  1244. store_name: str,
  1245. workspace: Optional[str],
  1246. pg_table: str,
  1247. name: str,
  1248. title: str,
  1249. abstract: Optional[str] = None,
  1250. keywords: Optional[List[str]] = None,
  1251. recalculate: Optional[str] = None
  1252. ) -> int:
  1253. if workspace is None:
  1254. workspace = "default"
  1255. recalculate_param = f"?recalculate={recalculate}" if recalculate else ""
  1256. url = "{}/rest/workspaces/{}/datastores/{}/featuretypes/{}.xml{}".format(
  1257. self.service_url, workspace, store_name, pg_table, recalculate_param
  1258. )
  1259. # Create XML for abstract and keywords
  1260. abstract_xml = f"<abstract>{abstract}</abstract>" if abstract else ""
  1261. keywords_xml = ""
  1262. if keywords:
  1263. keywords_xml = "<keywords>"
  1264. for keyword in keywords:
  1265. keywords_xml += f"<string>{keyword}</string>"
  1266. keywords_xml += "</keywords>"
  1267. layer_xml = f"""<featureType>
  1268. <name>{name}</name>
  1269. <title>{title}</title>
  1270. {abstract_xml}{keywords_xml}
  1271. </featureType>"""
  1272. headers = {"content-type": "text/xml"}
  1273. r = self._requests("put", url, data=layer_xml, headers=headers)
  1274. if r.status_code == 200:
  1275. return r.status_code
  1276. else:
  1277. raise GeoserverException(r.status_code, r.content)
  1278. def publish_featurestore_sqlview(
  1279. self,
  1280. name: str,
  1281. store_name: str,
  1282. sql: str,
  1283. parameters: Optional[Iterable[Dict]] = None,
  1284. key_column: Optional[str] = None,
  1285. geom_name: str = "geom",
  1286. geom_type: str = "Geometry",
  1287. srid: Optional[int] = 4326,
  1288. workspace: Optional[str] = None,
  1289. ) -> int:
  1290. if workspace is None:
  1291. workspace = "default"
  1292. # issue #87
  1293. if key_column is not None:
  1294. key_column_xml = """<keyColumn>{}</keyColumn>""".format(key_column)
  1295. else:
  1296. key_column_xml = """"""
  1297. parameters_xml = ""
  1298. if parameters is not None:
  1299. for parameter in parameters:
  1300. # non-string parameters MUST have a default value supplied
  1301. if not is_surrounded_by_quotes(sql, parameter["name"]) and not "defaultValue" in parameter:
  1302. raise ValueError(f"Parameter `{parameter['name']}` appears to be a non-string in the supplied query"
  1303. ", but does not have a default value specified. You must supply a default value "
  1304. "for non-string parameters using the `defaultValue` key.")
  1305. param_name = parameter.get("name", "")
  1306. default_value = parameter.get("defaultValue", "")
  1307. regexp_validator = parameter.get("regexpValidator", r"^[\w\d\s]+$")
  1308. parameters_xml += (f"""
  1309. <parameter>
  1310. <name>{param_name}</name>
  1311. <defaultValue>{default_value}</defaultValue>
  1312. <regexpValidator>{regexp_validator}</regexpValidator>
  1313. </parameter>\n
  1314. """.strip())
  1315. layer_xml = """<featureType>
  1316. <name>{0}</name>
  1317. <enabled>true</enabled>
  1318. <namespace>
  1319. <name>{4}</name>
  1320. </namespace>
  1321. <title>{0}</title>
  1322. <srs>EPSG:{5}</srs>
  1323. <metadata>
  1324. <entry key="JDBC_VIRTUAL_TABLE">
  1325. <virtualTable>
  1326. <name>{0}</name>
  1327. <sql>{1}</sql>
  1328. <escapeSql>true</escapeSql>
  1329. <geometry>
  1330. <name>{2}</name>
  1331. <type>{3}</type>
  1332. <srid>{5}</srid>
  1333. </geometry>{6}
  1334. {7}
  1335. </virtualTable>
  1336. </entry>
  1337. </metadata>
  1338. </featureType>""".format(
  1339. name, sql, geom_name, geom_type, workspace, srid, key_column_xml, parameters_xml
  1340. )
  1341. # rest API url
  1342. url = "{}/rest/workspaces/{}/datastores/{}/featuretypes".format(
  1343. self.service_url, workspace, store_name
  1344. )
  1345. # headers
  1346. headers = {"content-type": "text/xml"}
  1347. # request
  1348. r = self._requests("post", url, data=layer_xml, headers=headers)
  1349. if r.status_code == 201:
  1350. return r.status_code
  1351. else:
  1352. raise GeoserverException(r.status_code, r.content)
  1353. def get_featuretypes(self, workspace: str = None, store_name: str = None) -> List[str]:
  1354. url = "{}/rest/workspaces/{}/datastores/{}/featuretypes.json".format(
  1355. self.service_url, workspace, store_name
  1356. )
  1357. r = self._requests("get", url)
  1358. if r.status_code == 200:
  1359. r_dict = r.json()
  1360. features = [i["name"] for i in r_dict["featureTypes"]["featureType"]]
  1361. return features
  1362. else:
  1363. raise GeoserverException(r.status_code, r.content)
  1364. def get_feature_attribute(
  1365. self, feature_type_name: str, workspace: str, store_name: str
  1366. ) -> List[str]:
  1367. url = "{}/rest/workspaces/{}/datastores/{}/featuretypes/{}.json".format(
  1368. self.service_url, workspace, store_name, feature_type_name
  1369. )
  1370. r = self._requests("get", url)
  1371. if r.status_code == 200:
  1372. r_dict = r.json()
  1373. attribute = [
  1374. i["name"] for i in r_dict["featureType"]["attributes"]["attribute"]
  1375. ]
  1376. return attribute
  1377. else:
  1378. raise GeoserverException(r.status_code, r.content)
  1379. def get_featurestore(self, store_name: str, workspace: str) -> dict:
  1380. url = "{}/rest/workspaces/{}/datastores/{}".format(
  1381. self.service_url, workspace, store_name
  1382. )
  1383. r = self._requests("get", url)
  1384. if r.status_code == 200:
  1385. r_dict = r.json()
  1386. return r_dict["dataStore"]
  1387. else:
  1388. raise GeoserverException(r.status_code, r.content)
  1389. def delete_featurestore(
  1390. self, featurestore_name: str, workspace: Optional[str] = None
  1391. ) -> str:
  1392. payload = {"recurse": "true"}
  1393. url = "{}/rest/workspaces/{}/datastores/{}".format(
  1394. self.service_url, workspace, featurestore_name
  1395. )
  1396. if workspace is None:
  1397. url = "{}/datastores/{}".format(self.service_url, featurestore_name)
  1398. r = self._requests("delete", url, params=payload)
  1399. if r.status_code == 200:
  1400. return "Status code: {}, delete featurestore".format(r.status_code)
  1401. else:
  1402. return "Status code: {}, delete featurestore".format(r.status_code)
  1403. # raise GeoserverException(r.status_code, r.content)
  1404. def delete_coveragestore(
  1405. self, coveragestore_name: str, workspace: Optional[str] = None
  1406. ) -> str:
  1407. payload = {"recurse": "true"}
  1408. url = "{}/rest/workspaces/{}/coveragestores/{}".format(
  1409. self.service_url, workspace, coveragestore_name
  1410. )
  1411. if workspace is None:
  1412. url = "{}/rest/coveragestores/{}".format(
  1413. self.service_url, coveragestore_name
  1414. )
  1415. r = self._requests("delete", url, params=payload)
  1416. if r.status_code == 200:
  1417. return "Coverage store deleted successfully"
  1418. else:
  1419. raise GeoserverException(r.status_code, r.content)
  1420. def get_all_users(self, service=None) -> dict:
  1421. url = "{}/rest/security/usergroup/".format(self.service_url)
  1422. if service is None:
  1423. url += "users/"
  1424. else:
  1425. url += "service/{}/users/".format(service)
  1426. headers = {"accept": "application/xml"}
  1427. r = self._requests("get", url, headers=headers)
  1428. if r.status_code == 200:
  1429. return parse(r.content)
  1430. else:
  1431. raise GeoserverException(r.status_code, r.content)
  1432. def create_user(
  1433. self, username: str, password: str, enabled: bool = True, service=None
  1434. ) -> str:
  1435. url = "{}/rest/security/usergroup/".format(self.service_url)
  1436. if service is None:
  1437. url += "users/"
  1438. else:
  1439. url += "service/{}/users/".format(service)
  1440. data = "<user><userName>{}</userName><password>{}</password><enabled>{}</enabled></user>".format(
  1441. username, password, str(enabled).lower()
  1442. )
  1443. headers = {"content-type": "text/xml", "accept": "application/json"}
  1444. r = self._requests("post", url, data=data, headers=headers)
  1445. if r.status_code == 201:
  1446. return "User created successfully"
  1447. else:
  1448. raise GeoserverException(r.status_code, r.content)
  1449. def modify_user(
  1450. self, username: str, new_name=None, new_password=None, enable=None, service=None
  1451. ) -> str:
  1452. url = "{}/rest/security/usergroup/".format(self.service_url)
  1453. if service is None:
  1454. url += "user/{}".format(username)
  1455. else:
  1456. url += "service/{}/user/{}".format(service, username)
  1457. modifications = dict()
  1458. if new_name is not None:
  1459. modifications["userName"] = new_name
  1460. if new_password is not None:
  1461. modifications["password"] = new_password
  1462. if enable is not None:
  1463. modifications["enabled"] = enable
  1464. data = unparse({"user": modifications})
  1465. print(url, data)
  1466. headers = {"content-type": "text/xml", "accept": "application/json"}
  1467. r = self._requests("post", url, data=data, headers=headers)
  1468. if r.status_code == 200:
  1469. return "User modified successfully"
  1470. else:
  1471. raise GeoserverException(r.status_code, r.content)
  1472. def delete_user(self, username: str, service=None) -> str:
  1473. url = "{}/rest/security/usergroup/".format(self.service_url)
  1474. if service is None:
  1475. url += "user/{}".format(username)
  1476. else:
  1477. url += "service/{}/user/{}".format(service, username)
  1478. headers = {"accept": "application/json"}
  1479. r = self._requests("delete", url, headers=headers)
  1480. if r.status_code == 200:
  1481. return "User deleted successfully"
  1482. else:
  1483. raise GeoserverException(r.status_code, r.content)
  1484. def get_all_usergroups(self, service=None) -> dict:
  1485. url = "{}/rest/security/usergroup/".format(self.service_url)
  1486. if service is None:
  1487. url += "groups/"
  1488. else:
  1489. url += "service/{}/groups/".format(service)
  1490. r = self._requests("get", url)
  1491. if r.status_code == 200:
  1492. return parse(r.content)
  1493. else:
  1494. raise GeoserverException(r.status_code, r.content)
  1495. def create_usergroup(self, group: str, service=None) -> str:
  1496. url = "{}/rest/security/usergroup/".format(self.service_url)
  1497. if service is None:
  1498. url += "group/{}".format(group)
  1499. else:
  1500. url += "service/{}/group/{}".format(service, group)
  1501. r = self._requests("post", url)
  1502. if r.status_code == 201:
  1503. return "Group created successfully"
  1504. else:
  1505. raise GeoserverException(r.status_code, r.content)
  1506. def delete_usergroup(self, group: str, service=None) -> str:
  1507. url = "{}/rest/security/usergroup/".format(self.service_url)
  1508. if service is None:
  1509. url += "group/{}".format(group)
  1510. else:
  1511. url += "service/{}/group/{}".format(service, group)
  1512. r = self._requests("delete", url)
  1513. if r.status_code == 200:
  1514. return "Group deleted successfully"
  1515. else:
  1516. raise GeoserverException(r.status_code, r.content)
  1517. def caching_layer(
  1518. self,
  1519. layer_name: str,
  1520. auto_seed: Optional[bool] = True,
  1521. zoom_start: Optional[int] = default_cache_start,
  1522. zoom_stop: Optional[int] = default_cache_stop,
  1523. gridset_name: Optional[str] = default_gridset_name
  1524. ) -> int:
  1525. url = "{}/gwc/rest/layers/{}".format(
  1526. self.service_url, layer_name
  1527. )
  1528. cache_xml = f"""<GeoServerLayer>
  1529. <enabled>true</enabled>
  1530. <inMemoryCached>true</inMemoryCached>
  1531. <name>{layer_name}</name>
  1532. <mimeFormats>
  1533. <string>image/png</string>
  1534. <string>image/jpeg</string>
  1535. </mimeFormats>
  1536. <gridSubsets>
  1537. <gridSubset>
  1538. <gridSetName>{gridset_name}</gridSetName>
  1539. <zoomStart>{zoom_start}</zoomStart>
  1540. <zoomStop>{zoom_stop}</zoomStop>
  1541. <minCachedLevel>{zoom_start}</minCachedLevel>
  1542. <maxCachedLevel>{zoom_stop}</maxCachedLevel>
  1543. </gridSubset>
  1544. </gridSubsets>
  1545. <metaWidthHeight>
  1546. <int>4</int>
  1547. <int>4</int>
  1548. </metaWidthHeight>
  1549. <expireCache>0</expireCache>
  1550. <expireClients>0</expireClients>
  1551. <parameterFilters>
  1552. </parameterFilters>
  1553. <gutter>0</gutter>
  1554. <autoCacheStyles>true</autoCacheStyles>
  1555. </GeoServerLayer>"""
  1556. headers = {"content-type": "text/xml"}
  1557. gridinfo = self._requests("post", url, data=cache_xml, headers=headers)
  1558. if gridinfo.status_code == 200:
  1559. if auto_seed:
  1560. print(f"""发送{layer_name}切片请求。。。""")
  1561. self.seed_caching_layer(layer_name=layer_name, zoom_start=zoom_start, zoom_stop=zoom_stop)
  1562. return gridinfo.status_code
  1563. else:
  1564. raise GeoserverException(gridinfo.status_code, gridinfo.content)
  1565. def seed_caching_layer(
  1566. self,
  1567. layer_name: str,
  1568. zoom_start: Optional[int] = default_cache_start,
  1569. zoom_stop: Optional[int] = default_cache_stop,
  1570. gridset_name: Optional[str] = default_gridset_name,
  1571. seedtype: Optional[str] = default_seed_type,
  1572. ) -> int:
  1573. url = "{}/gwc/rest/seed/{}?" \
  1574. "threadCount=4" \
  1575. "&type={}" \
  1576. "&gridSetId={}" \
  1577. "&tileFormat=image%2Fpng" \
  1578. "&zoomStart={}" \
  1579. "&zoomStop={}" \
  1580. "&parameter_STYLES=" \
  1581. "&minX=&minY=&maxX=&maxY=&tileFailureRetryCount=-1&tileFailureRetryWaitTime=100&totalFailuresBeforeAborting=1000".format(
  1582. self.service_url, layer_name, seedtype, gridset_name, zoom_start, zoom_stop
  1583. )
  1584. headers = {"content-type": "application/json"}
  1585. gridinfo = self._requests("post", url, data=None, headers=headers)
  1586. if gridinfo.status_code == 201 or gridinfo.status_code == 200:
  1587. return gridinfo.status_code
  1588. else:
  1589. raise GeoserverException(gridinfo.status_code, gridinfo.content)