Coverage for pygeoapi/provider/base.py: 55%

145 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 08:15 +0000

1# ================================================================= 

2# 

3# Authors: Tom Kralidis <tomkralidis@gmail.com> 

4# 

5# Copyright (c) 2026 Tom Kralidis 

6# 

7# Permission is hereby granted, free of charge, to any person 

8# obtaining a copy of this software and associated documentation 

9# files (the "Software"), to deal in the Software without 

10# restriction, including without limitation the rights to use, 

11# copy, modify, merge, publish, distribute, sublicense, and/or sell 

12# copies of the Software, and to permit persons to whom the 

13# Software is furnished to do so, subject to the following 

14# conditions: 

15# 

16# The above copyright notice and this permission notice shall be 

17# included in all copies or substantial portions of the Software. 

18# 

19# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 

20# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 

21# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 

22# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 

23# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 

24# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 

25# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 

26# OTHER DEALINGS IN THE SOFTWARE. 

27# 

28# ================================================================= 

29 

30import json 

31import logging 

32from enum import Enum 

33from http import HTTPStatus 

34 

35from pygeoapi.crs import DEFAULT_STORAGE_CRS, get_crs 

36from pygeoapi.error import GenericError 

37 

38LOGGER = logging.getLogger(__name__) 

39 

40 

41class SchemaType(Enum): 

42 item = 'item' 

43 create = 'create' 

44 update = 'update' 

45 replace = 'replace' 

46 

47 

48class BaseProvider: 

49 """generic Provider ABC""" 

50 

51 def __init__(self, provider_def): 

52 """ 

53 Initialize object 

54 

55 :param provider_def: provider definition 

56 

57 :returns: pygeoapi.provider.base.BaseProvider 

58 """ 

59 

60 from pygeoapi.util import str2bool 

61 

62 try: 

63 self.name = provider_def['name'] 

64 self.type = provider_def['type'] 

65 self.data = provider_def['data'] 

66 except KeyError: 

67 raise RuntimeError('name/type/data are required') 

68 

69 self.editable = provider_def.get('editable', False) 

70 self.count = str2bool(provider_def.get('count', True)) 

71 self.options = provider_def.get('options') 

72 self.id_field = provider_def.get('id_field') 

73 self.uri_field = provider_def.get('uri_field') 

74 self.x_field = provider_def.get('x_field') 

75 self.y_field = provider_def.get('y_field') 

76 self.z_field = provider_def.get('z_field') 

77 self.time_field = provider_def.get('time_field') 

78 self.title_field = provider_def.get('title_field') 

79 self.properties = provider_def.get('properties', []) 

80 self.file_types = provider_def.get('file_types', []) 

81 self.include_extra_query_parameters = provider_def.get('include_extra_query_parameters', False) # noqa 

82 self._fields = {} 

83 self.filename = None 

84 

85 # CRS properties 

86 storage_crs_uri = provider_def.get('storage_crs', DEFAULT_STORAGE_CRS) 

87 self.storage_crs = get_crs(storage_crs_uri) 

88 

89 # for coverage providers 

90 self.axes = [] 

91 self.crs = None 

92 self.num_bands = None 

93 

94 def get_fields(self): 

95 """ 

96 Get provider field information (names, types) 

97 

98 Example response: 

99 {'field1': {'type': 'string'}, 'field2': {'type': 'number'}} 

100 

101 :returns: dict of field names and their associated JSON Schema types 

102 """ 

103 

104 raise NotImplementedError() 

105 

106 @property 

107 def fields(self) -> dict: 

108 """ 

109 Store provider field information (names, types) 

110 

111 Example response: 

112 {'field1': {'type': 'string'}, 'field2': {'type': 'number'}} 

113 

114 :returns: dict of dicts (field names and their 

115 associated JSON Schema definitions) 

116 """ 

117 

