22# y modificado un poco para agregar params a la url
33
44import json
5- import warnings
5+ from functools import partial
66from time import time
7+ from urllib .parse import urlencode
78
8- from geopy .compat import Request , string_compare , urlencode
99from 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
1515from geopy .location import Location
1616from 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
327336class 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