Skip to content

Commit 852196d

Browse files
committed
upgrade geopy_arcgis
1 parent e59de74 commit 852196d

File tree

1 file changed

+114
-111
lines changed

1 file changed

+114
-111
lines changed

apps/catastro/geopy_arcgis.py

Lines changed: 114 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
# y modificado un poco para agregar params a la url
33

44
import json
5-
import warnings
5+
from functools import partial
66
from time import time
7+
from urllib.parse import urlencode
78

8-
from geopy.compat import Request, string_compare, urlencode
99
from geopy.exc import (
1010
ConfigurationError,
1111
GeocoderAuthenticationFailure,
1212
GeocoderServiceError,
1313
)
14-
from geopy.geocoders.base import DEFAULT_SENTINEL, Geocoder
14+
from geopy.geocoders.base import DEFAULT_SENTINEL, Geocoder, _synchronized
1515
from geopy.location import Location
1616
from geopy.util import logger
1717

@@ -28,21 +28,26 @@ class ArcGIS(Geocoder):
2828
"""
2929

3030
_TOKEN_EXPIRED = 498
31-
_MAX_RETRIES = 3
32-
auth_api = 'https://www.arcgis.com/sharing/generateToken'
31+
32+
auth_path = '/sharing/generateToken'
33+
geocode_path = '/arcgis/rest/services/World/GeocodeServer/findAddressCandidates'
34+
reverse_path = '/arcgis/rest/services/World/GeocodeServer/reverseGeocode'
3335

3436
def __init__(
3537
self,
3638
username=None,
3739
password=None,
40+
*,
3841
referer=None,
3942
token_lifetime=60,
4043
scheme=None,
4144
timeout=DEFAULT_SENTINEL,
4245
proxies=DEFAULT_SENTINEL,
4346
user_agent=None,
44-
format_string=None,
4547
ssl_context=DEFAULT_SENTINEL,
48+
adapter_factory=None,
49+
auth_domain='www.arcgis.com',
50+
domain='geocode.arcgis.com'
4651
):
4752
"""
4853
@@ -74,26 +79,29 @@ def __init__(
7479
:param str user_agent:
7580
See :attr:`geopy.geocoders.options.default_user_agent`.
7681
77-
.. versionadded:: 1.12.0
78-
79-
:param str format_string:
80-
See :attr:`geopy.geocoders.options.default_format_string`.
81-
82-
.. versionadded:: 1.14.0
83-
8482
:type ssl_context: :class:`ssl.SSLContext`
8583
:param ssl_context:
8684
See :attr:`geopy.geocoders.options.default_ssl_context`.
8785
88-
.. versionadded:: 1.14.0
86+
:param callable adapter_factory:
87+
See :attr:`geopy.geocoders.options.default_adapter_factory`.
88+
89+
.. versionadded:: 2.0
90+
91+
:param str auth_domain: Domain where the target ArcGIS auth service
92+
is hosted. Used only in authenticated mode (i.e. username,
93+
password and referer are set).
94+
95+
:param str domain: Domain where the target ArcGIS service
96+
is hosted.
8997
"""
90-
super(ArcGIS, self).__init__(
91-
format_string=format_string,
98+
super().__init__(
9299
scheme=scheme,
93100
timeout=timeout,
94101
proxies=proxies,
95102
user_agent=user_agent,
96103
ssl_context=ssl_context,
104+
adapter_factory=adapter_factory,
97105
)
98106
if username or password or referer:
99107
if not (username and password and referer):
@@ -105,48 +113,38 @@ def __init__(
105113
raise ConfigurationError(
106114
"Authenticated mode requires scheme of 'https'"
107115
)
108-
self._base_call_geocoder = self._call_geocoder
109-
self._call_geocoder = self._authenticated_call_geocoder
110116

111117
self.username = username
112118
self.password = password
113119
self.referer = referer
120+
self.auth_domain = auth_domain.strip('/')
121+
self.auth_api = (
122+
'%s://%s%s' % (self.scheme, self.auth_domain, self.auth_path)
123+
)
114124

115-
self.token = None
116125
self.token_lifetime = token_lifetime * 60 # store in seconds
117-
self.token_expiry = None
118-
self.retry = 1
119126

127+
self.domain = domain.strip('/')
120128
self.api = (
121-
'%s://geocode.arcgis.com/arcgis/rest/services/'
122-
'World/GeocodeServer/findAddressCandidates' % self.scheme
129+
'%s://%s%s' % (self.scheme, self.domain, self.geocode_path)
123130
)
124131
self.reverse_api = (
125-
'%s://geocode.arcgis.com/arcgis/rest/services/'
126-
'World/GeocodeServer/reverseGeocode' % self.scheme
132+
'%s://%s%s' % (self.scheme, self.domain, self.reverse_path)
127133
)
128134

129-
def _authenticated_call_geocoder(self, url, timeout=DEFAULT_SENTINEL):
130-
"""
131-
Wrap self._call_geocoder, handling tokens.
132-
"""
133-
if self.token is None or int(time()) > self.token_expiry:
134-
self._refresh_authentication_token()
135-
request = Request(
136-
"&".join((url, urlencode({"token": self.token}))),
137-
headers={"Referer": self.referer}
138-
)
139-
return self._base_call_geocoder(request, timeout=timeout)
135+
# Mutable state
136+
self.token = None
137+
self.token_expiry = None
140138

141-
def geocode(self, query, exactly_one=True, timeout=DEFAULT_SENTINEL,
139+
def geocode(self, query, *, exactly_one=True, timeout=DEFAULT_SENTINEL,
142140
out_fields=None, extra_params=None):
143141
"""
144142
Return a location point by address.
145143
146144
:param str query: The address or query you wish to geocode.
147145
148146
:param bool exactly_one: Return one result or a list of results, if
149-
available.
147+
available.`
150148
151149
:param int timeout: Time, in seconds, to wait for the geocoding service
152150
to respond before raising a :class:`geopy.exc.GeocoderTimedOut`
@@ -159,35 +157,28 @@ def geocode(self, query, exactly_one=True, timeout=DEFAULT_SENTINEL,
159157
https://developers.arcgis.com/rest/geocode/api-reference/geocoding-service-output.htm
160158
for a list of supported output fields. If you want to return all
161159
supported output fields, set ``out_fields="*"``.
162-
163-
.. versionadded:: 1.14.0
164160
:type out_fields: str or iterable
165161
166162
:rtype: ``None``, :class:`geopy.location.Location` or a list of them, if
167163
``exactly_one=False``.
168164
"""
169-
params = {'singleLine': self.format_string % query, 'f': 'json'}
165+
params = {'singleLine': query, 'f': 'json'}
170166
if exactly_one:
171167
params['maxLocations'] = 1
172168
if out_fields is not None:
173-
if isinstance(out_fields, string_compare):
169+
if isinstance(out_fields, str):
174170
params['outFields'] = out_fields
175171
else:
176172
params['outFields'] = ",".join(out_fields)
177173
if extra_params is not None:
178174
params.update(extra_params)
179175
url = "?".join((self.api, urlencode(params)))
180176
logger.debug("%s.geocode: %s", self.__class__.__name__, url)
181-
response = self._call_geocoder(url, timeout=timeout)
177+
callback = partial(self._parse_geocode, exactly_one=exactly_one)
178+
return self._authenticated_call_geocoder(url, callback, timeout=timeout)
182179

183-
# Handle any errors; recursing in the case of an expired token.
180+
def _parse_geocode(self, response, exactly_one):
184181
if 'error' in response:
185-
if response['error']['code'] == self._TOKEN_EXPIRED:
186-
self.retry += 1
187-
self._refresh_authentication_token()
188-
return self.geocode(
189-
query, exactly_one=exactly_one, timeout=timeout
190-
)
191182
raise GeocoderServiceError(str(response['error']))
192183

193184
# Success; convert from the ArcGIS JSON format.
@@ -205,8 +196,8 @@ def geocode(self, query, exactly_one=True, timeout=DEFAULT_SENTINEL,
205196
return geocoded[0]
206197
return geocoded
207198

208-
def reverse(self, query, exactly_one=True, timeout=DEFAULT_SENTINEL,
209-
distance=None, wkid=DEFAULT_WKID):
199+
def reverse(self, query, *, exactly_one=True, timeout=DEFAULT_SENTINEL,
200+
distance=None):
210201
"""
211202
Return an address by location point.
212203
@@ -227,48 +218,23 @@ def reverse(self, query, exactly_one=True, timeout=DEFAULT_SENTINEL,
227218
within which to search. ArcGIS has a default of 100 meters, if not
228219
specified.
229220
230-
:param str wkid: WKID to use for both input and output coordinates.
231-
232-
.. deprecated:: 1.14.0
233-
It wasn't working before because it was specified incorrectly
234-
in the request parameters, and won't work even if we fix the
235-
request, because :class:`geopy.point.Point` normalizes the
236-
coordinates according to WKID 4326. Please open an issue in
237-
the geopy issue tracker if you believe that custom wkid values
238-
should be supported.
239-
240221
:rtype: ``None``, :class:`geopy.location.Location` or a list of them, if
241222
``exactly_one=False``.
242223
"""
243-
# ArcGIS is lon,lat; maintain lat,lon convention of geopy
244-
point = self._coerce_point_to_string(query).split(",")
245-
if wkid != DEFAULT_WKID:
246-
warnings.warn("%s.reverse: custom wkid value has been ignored. "
247-
"It wasn't working before because it was specified "
248-
"incorrectly in the request parameters, and won't "
249-
"work even if we fix the request, because geopy.Point "
250-
"normalizes the coordinates according to WKID %s. "
251-
"Please open an issue in the geopy issue tracker "
252-
"if you believe that custom wkid values should be "
253-
"supported." % (type(self).__name__, DEFAULT_WKID),
254-
DeprecationWarning)
255-
wkid = DEFAULT_WKID
256-
location = ",".join((point[1], point[0]))
224+
location = self._coerce_point_to_string(query, "%(lon)s,%(lat)s")
225+
wkid = DEFAULT_WKID
257226
params = {'location': location, 'f': 'json', 'outSR': wkid}
258227
if distance is not None:
259228
params['distance'] = distance
260229
url = "?".join((self.reverse_api, urlencode(params)))
261230
logger.debug("%s.reverse: %s", self.__class__.__name__, url)
262-
response = self._call_geocoder(url, timeout=timeout)
231+
callback = partial(self._parse_reverse, exactly_one=exactly_one)
232+
return self._authenticated_call_geocoder(url, callback, timeout=timeout)
233+
234+
def _parse_reverse(self, response, exactly_one):
263235
if not len(response):
264236
return None
265237
if 'error' in response:
266-
if response['error']['code'] == self._TOKEN_EXPIRED:
267-
self.retry += 1
268-
self._refresh_authentication_token()
269-
return self.reverse(query, exactly_one=exactly_one,
270-
timeout=timeout, distance=distance,
271-
wkid=wkid)
272238
# https://developers.arcgis.com/rest/geocode/api-reference/geocoding-service-output.htm
273239
if response['error']['code'] == 400:
274240
# 'details': ['Unable to find address for the specified location.']}
@@ -278,10 +244,15 @@ def reverse(self, query, exactly_one=True, timeout=DEFAULT_SENTINEL,
278244
except (KeyError, IndexError):
279245
pass
280246
raise GeocoderServiceError(str(response['error']))
281-
address = (
282-
"%(Address)s, %(City)s, %(Region)s %(Postal)s,"
283-
" %(CountryCode)s" % response['address']
284-
)
247+
248+
if response['address'].get('Address'):
249+
address = (
250+
"%(Address)s, %(City)s, %(Region)s %(Postal)s,"
251+
" %(CountryCode)s" % response['address']
252+
)
253+
else:
254+
address = response['address']['LongLabel']
255+
285256
location = Location(
286257
address,
287258
(response['location']['y'], response['location']['x']),
@@ -292,14 +263,50 @@ def reverse(self, query, exactly_one=True, timeout=DEFAULT_SENTINEL,
292263
else:
293264
return [location]
294265

295-
def _refresh_authentication_token(self):
296-
"""
297-
POST to ArcGIS requesting a new token.
298-
"""
299-
if self.retry == self._MAX_RETRIES:
300-
raise GeocoderAuthenticationFailure(
301-
'Too many retries for auth: %s' % self.retry
266+
def _authenticated_call_geocoder(
267+
self, url, parse_callback, *, timeout=DEFAULT_SENTINEL
268+
):
269+
if not self.username:
270+
return self._call_geocoder(url, parse_callback, timeout=timeout)
271+
272+
def query_callback():
273+
call_url = "&".join((url, urlencode({"token": self.token})))
274+
headers = {"Referer": self.referer}
275+
return self._call_geocoder(
276+
call_url,
277+
partial(maybe_reauthenticate_callback, from_token=self.token),
278+
timeout=timeout,
279+
headers=headers,
302280
)
281+
282+
def maybe_reauthenticate_callback(response, *, from_token):
283+
if "error" in response:
284+
if response["error"]["code"] == self._TOKEN_EXPIRED:
285+
return self._refresh_authentication_token(
286+
query_retry_callback, timeout=timeout, from_token=from_token
287+
)
288+
return parse_callback(response)
289+
290+
def query_retry_callback():
291+
call_url = "&".join((url, urlencode({"token": self.token})))
292+
headers = {"Referer": self.referer}
293+
return self._call_geocoder(
294+
call_url, parse_callback, timeout=timeout, headers=headers
295+
)
296+
297+
if self.token is None or int(time()) > self.token_expiry:
298+
return self._refresh_authentication_token(
299+
query_callback, timeout=timeout, from_token=self.token
300+
)
301+
else:
302+
return query_callback()
303+
304+
@_synchronized
305+
def _refresh_authentication_token(self, callback_success, *, timeout, from_token):
306+
if from_token != self.token:
307+
# Token has already been updated by a concurrent call.
308+
return callback_success()
309+
303310
token_request_arguments = {
304311
'username': self.username,
305312
'password': self.password,
@@ -312,16 +319,18 @@ def _refresh_authentication_token(self):
312319
"%s._refresh_authentication_token: %s",
313320
self.__class__.__name__, url
314321
)
315-
self.token_expiry = int(time()) + self.token_lifetime
316-
response = self._base_call_geocoder(url)
317-
if 'token' not in response:
318-
raise GeocoderAuthenticationFailure(
319-
'Missing token in auth request.'
320-
'Request URL: %s; response JSON: %s' %
321-
(url, json.dumps(response))
322-
)
323-
self.retry = 0
324-
self.token = response['token']
322+
323+
def cb(response):
324+
if "token" not in response:
325+
raise GeocoderAuthenticationFailure(
326+
"Missing token in auth request."
327+
"Request URL: %s; response JSON: %s" % (url, json.dumps(response))
328+
)
329+
self.token = response["token"]
330+
self.token_expiry = int(time()) + self.token_lifetime
331+
return callback_success()
332+
333+
return self._call_geocoder(url, cb, timeout=timeout)
325334

326335

327336
class ArcGISSuggest(ArcGIS):
@@ -374,7 +383,7 @@ def geocode(self, query, exactly_one=True, timeout=DEFAULT_SENTINEL,
374383
if exactly_one:
375384
params['maxLocations'] = 1
376385
if out_fields is not None:
377-
if isinstance(out_fields, string_compare):
386+
if isinstance(out_fields, str):
378387
params['outFields'] = out_fields
379388
else:
380389
params['outFields'] = ",".join(out_fields)
@@ -387,12 +396,6 @@ def geocode(self, query, exactly_one=True, timeout=DEFAULT_SENTINEL,
387396

388397
# Handle any errors; recursing in the case of an expired token.
389398
if 'error' in response:
390-
if response['error']['code'] == self._TOKEN_EXPIRED:
391-
self.retry += 1
392-
self._refresh_authentication_token()
393-
return self.geocode(
394-
query, exactly_one=exactly_one, timeout=timeout
395-
)
396399
raise GeocoderServiceError(str(response['error']))
397400

398401
# Success; convert from the ArcGIS JSON format.

0 commit comments

Comments
 (0)