Thanks to visit codestin.com
Credit goes to github.com

Skip to content
4 changes: 2 additions & 2 deletions geopy/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def isfinite(x):

if py3k: # pragma: no cover
from urllib.error import HTTPError
from urllib.parse import parse_qs, quote, urlencode, urlparse
from urllib.parse import parse_qs, quote, quote_plus, urlencode, urlparse
from urllib.request import (HTTPBasicAuthHandler, HTTPPasswordMgrWithDefaultRealm,
HTTPSHandler, ProxyHandler, Request, URLError,
build_opener, urlopen)
Expand All @@ -72,7 +72,7 @@ def iteritems(d):
return iter(d.items())

else: # pragma: no cover
from urllib import quote # noqa
from urllib import quote, quote_plus # noqa
from urllib import urlencode as original_urlencode
from urllib2 import (HTTPBasicAuthHandler, HTTPError, # noqa
HTTPPasswordMgrWithDefaultRealm, HTTPSHandler, ProxyHandler,
Expand Down
92 changes: 68 additions & 24 deletions geopy/geocoders/baidu.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
:class:`.Baidu` is the Baidu Maps geocoder.
"""

from geopy.compat import urlencode
from geopy.compat import quote, quote_plus
from geopy.exc import (
GeocoderServiceError,
GeocoderAuthenticationFailure,
GeocoderQueryError,
GeocoderQuotaExceeded,
)
from geopy.geocoders.base import DEFAULT_SENTINEL, Geocoder
from geopy.location import Location
from geopy.util import logger
import hashlib

__all__ = ("Baidu", )

Expand All @@ -33,6 +35,7 @@ def __init__(
user_agent=None,
format_string=None,
ssl_context=DEFAULT_SENTINEL,
security_key=None,
):
"""

Expand Down Expand Up @@ -67,6 +70,10 @@ def __init__(
See :attr:`geopy.geocoders.options.default_ssl_context`.

.. versionadded:: 1.14.0

:param str security_key: The security key to calculate <sn> parameter
in request if authentication setting requires.
(http://lbsyun.baidu.com/index.php?title=lbscloud/api/appendix)
"""
super(Baidu, self).__init__(
format_string=format_string,
Expand All @@ -77,7 +84,9 @@ def __init__(
ssl_context=ssl_context,
)
self.api_key = api_key
self.api = '%s://api.map.baidu.com/geocoder/v2/' % self.scheme
self.api_path = '/geocoder/v2/'
self.api = '%s://api.map.baidu.com%s' % (self.scheme, self.api_path)
self.security_key = security_key

@staticmethod
def _format_components_param(components):
Expand Down Expand Up @@ -114,10 +123,15 @@ def geocode(
params = {
'ak': self.api_key,
'output': 'json',
'address': self.format_string % query,
'address': quote(self.format_string % query, safe="!*'();:@&=+$,/?#[]"),
}
params = "&".join(["{}={}".format(k, v) for k, v in params.items()])

if self.security_key is None:
url = "?".join((self.api, params))
else:
url = self._url_with_signature(params)

url = "?".join((self.api, urlencode(params)))
logger.debug("%s.geocode: %s", self.__class__.__name__, url)
return self._parse_json(
self._call_geocoder(url, timeout=timeout), exactly_one=exactly_one
Expand Down Expand Up @@ -149,10 +163,17 @@ def reverse(self, query, exactly_one=True, timeout=DEFAULT_SENTINEL):
params = {
'ak': self.api_key,
'output': 'json',
'location': self._coerce_point_to_string(query),
'location': quote(
self._coerce_point_to_string(query),
safe="!*'();:@&=+$,/?#[]"
),
}
params = "&".join(["{}={}".format(k, v) for k, v in params.items()])

url = "?".join((self.api, urlencode(params)))
if self.security_key is None:
url = "?".join((self.api, params))
else:
url = self._url_with_signature(params)

logger.debug("%s.reverse: %s", self.__class__.__name__, url)
return self._parse_reverse_json(
Expand All @@ -163,7 +184,11 @@ def _parse_reverse_json(self, page, exactly_one=True):
"""
Parses a location from a single-result reverse API call.
"""
place = page.get('result')
place = page.get('result', None)

if not place:
self._check_status(page.get('status'))
return None

location = place.get('formatted_address').encode('utf-8')
latitude = place['location']['lat']
Expand Down Expand Up @@ -205,44 +230,63 @@ def _check_status(status):
"""
Validates error statuses.
"""
if status == '0':
if status == 0:
# When there are no results, just return.
return
if status == '1':
raise GeocoderQueryError(
if status == 1:
raise GeocoderServiceError(
'Internal server error.'
)
elif status == '2':
elif status == 2:
raise GeocoderQueryError(
'Invalid request.'
)
elif status == '3':
elif status == 3:
raise GeocoderAuthenticationFailure(
'Authentication failure.'
)
elif status == '4':
elif status == 4:
raise GeocoderQuotaExceeded(
'Quota validate failure.'
)
elif status == '5':
elif status == 5:
raise GeocoderQueryError(
'AK Illegal or Not Exist.'
)
elif status == '101':
raise GeocoderQueryError(
'Your request was denied.'
elif status == 101:
raise GeocoderAuthenticationFailure(
'No AK'
)
elif status == '102':
raise GeocoderQueryError(
'IP/SN/SCODE/REFERER Illegal:'
elif status == 102:
raise GeocoderAuthenticationFailure(
'MCODE Error'
)
elif status == '2xx':
raise GeocoderQueryError(
'Has No Privilleges.'
elif status == 200:
raise GeocoderAuthenticationFailure(
'Invalid AK'
)
elif status == 211:
raise GeocoderAuthenticationFailure(
'Invalid SK'
)
elif status == '3xx':
elif 200 <= status <= 300:
raise GeocoderAuthenticationFailure(
'Authentication Failure'
)
elif 301 <= status <= 402:
raise GeocoderQuotaExceeded(
'Quota Error.'
)
else:
raise GeocoderQueryError('Unknown error')

def _url_with_signature(self, quoted_params):
"""
Return the request url with signature.
qutoted_params is the escaped str of params in request.
"""

# Since quoted_params is escaped by urlencode, don't apply quote twice
raw = self.api_path + '?' + quoted_params + self.security_key
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the docs at http://lbsyun.baidu.com/index.php?title=lbscloud/api/appendix , the quote(params, safe="/:=&?#+!$,;'@()*[]") code is used for getting a raw string, while there we have urlencode(params). Wouldn't that cause different hashes for peculiar requests? I think the relevant test cases should be added. For example, a one with a comma: locator.geocode("beijing, china").

Copy link
Author

@jinzhc jinzhc May 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The quoted_params in self._url_with_signature was given by urlencode. urlencode has already called quote_plus on parameters internally. Except parameters, quote never escape the characters in site/path/key parts, because they are not special characters.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the list of safe characters match the one used in urlencode internally? For example, I see that comma is in that list, however urlencode uses percent encoding on commas, IIRC.

A test wouldn't harm in either way.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this patch code doesn't match the given sample code in docs. But it works.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It sure works, but for some cases it might not work. Could you please add the test with a comma, and fix the code, if that fails?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to stick to docs. I'm going to change urlencode(params) to urlencode(params, safe="!*'();:@&=+$,/?#[]", quote_via=quote). Will you accept this change?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add the test first :)

But yeah, that would be better than crafting the query string manually. Even better if all of this is not required and a simple urlencode works.

sn = hashlib.md5(quote_plus(raw).encode('utf-8')).hexdigest()
return self.api + '?' + quoted_params + '&sn=' + sn
69 changes: 66 additions & 3 deletions test/geocoders/baidu.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,25 @@
from geopy.point import Point
from geopy.geocoders import Baidu
from test.geocoders.util import GeocoderTestBase, env
from geopy.exc import GeocoderAuthenticationFailure


class BaiduTestCaseUnitTest(GeocoderTestBase):

def test_user_agent_custom(self):
geocoder = Baidu(
@classmethod
def setUpClass(cls):
cls.geocoder = Baidu(
api_key='DUMMYKEY1234',
user_agent='my_user_agent/1.0'
)
self.assertEqual(geocoder.headers['User-Agent'], 'my_user_agent/1.0')

def test_user_agent_custom(self):
self.assertEqual(self.geocoder.headers['User-Agent'], 'my_user_agent/1.0')

def test_invalid_ak(self):
with self.assertRaises(GeocoderAuthenticationFailure) as cm:
self.geocode_run({"query": u("baidu")}, None)
self.assertEqual(str(cm.exception), 'Invalid AK')


@unittest.skipUnless(
Expand Down Expand Up @@ -55,3 +64,57 @@ def test_reverse_point(self):
{"query": Point(39.983615544507, 116.32295155093), "exactly_one": False},
{"latitude": 39.983615544507, "longitude": 116.32295155093},
)


@unittest.skipUnless(
bool(env.get('BAIDU_KEY_REQUIRES_SK')) and bool(env.get('BAIDU_SEC_KEY')),
"BAIDU_KEY_REQUIRES_SK and BAIDU_SEC_KEY env variables not set"
)
class BaiduSKTestCase(GeocoderTestBase):

@classmethod
def setUpClass(cls):
cls.geocoder = Baidu(
api_key=env['BAIDU_KEY_REQUIRES_SK'],
security_key=env['BAIDU_SEC_KEY'],
timeout=3,
)
cls.delta_exact = 0.02

def test_basic_address(self):
"""
Baidu.geocode
"""
self.geocode_run(
{"query": u(
"\u5317\u4eac\u5e02\u6d77\u6dc0\u533a"
"\u4e2d\u5173\u6751\u5927\u885727\u53f7"
)},
{"latitude": 39.983615544507, "longitude": 116.32295155093},
)

def test_reverse_point(self):
"""
Baidu.reverse Point
"""
self.reverse_run(
{"query": Point(39.983615544507, 116.32295155093)},
{"latitude": 39.983615544507, "longitude": 116.32295155093},
)
self.reverse_run(
{"query": Point(39.983615544507, 116.32295155093), "exactly_one": False},
{"latitude": 39.983615544507, "longitude": 116.32295155093},
)

def test_safe_within_signature(self):
"""
Baidu signature calculation with safe characters
"""
self.geocode_run(
{"query": u(
"\u5317\u4eac\u5e02\u6d77\u6dc0\u533a"
"\u4e2d\u5173\u6751\u5927\u885727\u53f7"
"!*'();:@&=+$,/?[] %"
)},
{"latitude": 39.983615544507, "longitude": 116.32295155093},
)
2 changes: 2 additions & 0 deletions test/geocoders/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
'LIVESTREETS_AUTH_TOKEN',
'GEOCODEFARM_KEY',
'BAIDU_KEY',
'BAIDU_KEY_REQUIRES_SK',
'BAIDU_SEC_KEY',
'OPENCAGE_KEY',
'OPENMAPQUEST_APIKEY',
'PICKPOINT_KEY',
Expand Down