118 if hasattr(self, '_fields'): 118 ↛ 121line 118 didn't jump to line 121 because the condition on line 118 was always true

119 return self._fields 

120 else: 

121 return self.get_fields() 

122 

123 def get_schema(self, schema_type: SchemaType = SchemaType.item): 

124 """ 

125 Get provider schema model 

126 

127 :param schema_type: `SchemaType` of schema (default is 'item') 

128 

129 :returns: tuple pair of `str` of media type and `dict` of schema 

130 (i.e. JSON Schema) 

131 """ 

132 

133 raise NotImplementedError() 

134 

135 def get_data_path(self, baseurl, urlpath, dirpath): 

136 """ 

137 Gets directory listing or file description or raw file dump 

138 

139 :param baseurl: base URL of endpoint 

140 :param urlpath: base path of URL 

141 :param dirpath: directory basepath (equivalent of URL) 

142 

143 :returns: `dict` of file listing or `dict` of GeoJSON item or raw file 

144 """ 

145 

146 raise NotImplementedError() 

147 

148 def get_metadata(self): 

149 """ 

150 Provide data/file metadata 

151 

152 :returns: `dict` of metadata construct (format 

153 determined by provider/standard) 

154 """ 

155 

156 raise NotImplementedError() 

157 

158 def get_domains(self, properties=[], current=False): 

159 """ 

160 Get domains from dataset 

161 

162 :param properties: `list` of property names 

163 :param current: `bool` of whether to provide list of live 

164 values (default `False`) 

165 

166 :returns: `tuple` of domains and whether they are based on the 

167 current/live dataset 

168 """ 

169 

170 raise NotImplementedError() 

171 

172 def query(self): 

173 """ 

174 query the provider 

175 

176 :returns: dict of 0..n GeoJSON features or coverage data 

177 """ 

178 

179 raise NotImplementedError() 

180 

181 def get(self, identifier, **kwargs): 

182 """ 

183 query the provider by id 

184 

185 :param identifier: feature id 

186 

187 :returns: dict of single GeoJSON feature 

188 """ 

189 

190 raise NotImplementedError() 

191 

192 def create(self, item): 

193 """ 

194 Create a new item 

195 

196 :param item: `dict` of new item 

197 

198 :returns: identifier of created item 

199 """ 

200 

201 raise NotImplementedError() 

202 

203 def update(self, identifier, item): 

204 """ 

205 Updates an existing item 

206 

207 :param identifier: feature id 

208 :param item: `dict` of partial or full item 

209 

210 :returns: `bool` of update result 

211 """ 

212 

213 raise NotImplementedError() 

214 

215 def delete(self, identifier): 

216 """ 

217 Deletes an existing item 

218 

219 :param identifier: item id 

220 

221 :returns: `bool` of deletion result 

222 """ 

223 

224 raise NotImplementedError() 

225 

226 def _load_and_prepare_item(self, item, identifier=None, 

227 accept_missing_identifier=False, 

228 raise_if_exists=True): 

229 """ 

230 Helper function to load a record, detect its idenfier and prepare 

231 a record item 

232 

233 :param item: `str` of incoming item data 

234 :param identifier: `str` of item identifier (optional) 

235 :param accept_missing_identifier: `bool` of whether a missing 

236 identifier in item is valid 

237 (typically for a create() method) 

238 :param raise_if_exists: `bool` of whether to check if record 

239 already exists 

240 

241 :returns: `tuple` of item identifier and item data/payload 

242 """ 

243 

244 identifier2 = None 

245 msg = None 

246 

247 LOGGER.debug('Loading data') 

248 LOGGER.debug(f'Data: {item}') 

249 try: 

250 json_data = json.loads(item) 

251 except TypeError as err: 

252 LOGGER.error(err) 

253 msg = 'Invalid data' 

254 except json.decoder.JSONDecodeError as err: 

255 LOGGER.error(err) 

256 msg = 'Invalid JSON data' 

