Skip to content

Commit 99446d5

Browse files
authored
Update ESRI Provider (#2225)
- Allow for alternate id_field - Do native CRS conversion - Safe handle empty responses on get_fields
1 parent 4a8b8ea commit 99446d5

2 files changed

Lines changed: 111 additions & 22 deletions

File tree

pygeoapi/provider/esri.py

Lines changed: 87 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
import logging
3333
from requests import Session, codes
3434

35-
from pygeoapi.crs import crs_transform, get_srid
35+
from pygeoapi.crs import get_srid
3636
from pygeoapi.provider.base import (BaseProvider, ProviderConnectionError,
3737
ProviderTypeError, ProviderQueryError)
3838
from pygeoapi.util import format_datetime
@@ -60,14 +60,19 @@ def __init__(self, provider_def):
6060
super().__init__(provider_def)
6161

6262
self.url = f'{self.data}/query'
63-
self.crs = get_srid(self.storage_crs)
63+
self.srid = get_srid(self.storage_crs)
6464
self.username = provider_def.get('username')
6565
self.password = provider_def.get('password')
6666
self.token_url = provider_def.get('token_service', ARCGIS_URL)
6767
self.token_referer = provider_def.get('referer', GENERATE_TOKEN_URL)
6868
self.token = None
6969
self.session = Session()
7070

71+
self.using_deafult_id = any(
72+
kw == self.id_field
73+
for kw in ['OBJECTID', 'objectid', 'fid']
74+
)
75+
7176
self.login()
7277
self.get_fields()
7378

@@ -80,13 +85,17 @@ def get_fields(self):
8085

8186
if not self._fields:
8287
# Load fields
83-
params = {'f': 'pjson'}
84-
resp = self.get_response(self.data, params=params)
88+
try:
89+
resp = self.get_response(self.data, params={'f': 'pjson'})
90+
except ProviderConnectionError as err:
91+
msg = f'Could not access resource {self.data}: {err}'
92+
LOGGER.error(msg)
93+
return {}
8594

8695
if resp.get('error') is not None:
8796
msg = f"Connection error: {resp['error']['message']}"
8897
LOGGER.error(msg)
89-
raise ProviderConnectionError(msg)
98+
return {}
9099

91100
try:
92101
# Verify Feature/Map Service supports required capabilities
@@ -108,10 +117,10 @@ def get_fields(self):
108117

109118
return self._fields
110119

111-
@crs_transform
112120
def query(self, offset=0, limit=10, resulttype='results',
113121
bbox=[], datetime_=None, properties=[], sortby=[],
114-
select_properties=[], skip_geometry=False, q=None, **kwargs):
122+
select_properties=[], skip_geometry=False,
123+
crs_transform_spec=None, **kwargs):
115124
"""
116125
ESRI query
117126
@@ -124,7 +133,7 @@ def query(self, offset=0, limit=10, resulttype='results',
124133
:param sortby: list of dicts (property, order)
125134
:param select_properties: list of property names
126135
:param skip_geometry: bool of whether to skip geometry (default False)
127-
:param q: full-text search term(s)
136+
:param crs_transform_spec: `CrsTransformSpec` instance, optional
128137
129138
:returns: `dict` of GeoJSON FeatureCollection
130139
"""
@@ -133,7 +142,7 @@ def query(self, offset=0, limit=10, resulttype='results',
133142

134143
params = {
135144
'f': 'geoJSON',
136-
'outSR': self.crs,
145+
'outSR': self._get_srid(crs_transform_spec),
137146
'outFields': self._make_fields(select_properties),
138147
'where': self._make_where(properties, datetime_)
139148
}
@@ -166,30 +175,41 @@ def query(self, offset=0, limit=10, resulttype='results',
166175

167176
return fc
168177

169-
@crs_transform
170-
def get(self, identifier, **kwargs):
178+
def get(self, identifier, crs_transform_spec=None, **kwargs):
171179
"""
172180
Query ESRI by id
173181
174182
:param identifier: feature id
183+
:param crs_transform_spec: `CrsTransformSpec` instance, optional
175184
176185
:returns: dict of single GeoJSON feature
177186
"""
178187

179188
LOGGER.debug(f'Fetching item: {identifier}')
180189
params = {
181190
'f': 'geoJSON',
182-
'outSR': self.crs,
183-
'objectIds': identifier,
191+
'outSR': self._get_srid(crs_transform_spec),
184192
'outFields': self._make_fields()
185193
}
186194

187-
resp = self.get_response(self.url, params=params)
195+
if self.using_deafult_id:
196+
params['objectIds'] = identifier
197+
else:
198+
params['where'] = self._make_where(
199+
[(self.id_field, identifier)]
200+
)
201+
188202
LOGGER.debug('Returning item')
189-
return resp['features'].pop()
203+
[feature] = self._make_features(
204+
self.get_response(params=params)
205+
)
206+
207+
return feature
190208

191209
def login(self):
192-
# Generate token from username and password
210+
"""
211+
Generate login token from username and password
212+
"""
193213
if self.token is None:
194214

195215
if None in [self.username, self.password]:
@@ -211,7 +231,17 @@ def login(self):
211231
'X-Esri-Authorization': f'Bearer {self.token}'
212232
})
213233

