def get_collection_item(self, headers_, format_, dataset, identifier): """ Get a single feature :param headers_: copy of HEADERS object :param format_: format of requests, pre checked by pre_process decorator :param dataset: dataset name :param identifier: feature identifier :returns: tuple of headers, status code, content """ if format_ is not None and format_ not in FORMATS: exception = { 'code': 'InvalidParameterValue', 'description': 'Invalid format' } LOGGER.error(exception) return headers_, 400, json.dumps(exception) LOGGER.debug('Processing query parameters') if dataset not in self.config['datasets'].keys(): exception = { 'code': 'InvalidParameterValue', 'description': 'Invalid feature collection' } LOGGER.error(exception) return headers_, 400, json.dumps(exception) LOGGER.debug('Loading provider') p = load_plugin('provider', self.config['datasets'][dataset]['provider']) try: LOGGER.debug('Fetching id {}'.format(identifier)) content = p.get(identifier) except ProviderQueryError as err: exception = { 'code': 'NoApplicableCode', 'description': 'query error (check logs)' } LOGGER.error(err) return headers_, 500, json.dumps(exception) except ProviderGenericError as err: exception = { 'code': 'NoApplicableCode', 'description': 'generic error (check logs)' } LOGGER.error(err) return headers_, 500, json.dumps(exception) if content is None: exception = { 'code': 'NotFound', 'description': 'identifier not found' } LOGGER.error(exception) return headers_, 404, json.dumps(exception) content['links'] = [{ 'rel': 'self' if not format_ or format_ == 'json' else 'alternate', 'type': 'application/geo+json', 'title': 'This document as GeoJSON', 'href': '{}/collections/{}/items/{}?f=json'.format( self.config['server']['url'], dataset, identifier) }, { 'rel': 'self' if format_ == 'jsonld' else 'alternate', 'type': 'application/ld+json', 'title': 'This document as RDF (JSON-LD)', 'href': '{}/collections/{}/items/{}?f=jsonld'.format( self.config['server']['url'], dataset, identifier) }, { 'rel': 'self' if format_ == 'html' else 'alternate', 'type': 'text/html', 'title': 'This document as HTML', 'href': '{}/collections/{}/items/{}?f=html'.format( self.config['server']['url'], dataset, identifier) }, { 'rel': 'collection', 'type': 'application/json', 'title': self.config['datasets'][dataset]['title'], 'href': '{}/collections/{}'.format(self.config['server']['url'], dataset) }, { 'rel': 'prev', 'type': 'application/geo+json', 'href': '{}/collections/{}/items/{}'.format(self.config['server']['url'], dataset, identifier) }, { 'rel': 'next', 'type': 'application/geo+json', 'href': '{}/collections/{}/items/{}'.format(self.config['server']['url'], dataset, identifier) }] if format_ == 'html': # render headers_['Content-Type'] = 'text/html' content['title'] = self.config['datasets'][dataset]['title'] content = render_j2_template(self.config, 'item.html', content) return headers_, 200, content elif format_ == 'jsonld': headers_['Content-Type'] = 'application/ld+json' content = geojson2geojsonld(self.config, content, dataset, identifier=identifier) return headers_, 200, content return headers_, 200, json.dumps(content, default=json_serial)
def get_collection_items(self, headers, args, dataset, pathinfo=None): """ Queries feature collection :param headers: dict of HTTP headers :param args: dict of HTTP request parameters :param dataset: dataset name :param pathinfo: path location :returns: tuple of headers, status code, content """ headers_ = HEADERS.copy() properties = [] reserved_fieldnames = [ 'bbox', 'f', 'limit', 'startindex', 'resulttype', 'datetime', 'sortby' ] formats = FORMATS formats.extend(f.lower() for f in PLUGINS['formatter'].keys()) if dataset not in self.config['datasets'].keys(): exception = { 'code': 'InvalidParameterValue', 'description': 'Invalid feature collection' } LOGGER.error(exception) return headers_, 400, json.dumps(exception, default=json_serial) format_ = check_format(args, headers) if format_ is not None and format_ not in formats: exception = { 'code': 'InvalidParameterValue', 'description': 'Invalid format' } LOGGER.error(exception) return headers_, 400, json.dumps(exception) LOGGER.debug('Processing query parameters') LOGGER.debug('Processing startindex parameter') try: startindex = int(args.get('startindex')) if startindex < 0: exception = { 'code': 'InvalidParameterValue', 'description': 'startindex value should be positive ' + 'or zero' } LOGGER.error(exception) return headers_, 400, json.dumps(exception) except (TypeError) as err: LOGGER.warning(err) startindex = 0 except ValueError as err: LOGGER.warning(err) exception = { 'code': 'InvalidParameterValue', 'description': 'startindex value should be an integer' } LOGGER.error(exception) return headers_, 400, json.dumps(exception) LOGGER.debug('Processing limit parameter') try: limit = int(args.get('limit')) # TODO: We should do more validation, against the min and max # allowed by the server configuration if limit <= 0: exception = { 'code': 'InvalidParameterValue', 'description': 'limit value should be strictly positive' } LOGGER.error(exception) return headers_, 400, json.dumps(exception) except TypeError as err: LOGGER.warning(err) limit = int(self.config['server']['limit']) except ValueError as err: LOGGER.warning(err) exception = { 'code': 'InvalidParameterValue', 'description': 'limit value should be an integer' } LOGGER.error(exception) return headers_, 400, json.dumps(exception) resulttype = args.get('resulttype') or 'results' LOGGER.debug('Processing bbox parameter') try: bbox = args.get('bbox').split(',') if len(bbox) != 4: exception = { 'code': 'InvalidParameterValue', 'description': 'bbox values should be minx,miny,maxx,maxy' } LOGGER.error(exception) return headers_, 400, json.dumps(exception) except AttributeError: bbox = [] try: bbox = [float(c) for c in bbox] except ValueError: exception = { 'code': 'InvalidParameterValue', 'description': 'bbox values must be numbers' } LOGGER.error(exception) return headers_, 400, json.dumps(exception) LOGGER.debug('Processing datetime parameter') # TODO: pass datetime to query as a `datetime` object # we would need to ensure partial dates work accordingly # as well as setting '..' values to `None` so that underlying # providers can just assume a `datetime.datetime` object # # NOTE: needs testing when passing partials from API to backend datetime_ = args.get('datetime') datetime_invalid = False if (datetime_ is not None and 'temporal' in self.config['datasets'][dataset]['extents']): te = self.config['datasets'][dataset]['extents']['temporal'] if te['begin'].tzinfo is None: te['begin'] = te['begin'].replace(tzinfo=pytz.UTC) if te['end'].tzinfo is None: te['end'] = te['end'].replace(tzinfo=pytz.UTC) if '/' in datetime_: # envelope LOGGER.debug('detected time range') LOGGER.debug('Validating time windows') datetime_begin, datetime_end = datetime_.split('/') if datetime_begin != '..': datetime_begin = dateparse(datetime_begin) if datetime_begin.tzinfo is None: datetime_begin = datetime_begin.replace( tzinfo=pytz.UTC) if datetime_end != '..': datetime_end = dateparse(datetime_end) if datetime_end.tzinfo is None: datetime_end = datetime_end.replace(tzinfo=pytz.UTC) if te['begin'] is not None and datetime_begin != '..': if datetime_begin < te['begin']: datetime_invalid = True if te['end'] is not None and datetime_end != '..': if datetime_end > te['end']: datetime_invalid = True else: # time instant datetime__ = dateparse(datetime_) if datetime__ != '..': if datetime__.tzinfo is None: datetime__ = datetime__.replace(tzinfo=pytz.UTC) LOGGER.debug('detected time instant') if te['begin'] is not None and datetime__ != '..': if datetime__ < te['begin']: datetime_invalid = True if te['end'] is not None and datetime__ != '..': if datetime__ > te['end']: datetime_invalid = True if datetime_invalid: exception = { 'code': 'InvalidParameterValue', 'description': 'datetime parameter out of range' } LOGGER.error(exception) return headers_, 400, json.dumps(exception) LOGGER.debug('Loading provider') try: p = load_plugin('provider', self.config['datasets'][dataset]['provider']) except ProviderConnectionError: exception = { 'code': 'NoApplicableCode', 'description': 'connection error (check logs)' } LOGGER.error(exception) return headers_, 500, json.dumps(exception) except ProviderQueryError: exception = { 'code': 'NoApplicableCode', 'description': 'query error (check logs)' } LOGGER.error(exception) return headers_, 500, json.dumps(exception) LOGGER.debug('processing property parameters') for k, v in args.items(): if k not in reserved_fieldnames and k not in p.fields.keys(): exception = { 'code': 'InvalidParameterValue', 'description': 'unknown query parameter' } LOGGER.error(exception) return headers_, 400, json.dumps(exception) elif k not in reserved_fieldnames and k in p.fields.keys(): LOGGER.debug('Add property filter {}={}'.format(k, v)) properties.append((k, v)) LOGGER.debug('processing sort parameter') val = args.get('sortby') if val is not None: sortby = [] sorts = val.split(',') for s in sorts: if ':' in s: prop, order = s.split(':') if order not in ['A', 'D']: exception = { 'code': 'InvalidParameterValue', 'description': 'sort order should be A or D' } LOGGER.error(exception) return headers_, 400, json.dumps(exception) sortby.append({'property': prop, 'order': order}) else: sortby.append({'property': s, 'order': 'A'}) for s in sortby: if s['property'] not in p.fields.keys(): exception = { 'code': 'InvalidParameterValue', 'description': 'bad sort property' } LOGGER.error(exception) return headers_, 400, json.dumps(exception) else: sortby = [] LOGGER.debug('Querying provider') LOGGER.debug('startindex: {}'.format(startindex)) LOGGER.debug('limit: {}'.format(limit)) LOGGER.debug('resulttype: {}'.format(resulttype)) LOGGER.debug('sortby: {}'.format(sortby)) try: content = p.query(startindex=startindex, limit=limit, resulttype=resulttype, bbox=bbox, datetime=datetime_, properties=properties, sortby=sortby) except ProviderConnectionError as err: exception = { 'code': 'NoApplicableCode', 'description': 'connection error (check logs)' } LOGGER.error(err) return headers_, 500, json.dumps(exception) except ProviderQueryError as err: exception = { 'code': 'NoApplicableCode', 'description': 'query error (check logs)' } LOGGER.error(err) return headers_, 500, json.dumps(exception) except ProviderGenericError as err: exception = { 'code': 'NoApplicableCode', 'description': 'generic error (check logs)' } LOGGER.error(err) return headers_, 500, json.dumps(exception) serialized_query_params = '' for k, v in args.items(): if k not in ('f', 'startindex'): serialized_query_params += '&' serialized_query_params += urllib.parse.quote(k, safe='') serialized_query_params += '=' serialized_query_params += urllib.parse.quote(str(v), safe=',') content['links'] = [{ 'type': 'application/geo+json', 'rel': 'self' if not format_ or format_ == 'json' else 'alternate', 'title': 'This document as GeoJSON', 'href': '{}/collections/{}/items?f=json{}'.format( self.config['server']['url'], dataset, serialized_query_params) }, { 'rel': 'self' if format_ == 'jsonld' else 'alternate', 'type': 'application/ld+json', 'title': 'This document as RDF (JSON-LD)', 'href': '{}/collections/{}/items?f=jsonld{}'.format( self.config['server']['url'], dataset, serialized_query_params) }, { 'type': 'text/html', 'rel': 'self' if format_ == 'html' else 'alternate', 'title': 'This document as HTML', 'href': '{}/collections/{}/items?f=html{}'.format( self.config['server']['url'], dataset, serialized_query_params) }] if startindex > 0: prev = max(0, startindex - limit) content['links'].append({ 'type': 'application/geo+json', 'rel': 'prev', 'title': 'items (prev)', 'href': '{}/collections/{}/items?startindex={}{}'.format( self.config['server']['url'], dataset, prev, serialized_query_params) }) if len(content['features']) == limit: next_ = startindex + limit content['links'].append({ 'type': 'application/geo+json', 'rel': 'next', 'title': 'items (next)', 'href': '{}/collections/{}/items?startindex={}{}'.format( self.config['server']['url'], dataset, next_, serialized_query_params) }) content['links'].append({ 'type': 'application/json', 'title': self.config['datasets'][dataset]['title'], 'rel': 'collection', 'href': '{}/collections/{}'.format(self.config['server']['url'], dataset) }) content['timeStamp'] = datetime.utcnow().strftime( '%Y-%m-%dT%H:%M:%S.%fZ') if format_ == 'html': # render headers_['Content-Type'] = 'text/html' # For constructing proper URIs to items if pathinfo: path_info = '/'.join([ self.config['server']['url'].rstrip('/'), pathinfo.strip('/') ]) else: path_info = '/'.join([ self.config['server']['url'].rstrip('/'), headers.environ['PATH_INFO'].strip('/') ]) content['items_path'] = path_info content['dataset_path'] = '/'.join(path_info.split('/')[:-1]) content['collections_path'] = '/'.join(path_info.split('/')[:-2]) content['startindex'] = startindex content = render_j2_template(self.config, 'items.html', content) return headers_, 200, content elif format_ == 'csv': # render formatter = load_plugin('formatter', {'name': 'CSV', 'geom': True}) content = formatter.write( data=content, options={ 'provider_def': self.config['datasets'][dataset]['provider'] }) headers_['Content-Type'] = '{}; charset={}'.format( formatter.mimetype, self.config['server']['encoding']) cd = 'attachment; filename="{}.csv"'.format(dataset) headers_['Content-Disposition'] = cd return headers_, 200, content elif format_ == 'jsonld': headers_['Content-Type'] = 'application/ld+json' content = geojson2geojsonld(self.config, content, dataset) return headers_, 200, content return headers_, 200, json.dumps(content, default=json_serial)