Skip to content

Commit 8129026

Browse files
committed
typed features based on csv provider
specialisation of csv with featureType in one of the columns and context file in external json-ld.
1 parent d1ce193 commit 8129026

File tree

2 files changed

+215
-1
lines changed

2 files changed

+215
-1
lines changed

pygeoapi/plugin.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@
6666
'WMSFacade': 'pygeoapi.provider.wms_facade.WMSFacadeProvider',
6767
'WMTSFacade': 'pygeoapi.provider.wmts_facade.WMTSFacadeProvider',
6868
'xarray': 'pygeoapi.provider.xarray_.XarrayProvider',
69-
'xarray-edr': 'pygeoapi.provider.xarray_edr.XarrayEDRProvider'
69+
'xarray-edr': 'pygeoapi.provider.xarray_edr.XarrayEDRProvider',
70+
'xarray-edr-timeseries': 'pygeoapi.provider.xarray_edr_timeseries.XarrayEDRTimeSeriesProvider',
71+
'CSVTyped': 'pygeoapi.provider.csv_typed.CSVTypedProvider'
7072
},
7173
'formatter': {
7274
'CSV': 'pygeoapi.formatter.csv_.CSVFormatter'

pygeoapi/provider/csv_typed.py

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# =================================================================
2+
#
3+
# Authors: Tom Kralidis <[email protected]>
4+
#
5+
# Copyright (c) 2022 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+
30+
from collections import OrderedDict
31+
import csv
32+
import itertools
33+
import logging
34+
35+
from pygeoapi.provider.base import (BaseProvider, ProviderQueryError,
36+
ProviderItemNotFoundError)
37+
from pygeoapi.provider.csv_ import CSVProvider
38+
from pygeoapi.util import get_typed_value, crs_transform
39+
40+
LOGGER = logging.getLogger(__name__)
41+
42+
43+
class CSVTypedProvider(CSVProvider):
44+
"""CSV provider"""
45+
46+
def __init__(self, provider_def):
47+
"""
48+
Initialize object
49+
50+
:param provider_def: provider definition
51+
52+
:returns: pygeoapi.provider.csv_.CSVProvider
53+
"""
54+
55+
super().__init__(provider_def)
56+
self.feature_type_field = provider_def['type_field']
57+
self.context = provider_def['context_value']
58+
59+
def get_fields(self):
60+
"""
61+
Get provider field information (names, types)
62+
63+
:returns: dict of fields
64+
"""
65+
return super().get_fields()
66+
67+
def _load(self, offset=0, limit=10, resulttype='results',
68+
identifier=None, bbox=[], datetime_=None, properties=[],
69+
select_properties=[], skip_geometry=False, q=None):
70+
"""
71+
Load CSV data
72+
73+
:param offset: starting record to return (default 0)
74+
:param limit: number of records to return (default 10)
75+
:param datetime_: temporal (datestamp or extent)
76+
:param resulttype: return results or hit limit (default results)
77+
:param properties: list of tuples (name, value)
78+
:param select_properties: list of property names
79+
:param skip_geometry: bool of whether to skip geometry (default False)
80+
:param q: full-text search term(s)
81+
82+
:returns: dict of GeoJSON FeatureCollection
83+
"""
84+
85+
found = False
86+
result = None
87+
feature_collection = {
88+
'type': 'FeatureCollection',
89+
'features': []
90+
}
91+
if identifier is not None:
92+
# Loop through all rows when searching for a single feature
93+
limit = self._load(resulttype='hits').get('numberMatched')
94+
95+
with open(self.data) as ff:
96+
LOGGER.debug('Serializing DictReader')
97+
data_ = csv.DictReader(ff)
98+
if properties:
99+
data_ = filter(
100+
lambda p: all(
101+
[p[prop[0]] == prop[1] for prop in properties]), data_)
102+
103+
if resulttype == 'hits':
104+
LOGGER.debug('Returning hits only')
105+
feature_collection['numberMatched'] = len(list(data_))
106+
return feature_collection
107+
LOGGER.debug('Slicing CSV rows')
108+
for row in itertools.islice(data_, 0, None):
109+
try:
110+
coordinates = [
111+
float(row.pop(self.geometry_x)),
112+
float(row.pop(self.geometry_y)),
113+
]
114+
except ValueError:
115+
msg = f'Skipping row with invalid geometry: {row.get(self.id_field)}' # noqa
116+
LOGGER.error(msg)
117+
continue
118+
feature = {'type': 'Feature'}
119+
feature['id'] = row.pop(self.id_field)
120+
if not skip_geometry:
121+
feature['geometry'] = {
122+
'type': 'Point',
123+
'coordinates': coordinates
124+
}
125+
else:
126+
feature['geometry'] = None
127+
128+
self.generate_custom_data(select_properties, row, feature)
129+
130+
if identifier is not None and feature['id'] == identifier:
131+
found = True
132+
result = feature
133+
134+
feature_collection['features'].append(feature)
135+
136+
feature_collection['numberMatched'] = \
137+
len(feature_collection['features'])
138+
139+
if identifier is not None and not found:
140+
return None
141+
elif identifier is not None and found:
142+
return result
143+
144+
features_returned = feature_collection['features'][offset:offset+limit]
145+
feature_collection['features'] = features_returned
146+
147+
feature_collection['numberReturned'] = len(
148+
feature_collection['features'])
149+
feature_collection['@context'] = self.context
150+
return feature_collection
151+
152+
def generate_custom_data(self, select_properties, row, feature):
153+
feature['featureType'] = row.pop(self.feature_type_field)
154+
feature['properties'] = OrderedDict()
155+
156+
if self.properties or select_properties:
157+
for p in set(self.properties) | set(select_properties):
158+
try:
159+
feature['properties'][p] = get_typed_value(row[p])
160+
except KeyError as err:
161+
LOGGER.error(err)
162+
raise ProviderQueryError()
163+
else:
164+
for key, value in row.items():
165+
LOGGER.debug(f'key: {key}, value: {value}')
166+
feature['properties'][key] = get_typed_value(value)
167+
168+
@crs_transform
169+
def query(self, offset=0, limit=10, resulttype='results',
170+
bbox=[], datetime_=None, properties=[], sortby=[],
171+
select_properties=[], skip_geometry=False, q=None, **kwargs):
172+
"""
173+
CSV query
174+
175+
:param offset: starting record to return (default 0)
176+
:param limit: number of records to return (default 10)
177+
:param resulttype: return results or hit limit (default results)
178+
:param bbox: bounding box [minx,miny,maxx,maxy]
179+
:param datetime_: temporal (datestamp or extent)
180+
:param properties: list of tuples (name, value)
181+
:param sortby: list of dicts (property, order)
182+
:param select_properties: list of property names
183+
:param skip_geometry: bool of whether to skip geometry (default False)
184+
:param q: full-text search term(s)
185+
186+
:returns: dict of GeoJSON FeatureCollection
187+
"""
188+
189+
return self._load(offset, limit, resulttype,
190+
properties=properties,
191+
select_properties=select_properties,
192+
skip_geometry=skip_geometry)
193+
194+
@crs_transform
195+
def get(self, identifier, **kwargs):
196+
"""
197+
query CSV id
198+
199+
:param identifier: feature id
200+
201+
:returns: dict of single GeoJSON feature
202+
"""
203+
item = self._load(identifier=identifier)
204+
if item:
205+
return item
206+
else:
207+
err = f'item {identifier} not found'
208+
LOGGER.error(err)
209+
raise ProviderItemNotFoundError(err)
210+
211+
def __repr__(self):
212+
return f'<CSVProvider> {self.data}'

0 commit comments

Comments
 (0)