214-
def get_response(self, url, **kwargs):
234+
def get_response(self, url: str = None, **kwargs):
235+
"""
236+
Get response from ESRI service
237+
238+
:param url: `str` of ESRI service URL if not using default
239+
240+
:returns: `dict` of ESRI response
241+
"""
242+
if url is None:
243+
url = self.url
244+
215245
# Form URL for GET request
216246
LOGGER.debug('Sending query')
217247
with self.session.get(url, **kwargs) as r:
@@ -314,9 +344,22 @@ def _get_count(self, params):
314344
params['returnCountOnly'] = 'true'
315345
params['f'] = 'pjson'
316346

317-
response = self.get_response(self.url, params=params)
347+
response = self.get_response(params=params)
318348
return response.get('count', 0)
319349

350+
def _get_srid(self, crs_transform_spec):
351+
"""
352+
Get SRID from CrsTransformSpec
353+
354+
:param crs_transform_spec: `CrsTransformSpec` instance
355+
356+
:returns: `int` of SRID
357+
"""
358+
if crs_transform_spec is not None:
359+
return get_srid(crs_transform_spec.target_crs)
360+
361+
return self.srid
362+
320363
def _get_all(self, params, hits_):
321364
"""
322365
Get all features from query args
@@ -329,7 +372,9 @@ def _get_all(self, params, hits_):
329372
params = deepcopy(params)
330373

331374
# Return feature collection
332-
features = self.get_response(self.url, params=params).get('features')
375+
features = self._make_features(
376+
self.get_response(params=params)
377+
)
333378
step = len(features)
334379

335380
# Query if values are less than expected
@@ -338,15 +383,37 @@ def _get_all(self, params, hits_):
338383
params['resultOffset'] += step
339384
params['resultRecordCount'] += step
340385

341-
fs = self.get_response(self.url, params=params).get('features')
386+
fs = self._make_features(
387+
self.get_response(params=params)
388+
)
342389
if len(fs) != 0:
343390
features.extend(fs)
344391
else:
345392
break
346393

347394
return features
348395

396+
def _make_features(self, feature_collection: dict = {}):
397+
"""
398+
Make a feature from features list
399+
400+
:param features: `dict` of features
401+
402+
:returns: `dict` of single feature
403+
"""
404+
features = feature_collection.get('features', [])
405+
406+
for feature in features:
407+
if not self.using_deafult_id:
408+
feature['id'] = \
409+
feature['properties'][self.id_field]
410+
411+
return features
412+
349413
def __exit__(self, **kwargs):
414+
"""
415+
Exit and close session
416+
"""
350417
self.session.close()
351418

352419
def __repr__(self):

tests/provider/test_esri_provider.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,34 @@
3535

3636
TIME_FIELD = 'Date_Time'
3737

38+
BASE_URL = 'https://sampleserver6.arcgisonline.com/arcgis/rest/services'
39+
3840

3941
@pytest.fixture()
4042
def config():
41-
# National Hurricane Center ()
43+
# National Hurricane Center
4244
# source: ESRI, NOAA/National Weather Service
4345
return {
4446
'name': 'ESRI',
4547
'type': 'feature',
46-
'data': 'https://sampleserver6.arcgisonline.com/arcgis/rest/services/Hurricanes/MapServer/0', # noqa
48+
'data': f'{BASE_URL}/Hurricanes/MapServer/0',
4749
'id_field': 'OBJECTID',
4850
'time_field': TIME_FIELD
4951
}
5052

5153

54+
@pytest.fixture()
55+
def config_alt_id():
56+
# Emergency Facilities
57+
# source: ESRI
58+
return {
59+
'name': 'ESRI',
60+
'type': 'feature',
61+
'data': f'{BASE_URL}/EmergencyFacilities/FeatureServer/0',
62+
'id_field': 'facilityid'
63+
}
64+
65+
5266
def test_query(config):
5367
p = ESRIServiceProvider(config)
5468

@@ -179,3 +193,11 @@ def test_get(config):
179193
result = p.get(6)
180194
assert result['id'] == 6
181195
assert result['properties']['EVENTID'] == 'Alberto'
196+
197+
198+
def test_alternative_id_field(config_alt_id):
199+
p = ESRIServiceProvider(config_alt_id)
200+
201+
result = p.get('F0234')
202+
assert result['id'] == 'F0234'
203+
assert result['properties']['facname'] == 'Redlands Community Hospital'

0 commit comments

Comments
 (0)