From 1b890816631c2be084a2760bbc977448199a6fbc Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Tue, 16 Aug 2011 17:44:09 -0700 Subject: [PATCH] Support searching with multiple place categories. The existing 'category' search parameter can now also be a sequence of category names. Backwards compatibility with single string-based category searches has been preserved. This change requires a patch to the python-oauth2 library that adds support for repeated URL parameters (used by the place search API): https://github.com/simplegeo/python-oauth2/pull/86 --- simplegeo/__init__.py | 2 +- simplegeo/places/__init__.py | 22 +++++++++++++--------- simplegeo/test/client/test_places.py | 23 +++++++++++++++++++++++ simplegeo/util.py | 15 +++++++++++++++ 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/simplegeo/__init__.py b/simplegeo/__init__.py index 7314de5..5091ba1 100644 --- a/simplegeo/__init__.py +++ b/simplegeo/__init__.py @@ -122,7 +122,7 @@ def _request(self, endpoint, method, data=None): body = None params = {} if method == 'GET' and isinstance(data, dict) and len(data) > 0: - endpoint = endpoint + '?' + urllib.urlencode(data) + endpoint = endpoint + '?' + urllib.urlencode(data, True) else: if isinstance(data, dict): body = urllib.urlencode(data) diff --git a/simplegeo/places/__init__.py b/simplegeo/places/__init__.py index e2be737..d2131fd 100644 --- a/simplegeo/places/__init__.py +++ b/simplegeo/places/__init__.py @@ -1,8 +1,8 @@ import urllib from simplegeo.util import (json_decode, APIError, SIMPLEGEOHANDLE_RSTR, - is_valid_lat, is_valid_lon, _assert_valid_lat, _assert_valid_lon, + _assert_valid_category, is_valid_ip, is_numeric, is_simplegeohandle) from simplegeo.models import Feature from simplegeo import Client as ParentClient @@ -59,12 +59,11 @@ def search(self, lat, lon, radius=None, query=None, category=None, num=None): """Search for places near a lat/lon, within a radius (in kilometers).""" _assert_valid_lat(lat) _assert_valid_lon(lon) + _assert_valid_category(category) if (radius and not is_numeric(radius)): raise ValueError("Radius must be numeric.") if (query and not isinstance(query, basestring)): raise ValueError("Query must be a string.") - if (category and not isinstance(category, basestring)): - raise ValueError("Category must be a string.") if (num and not is_numeric(num)): raise ValueError("Num parameter must be numeric.") @@ -72,6 +71,8 @@ def search(self, lat, lon, radius=None, query=None, category=None, num=None): query = query.encode('utf-8') if isinstance(category, unicode): category = category.encode('utf-8') + if isinstance(category, (list, tuple)): + category = [s.encode('utf-8') for s in category] kwargs = { } if radius: @@ -99,14 +100,13 @@ def search_by_ip(self, ipaddr, radius=None, query=None, category=None, num=None) ipaddr and then does the same thing as search(), using that guessed latitude and longitude. """ + _assert_valid_category(category) if not is_valid_ip(ipaddr): raise ValueError("Address %s is not a valid IP" % ipaddr) if (radius and not is_numeric(radius)): raise ValueError("Radius must be numeric.") if (query and not isinstance(query, basestring)): raise ValueError("Query must be a string.") - if (category and not isinstance(category, basestring)): - raise ValueError("Category must be a string.") if (num and not is_numeric(num)): raise ValueError("Num parameter must be numeric.") @@ -114,6 +114,8 @@ def search_by_ip(self, ipaddr, radius=None, query=None, category=None, num=None) query = query.encode('utf-8') if isinstance(category, unicode): category = category.encode('utf-8') + if isinstance(category, (list, tuple)): + category = [s.encode('utf-8') for s in category] kwargs = { } if radius: @@ -142,12 +144,11 @@ def search_by_my_ip(self, radius=None, query=None, category=None, num=None): HTTP proxy device between you and the server), and then does the same thing as search_by_ip(), using that IP address. """ + _assert_valid_category(category) if (radius and not is_numeric(radius)): raise ValueError("Radius must be numeric.") if (query and not isinstance(query, basestring)): raise ValueError("Query must be a string.") - if (category and not isinstance(category, basestring)): - raise ValueError("Category must be a string.") if (num and not is_numeric(num)): raise ValueError("Num parameter must be numeric.") @@ -155,6 +156,8 @@ def search_by_my_ip(self, radius=None, query=None, category=None, num=None): query = query.encode('utf-8') if isinstance(category, unicode): category = category.encode('utf-8') + if isinstance(category, (list, tuple)): + category = [s.encode('utf-8') for s in category] kwargs = { } if radius: @@ -182,14 +185,13 @@ def search_by_address(self, address, radius=None, query=None, category=None, num street address and then does the same thing as search(), using that deduced latitude and longitude. """ + _assert_valid_category(category) if not isinstance(address, basestring) or not address.strip(): raise ValueError("Address must be a non-empty string.") if (radius and not is_numeric(radius)): raise ValueError("Radius must be numeric.") if (query and not isinstance(query, basestring)): raise ValueError("Query must be a string.") - if (category and not isinstance(category, basestring)): - raise ValueError("Category must be a string.") if (num and not is_numeric(num)): raise ValueError("Num parameter must be numeric.") @@ -199,6 +201,8 @@ def search_by_address(self, address, radius=None, query=None, category=None, num query = query.encode('utf-8') if isinstance(category, unicode): category = category.encode('utf-8') + if isinstance(category, (list, tuple)): + category = [s.encode('utf-8') for s in category] kwargs = { 'address': address } if radius: diff --git a/simplegeo/test/client/test_places.py b/simplegeo/test/client/test_places.py index f1299da..e1393ba 100644 --- a/simplegeo/test/client/test_places.py +++ b/simplegeo/test/client/test_places.py @@ -307,6 +307,29 @@ def test_search(self): self.assertEqual(mockhttp.method_calls[0][1][0], 'http://api.simplegeo.com:80/%s/places/%s,%s.json?q=monkeys&category=animal' % (API_VERSION, lat, lon)) self.assertEqual(mockhttp.method_calls[0][1][1], 'GET') + def test_search_with_multiple_categories(self): + rec1 = Feature((D('11.03'), D('10.04')), simplegeohandle='SG_abcdefghijkmlnopqrstuv', properties={'name': "Bob's House Of Monkeys", 'category': "monkey dealership"}) + rec2 = Feature((D('11.03'), D('10.05')), simplegeohandle='SG_abcdefghijkmlnopqrstuv', properties={'name': "Monkey Food 'R' Us", 'category': "pet food store"}) + + mockhttp = mock.Mock() + mockhttp.request.return_value = ({'status': '200', 'content-type': 'application/json', }, json.dumps({'type': "FeatureColllection", 'features': [rec1.to_dict(), rec2.to_dict()]})) + self.client.places.http = mockhttp + + self.failUnlessRaises(ValueError, self.client.places.search, -91, 100) + self.failUnlessRaises(ValueError, self.client.places.search, -81, 361) + self.failUnlessRaises(ValueError, self.client.places.search, -0, 0, category=10) + self.failUnlessRaises(ValueError, self.client.places.search, -0, 0, category=[10]) + + lat = D('11.03') + lon = D('10.04') + res = self.client.places.search(lat, lon, query='monkeys', category=['animal', 'mineral']) + self.failUnless(isinstance(res, (list, tuple)), (repr(res), type(res))) + self.failUnlessEqual(len(res), 2) + self.failUnless(all(isinstance(f, Feature) for f in res)) + self.assertEqual(mockhttp.method_calls[0][0], 'request') + self.assertEqual(mockhttp.method_calls[0][1][0], 'http://api.simplegeo.com:80/%s/places/%s,%s.json?q=monkeys&category=animal&category=mineral' % (API_VERSION, lat, lon)) + self.assertEqual(mockhttp.method_calls[0][1][1], 'GET') + def test_search_by_ip_nonascii(self): rec1 = Feature((D('11.03'), D('10.04')), simplegeohandle='SG_abcdefghijkmlnopqrstuv', properties={'name': u"Bob's House Of M❤nkeys", 'category': u"m❤nkey dealership"}) rec2 = Feature((D('11.03'), D('10.05')), simplegeohandle='SG_abcdefghijkmlnopqrstuv', properties={'name': u"M❤nkey Food 'R' Us", 'category': "pet food store"}) diff --git a/simplegeo/util.py b/simplegeo/util.py index 9b355c8..8712d07 100644 --- a/simplegeo/util.py +++ b/simplegeo/util.py @@ -32,6 +32,10 @@ def _assert_valid_lon(x, strict=False): if not is_valid_lon(x, strict=strict): raise ValueError("not a valid lon (strict=%s): %s" % (strict, x,)) +def _assert_valid_category(c): + if c is not None and not is_valid_category(c): + raise ValueError("Category must be a string or sequence of strings.") + def is_valid_lat(x): return is_numeric(x) and (x <= 90) and (x >= -90) @@ -56,6 +60,17 @@ def is_valid_lon(x, strict=False): else: return is_numeric(x) and (x <= 360) and (x >= -360) +def is_valid_category(c): + """A category must be a string or a sequence of strings.""" + if isinstance(c, basestring): + return True + if isinstance(c, (list, tuple)): + for item in c: + if not isinstance(item, basestring): + return False + return True + return False + def deep_validate_lat_lon(struc, strict_lon_validation=False): """ For the meaning of strict_lon_validation, please see the function