257 

258 if msg is not None: 

259 raise ProviderInvalidDataError(msg) 

260 

261 LOGGER.debug('Detecting identifier') 

262 if identifier is not None: 

263 identifier2 = identifier 

264 else: 

265 try: 

266 identifier2 = json_data['id'] 

267 except KeyError: 

268 LOGGER.debug('Cannot find id; trying properties.identifier') 

269 try: 

270 identifier2 = json_data['properties']['identifier'] 

271 except KeyError: 

272 LOGGER.debug('Cannot find properties.identifier') 

273 

274 if identifier2 is None and not accept_missing_identifier: 

275 msg = 'Missing identifier (id or properties.identifier)' 

276 LOGGER.error(msg) 

277 raise ProviderInvalidDataError(msg) 

278 

279 if 'geometry' not in json_data or 'properties' not in json_data: 

280 msg = 'Missing core GeoJSON geometry or properties' 

281 LOGGER.error(msg) 

282 raise ProviderInvalidDataError(msg) 

283 

284 if identifier2 is not None and raise_if_exists: 

285 LOGGER.debug('Querying database whether item exists') 

286 try: 

287 _ = self.get(identifier2) 

288 

289 msg = 'record already exists' 

290 LOGGER.error(msg) 

291 raise ProviderInvalidDataError(user_msg=msg) 

292 except ProviderItemNotFoundError: 

293 LOGGER.debug('record does not exist') 

294 

295 return identifier2, json_data 

296 

297 def __repr__(self): 

298 return f'<BaseProvider> {self.type}' 

299 

300 

301class ProviderGenericError(GenericError): 

302 """provider generic error""" 

303 default_msg = 'generic error (check logs)' 

304 

305 

306class ProviderConnectionError(ProviderGenericError): 

307 """provider connection error""" 

308 default_msg = 'connection error (check logs)' 

309 

310 

311class ProviderTypeError(ProviderGenericError): 

312 """provider type error""" 

313 default_msg = 'invalid provider type' 

314 http_status_code = HTTPStatus.BAD_REQUEST 

315 

316 

317class ProviderInvalidQueryError(ProviderGenericError): 

318 """provider invalid query error""" 

319 ogc_exception_code = 'InvalidQuery' 

320 http_status_code = HTTPStatus.BAD_REQUEST 

321 default_msg = "query error" 

322 

323 

324class ProviderQueryError(ProviderGenericError): 

325 """provider query error""" 

326 default_msg = 'query error (check logs)' 

327 

328 

329class ProviderItemNotFoundError(ProviderGenericError): 

330 """provider item not found query error""" 

331 ogc_exception_code = 'NotFound' 

332 http_status_code = HTTPStatus.NOT_FOUND 

333 default_msg = 'identifier not found' 

334 

335 

336class ProviderNoDataError(ProviderGenericError): 

337 """provider no data error""" 

338 ogc_exception_code = 'InvalidParameterValue' 

339 http_status_code = HTTPStatus.NO_CONTENT 

340 default_msg = 'No data found' 

341 

342 

343class ProviderNotFoundError(ProviderGenericError): 

344 """provider not found error""" 

345 pass 

346 

347 

348class ProviderVersionError(ProviderGenericError): 

349 """provider incorrect version error""" 

350 pass 

351 

352 

353class ProviderInvalidDataError(ProviderGenericError): 

354 """provider invalid data error""" 

355 pass 

356 

357 

358class ProviderRequestEntityTooLargeError(ProviderGenericError): 

359 """provider request entity too large error""" 

360 http_status_code = HTTPStatus.REQUEST_ENTITY_TOO_LARGE 

361 

362 def __init__(self, msg=None, *args, user_msg=None) -> None: 

363 if msg and not user_msg: 

364 # This error type shows the error by default 

365 user_msg = msg 

366 super().__init__(msg, *args, user_msg=user_msg)