Skip to content

Support searching with multiple place categories. #25

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion simplegeo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 13 additions & 9 deletions simplegeo/places/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -59,19 +59,20 @@ 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.")

if isinstance(query, unicode):
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:
Expand Down Expand Up @@ -99,21 +100,22 @@ 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.")

if isinstance(query, unicode):
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:
Expand Down Expand Up @@ -142,19 +144,20 @@ 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.")

if isinstance(query, unicode):
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:
Expand Down Expand Up @@ -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.")

Expand All @@ -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:
Expand Down
23 changes: 23 additions & 0 deletions simplegeo/test/client/test_places.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
Expand Down
15 changes: 15 additions & 0 deletions simplegeo/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand Down