Geoserver.py 68 KB

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