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
« 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# =================================================================
30import json
31import logging
32from enum import Enum
33from http import HTTPStatus
35from pygeoapi.crs import DEFAULT_STORAGE_CRS, get_crs
36from pygeoapi.error import GenericError
38LOGGER = logging.getLogger(__name__)
41class SchemaType(Enum):
42 item = 'item'
43 create = 'create'
44 update = 'update'
45 replace = 'replace'
48class BaseProvider:
49 """generic Provider ABC"""
51 def __init__(self, provider_def):
52 """
53 Initialize object
55 :param provider_def: provider definition
57 :returns: pygeoapi.provider.base.BaseProvider
58 """
60 from pygeoapi.util import str2bool
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')
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
85 # CRS properties
86 storage_crs_uri = provider_def.get('storage_crs', DEFAULT_STORAGE_CRS)
87 self.storage_crs = get_crs(storage_crs_uri)
89 # for coverage providers
90 self.axes = []
91 self.crs = None
92 self.num_bands = None
94 def get_fields(self):
95 """
96 Get provider field information (names, types)
98 Example response:
99 {'field1': {'type': 'string'}, 'field2': {'type': 'number'}}
101 :returns: dict of field names and their associated JSON Schema types
102 """
104 raise NotImplementedError()
106 @property
107 def fields(self) -> dict:
108 """
109 Store provider field information (names, types)
111 Example response:
112 {'field1': {'type': 'string'}, 'field2': {'type': 'number'}}
114 :returns: dict of dicts (field names and their
115 associated JSON Schema definitions)
116 """
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()
123 def get_schema(self, schema_type: SchemaType = SchemaType.item):
124 """
125 Get provider schema model
127 :param schema_type: `SchemaType` of schema (default is 'item')
129 :returns: tuple pair of `str` of media type and `dict` of schema
130 (i.e. JSON Schema)
131 """
133 raise NotImplementedError()
135 def get_data_path(self, baseurl, urlpath, dirpath):
136 """
137 Gets directory listing or file description or raw file dump
139 :param baseurl: base URL of endpoint
140 :param urlpath: base path of URL
141 :param dirpath: directory basepath (equivalent of URL)
143 :returns: `dict` of file listing or `dict` of GeoJSON item or raw file
144 """
146 raise NotImplementedError()
148 def get_metadata(self):
149 """
150 Provide data/file metadata
152 :returns: `dict` of metadata construct (format
153 determined by provider/standard)
154 """
156 raise NotImplementedError()
158 def get_domains(self, properties=[], current=False):
159 """
160 Get domains from dataset
162 :param properties: `list` of property names
163 :param current: `bool` of whether to provide list of live
164 values (default `False`)
166 :returns: `tuple` of domains and whether they are based on the
167 current/live dataset
168 """
170 raise NotImplementedError()
172 def query(self):
173 """
174 query the provider
176 :returns: dict of 0..n GeoJSON features or coverage data
177 """
179 raise NotImplementedError()
181 def get(self, identifier, **kwargs):
182 """
183 query the provider by id
185 :param identifier: feature id
187 :returns: dict of single GeoJSON feature
188 """
190 raise NotImplementedError()
192 def create(self, item):
193 """
194 Create a new item
196 :param item: `dict` of new item
198 :returns: identifier of created item
199 """
201 raise NotImplementedError()
203 def update(self, identifier, item):
204 """
205 Updates an existing item
207 :param identifier: feature id
208 :param item: `dict` of partial or full item
210 :returns: `bool` of update result
211 """
213 raise NotImplementedError()
215 def delete(self, identifier):
216 """
217 Deletes an existing item
219 :param identifier: item id
221 :returns: `bool` of deletion result
222 """
224 raise NotImplementedError()
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
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
241 :returns: `tuple` of item identifier and item data/payload
242 """
244 identifier2 = None
245 msg = None
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'
258 if msg is not None:
259 raise ProviderInvalidDataError(msg)
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')
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)
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)
284 if identifier2 is not None and raise_if_exists:
285 LOGGER.debug('Querying database whether item exists')
286 try:
287 _ = self.get(identifier2)
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')
295 return identifier2, json_data
297 def __repr__(self):
298 return f'<BaseProvider> {self.type}'
301class ProviderGenericError(GenericError):
302 """provider generic error"""
303 default_msg = 'generic error (check logs)'
306class ProviderConnectionError(ProviderGenericError):
307 """provider connection error"""
308 default_msg = 'connection error (check logs)'
311class ProviderTypeError(ProviderGenericError):
312 """provider type error"""
313 default_msg = 'invalid provider type'
314 http_status_code = HTTPStatus.BAD_REQUEST
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"
324class ProviderQueryError(ProviderGenericError):
325 """provider query error"""
326 default_msg = 'query error (check logs)'
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'
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'
343class ProviderNotFoundError(ProviderGenericError):
344 """provider not found error"""
345 pass
348class ProviderVersionError(ProviderGenericError):
349 """provider incorrect version error"""
350 pass
353class ProviderInvalidDataError(ProviderGenericError):
354 """provider invalid data error"""
355 pass
358class ProviderRequestEntityTooLargeError(ProviderGenericError):
359 """provider request entity too large error"""
360 http_status_code = HTTPStatus.REQUEST_ENTITY_TOO_LARGE
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)