diff --git a/tests/client_test.py b/tests/client_test.py index b61d9b4..a25c099 100644 --- a/tests/client_test.py +++ b/tests/client_test.py @@ -1,103 +1,32 @@ # -*- coding: UTF-8 -*- -import io -import json - import mock import pytest import six -from tests.testing import resource_filename from yelp.client import Client -from yelp.oauth1_authenticator import Oauth1Authenticator -from yelp.obj.business_response import BusinessResponse -from yelp.obj.search_response import SearchResponse class TestClient(object): - sample_location = 'San Francisco, CA' - @classmethod def setup_class(cls): - with io.open(resource_filename('json/credentials.json')) as cred: - test_creds = json.load(cred) - auth = Oauth1Authenticator(**test_creds) - cls.client = Client(auth) - - with io.open(resource_filename('json/search_response.json')) as resp: - cls.search_response = json.load(resp) - with io.open(resource_filename('json/business_response.json')) as resp: - cls.business_response = json.load(resp) - - def test_get_business_builds_correct_params(self): - with mock.patch('yelp.client.Client._make_request') as request: - request.return_value = self.business_response - response = self.client.get_business('test-id') - request.assert_called_once_with('/v2/business/test-id', {}) - assert type(response) is BusinessResponse - - def test_get_business_builds_correct_params_with_lang(self): - with mock.patch('yelp.client.Client._make_request') as request: - request.return_value = self.business_response - params = {'lang': 'fr'} - self.client.get_business('test-id', **params) - request.assert_called_once_with('/v2/business/test-id', params) - - def test_search_builds_correct_params(self): - with mock.patch('yelp.client.Client._make_request') as request: - request.return_value = self.search_response - params = { - 'term': 'food', - } - response = self.client.search(self.sample_location, **params) - params.update({ - 'location': self.sample_location - }) - request.assert_called_once_with('/v2/search/', params) - assert type(response) is SearchResponse - - def test_search_builds_correct_params_with_current_lat_long(self): - with mock.patch('yelp.client.Client._make_request') as request: - params = { - 'term': 'food', - } - self.client.search(self.sample_location, 0, 0, **params) - params.update({ - 'location': self.sample_location, - 'cll': '0,0' - }) - request.assert_called_once_with('/v2/search/', params) - - def test_search_by_bounding_box_builds_correct_params(self): - with mock.patch('yelp.client.Client._make_request') as request: - params = { - 'term': 'food', - } - self.client.search_by_bounding_box(0, 0, 0, 0, **params) - params['bounds'] = '0,0|0,0' - request.assert_called_once_with('/v2/search/', params) - - def test_search_by_coordinates_builds_correct_params(self): - with mock.patch('yelp.client.Client._make_request') as request: - self.client.search_by_coordinates(0, 0, 0, 0, 0) - request.assert_called_once_with('/v2/search/', {'ll': '0,0,0,0,0'}) - - def test_phone_search_builds_correct_params(self): - with mock.patch('yelp.client.Client._make_request') as request: - request.return_value = self.search_response - params = { - 'category': 'fashion' - } - response = self.client.phone_search('5555555555', **params) - params['phone'] = '5555555555' - request.assert_called_once_with('/v2/phone_search/', params) - assert type(response) is SearchResponse + auth = mock.Mock() + cls.client = Client(auth) + + def test_add_instance_methods(self): + methods = [ + ('_private', 'private_method'), + ('public', 'public_method') + ] + self.client._add_instance_methods(methods) + assert self.client.public == 'public_method' + assert not hasattr(self.client, '_private') def test_make_connection_closes(self): mock_conn = mock.Mock() - mock_conn.read.return_value = b"{}" + mock_conn.read.return_value = b'{}' with mock.patch( - 'six.moves.urllib.request.urlopen', return_value=mock_conn, + 'six.moves.urllib.request.urlopen', return_value=mock_conn, ): self.client._make_connection("") mock_conn.close.assert_called_once_with() @@ -106,7 +35,7 @@ def test_make_connection_closes_with_exception(self): mock_conn = mock.Mock() mock_conn.read.side_effect = Exception with mock.patch( - 'six.moves.urllib.request.urlopen', return_value=mock_conn, + 'six.moves.urllib.request.urlopen', return_value=mock_conn, ): with pytest.raises(Exception): self.client._make_connection("") diff --git a/tests/endpoint/__init__.py b/tests/endpoint/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/endpoint/business_test.py b/tests/endpoint/business_test.py new file mode 100644 index 0000000..9a2b700 --- /dev/null +++ b/tests/endpoint/business_test.py @@ -0,0 +1,26 @@ +# -*- coding: UTF-8 -*- +import mock + +from yelp.client import Client +from yelp.obj.business_response import BusinessResponse + + +class TestBusiness(object): + + @classmethod + def setup_class(cls): + auth = mock.Mock() + cls.client = Client(auth) + + def test_get_business_builds_correct_params(self): + with mock.patch('yelp.client.Client._make_request') as request: + request.return_value = '{}' + response = self.client.get_business('test-id') + request.assert_called_once_with('/v2/business/test-id', {}) + assert type(response) is BusinessResponse + + def test_get_business_builds_correct_params_with_lang(self): + with mock.patch('yelp.client.Client._make_request') as request: + params = {'lang': 'fr'} + self.client.get_business('test-id', **params) + request.assert_called_once_with('/v2/business/test-id', params) diff --git a/tests/endpoint/phone_search_test.py b/tests/endpoint/phone_search_test.py new file mode 100644 index 0000000..e005f4c --- /dev/null +++ b/tests/endpoint/phone_search_test.py @@ -0,0 +1,24 @@ +# -*- coding: UTF-8 -*- +import mock + +from yelp.client import Client +from yelp.obj.search_response import SearchResponse + + +class TestBusiness(object): + + @classmethod + def setup_class(cls): + auth = mock.Mock() + cls.client = Client(auth) + + def test_phone_search_builds_correct_params(self): + with mock.patch('yelp.client.Client._make_request') as request: + request.return_value = '{}' + params = { + 'category': 'fashion' + } + response = self.client.phone_search('5555555555', **params) + params['phone'] = '5555555555' + request.assert_called_once_with('/v2/phone_search/', params) + assert type(response) is SearchResponse diff --git a/tests/endpoint/search_test.py b/tests/endpoint/search_test.py new file mode 100644 index 0000000..6440980 --- /dev/null +++ b/tests/endpoint/search_test.py @@ -0,0 +1,54 @@ +# -*- coding: UTF-8 -*- +import mock + +from yelp.client import Client +from yelp.obj.search_response import SearchResponse + + +class TestBusiness(object): + + sample_location = 'San Francisco, CA' + + @classmethod + def setup_class(cls): + auth = mock.Mock() + cls.client = Client(auth) + + def test_search_builds_correct_params(self): + with mock.patch('yelp.client.Client._make_request') as request: + request.return_value = '{}' + params = { + 'term': 'food', + } + response = self.client.search(self.sample_location, **params) + params.update({ + 'location': self.sample_location + }) + request.assert_called_once_with('/v2/search/', params) + assert type(response) is SearchResponse + + def test_search_builds_correct_params_with_current_lat_long(self): + with mock.patch('yelp.client.Client._make_request') as request: + params = { + 'term': 'food', + } + self.client.search(self.sample_location, 0, 0, **params) + params.update({ + 'location': self.sample_location, + 'cll': '0,0' + }) + request.assert_called_once_with('/v2/search/', params) + + def test_search_by_bounding_box_builds_correct_params(self): + with mock.patch('yelp.client.Client._make_request') as request: + params = { + 'term': 'food', + } + self.client.search_by_bounding_box(0, 0, 0, 0, **params) + params['bounds'] = '0,0|0,0' + request.assert_called_once_with('/v2/search/', params) + + def test_search_by_coordinates_builds_correct_params(self): + with mock.patch('yelp.client.Client._make_request') as request: + self.client.search_by_coordinates(0, 0, 0, 0, 0) + request.assert_called_once_with('/v2/search/', {'ll': '0,0,0,0,0'}) diff --git a/yelp/client.py b/yelp/client.py index f546b77..76dd8e6 100644 --- a/yelp/client.py +++ b/yelp/client.py @@ -1,194 +1,43 @@ # -*- coding: UTF-8 -*- +import inspect import json import six from yelp.config import API_HOST -from yelp.config import BUSINESS_PATH -from yelp.config import PHONE_SEARCH_PATH -from yelp.config import SEARCH_PATH +from yelp.endpoint.business import Business +from yelp.endpoint.phone_search import PhoneSearch +from yelp.endpoint.search import Search from yelp.errors import ErrorHandler -from yelp.obj.business_response import BusinessResponse -from yelp.obj.search_response import SearchResponse class Client(object): + _endpoints = [ + Business, + PhoneSearch, + Search + ] + def __init__(self, authenticator): self.authenticator = authenticator self._error_handler = ErrorHandler() - - def get_business(self, business_id, **url_params): - """Make a request to the business endpoint. More info at - https://www.yelp.com/developers/documentation/v2/business - - Args: - business_id (str): The business id. - **url_params: Dict corresponding to business API params - https://www.yelp.com/developers/documentation/v2/business#lParam - - Returns: - BusinessResponse object that wraps the response. - - """ - business_path = BUSINESS_PATH + business_id - return BusinessResponse(self._make_request(business_path, url_params)) - - def search( - self, - location, - current_lat=None, - current_long=None, - **url_params - ): - """Make a request to the search endpoint. Specify a location by - neighbourhood, address, or city. More info at - https://www.yelp.com/developers/documentation/v2/search_api#searchNAC - - Args: - location (str): A string that specifies location by neighbourhood, - address, or city. - current_lat (float): Optional latitude to disambiguate location. - current_long (float): Optional longitude to disambiguate location. - **url_params: Dict corresponding to search API params - https://www.yelp.com/developers/documentation/v2/search_api#searchGP - - Returns: - SearchResponse object that wraps the response. - - """ - url_params.update({ - 'location': location - }) - if current_lat is not None and current_long is not None: - url_params['cll'] = self._format_current_lat_long( - current_lat, - current_long - ) - - return SearchResponse(self._make_request(SEARCH_PATH, url_params)) - - def search_by_bounding_box( - self, - sw_latitude, - sw_longitude, - ne_latitude, - ne_longitude, - **url_params - ): - """Make a request to the search endpoint by bounding box. Specify a - southwest latitude/longitude and a northeast latitude/longitude. See - http://www.yelp.com/developers/documentation/v2/search_api#searchGBB - - Args: - sw_latitude (float): Southwest latitude of bounding box. - sw_longitude (float): Southwest longitude of bounding box. - ne_latitude (float): Northeast latitude of bounding box. - ne_longitude (float): Northeast longitude of bounding box. - **url_params: Dict corresponding to search API params - https://www.yelp.ca/developers/documentation/v2/search_api#searchGP - - Returns: - SearchResponse object that wraps the response. - - """ - url_params['bounds'] = self._format_bounds( - sw_latitude, - sw_longitude, - ne_latitude, - ne_longitude - ) - - return SearchResponse(self._make_request(SEARCH_PATH, url_params)) - - def search_by_coordinates( - self, - latitude, - longitude, - accuracy=None, - altitude=None, - altitude_accuracy=None, - **url_params - ): - """Make a request to the search endpoint by geographic coordinate. - Specify a latitude and longitude with optional accuracy, altitude, and - altitude_accuracy. More info at - http://www.yelp.com/developers/documentation/v2/search_api#searchGC - - Args: - latitude (float): Latitude of geo-point to search near. - longitude (float): Longitude of geo-point to search near. - accuracy (float): Optional accuracy of latitude, longitude. - altitude (float): Optional altitude of geo-point to search near. - altitude_accuracy (float): Optional accuracy of altitude. - **url_params: Dict corresponding to search API params - https://www.yelp.ca/developers/documentation/v2/search_api#searchGP - - Returns: - SearchResponse object that wraps the response. - - """ - url_params['ll'] = self._format_coordinates( - latitude, - longitude, - accuracy, - altitude, - altitude_accuracy - ) - - return SearchResponse(self._make_request(SEARCH_PATH, url_params)) - - def phone_search(self, phone, **url_params): - """Make a request to the phone search endpoint.More info at - https://www.yelp.com/developers/documentation/v2/phone_search - - Args: - phone (str): Business phone number to search for. - **url_params: Dict corresponding to phone search API params - https://www.yelp.com/developers/documentation/v2/phone_search - - Returns: - SearchResponse object that wraps the response. - - """ - url_params['phone'] = phone - - return SearchResponse( - self._make_request(PHONE_SEARCH_PATH, url_params) - ) - - def _format_current_lat_long(self, lat, long): - return '{0},{1}'.format(lat, long) - - def _format_bounds( - self, - sw_latitude, - sw_longitude, - ne_latitude, - ne_longitude - ): - return '{0},{1}|{2},{3}'.format( - sw_latitude, - sw_longitude, - ne_latitude, - ne_longitude - ) - - def _format_coordinates( - self, - latitude, - longitude, - accuracy, - altitude, - altitude_accuracy - ): - coord = '{0},{1}'.format(latitude, longitude) - for field in (accuracy, altitude, altitude_accuracy): - if field is not None: - coord += ',' + str(field) - else: - break - return coord + self._define_request_methods() + + # Creates an instance of each endpoint class and adds the instances' public + # singleton methods to Client. We do this to promote modularity. + def _define_request_methods(self): + endpoint_instances = [end(self) for end in self._endpoints] + for endpoint in endpoint_instances: + instance_methods = inspect.getmembers(endpoint, inspect.ismethod) + self._add_instance_methods(instance_methods) + + def _add_instance_methods(self, instance_methods): + # instance_methods is a list of (name, value) tuples where value is the + # instance of the bound method + for method in instance_methods: + if method[0][0] is not '_': + self.__setattr__(method[0], method[1]) def _make_request(self, path, url_params={}): url = 'https://{0}{1}?'.format( diff --git a/yelp/endpoint/__init__.py b/yelp/endpoint/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yelp/endpoint/business.py b/yelp/endpoint/business.py new file mode 100644 index 0000000..8cac63f --- /dev/null +++ b/yelp/endpoint/business.py @@ -0,0 +1,27 @@ +# -*- coding: UTF-8 -*- +from yelp.config import BUSINESS_PATH +from yelp.obj.business_response import BusinessResponse + + +class Business(object): + + def __init__(self, client): + self.client = client + + def get_business(self, business_id, **url_params): + """Make a request to the business endpoint. More info at + https://www.yelp.com/developers/documentation/v2/business + + Args: + business_id (str): The business id. + **url_params: Dict corresponding to business API params + https://www.yelp.com/developers/documentation/v2/business#lParam + + Returns: + BusinessResponse object that wraps the response. + + """ + business_path = BUSINESS_PATH + business_id + return BusinessResponse( + self.client._make_request(business_path, url_params) + ) diff --git a/yelp/endpoint/phone_search.py b/yelp/endpoint/phone_search.py new file mode 100644 index 0000000..e8adfc6 --- /dev/null +++ b/yelp/endpoint/phone_search.py @@ -0,0 +1,28 @@ +# -*- coding: UTF-8 -*- +from yelp.config import PHONE_SEARCH_PATH +from yelp.obj.search_response import SearchResponse + + +class PhoneSearch(object): + + def __init__(self, client): + self.client = client + + def phone_search(self, phone, **url_params): + """Make a request to the phone search endpoint.More info at + https://www.yelp.com/developers/documentation/v2/phone_search + + Args: + phone (str): Business phone number to search for. + **url_params: Dict corresponding to phone search API params + https://www.yelp.com/developers/documentation/v2/phone_search + + Returns: + SearchResponse object that wraps the response. + + """ + url_params['phone'] = phone + + return SearchResponse( + self.client._make_request(PHONE_SEARCH_PATH, url_params) + ) diff --git a/yelp/endpoint/search.py b/yelp/endpoint/search.py new file mode 100644 index 0000000..4c96823 --- /dev/null +++ b/yelp/endpoint/search.py @@ -0,0 +1,152 @@ +# -*- coding: UTF-8 -*- +from yelp.config import SEARCH_PATH +from yelp.obj.search_response import SearchResponse + + +class Search(object): + + def __init__(self, client): + self.client = client + + def search( + self, + location, + current_lat=None, + current_long=None, + **url_params + ): + """Make a request to the search endpoint. Specify a location by + neighbourhood, address, or city. More info at + https://www.yelp.com/developers/documentation/v2/search_api#searchNAC + + Args: + location (str): A string that specifies location by neighbourhood, + address, or city. + current_lat (float): Optional latitude to disambiguate location. + current_long (float): Optional longitude to disambiguate location. + **url_params: Dict corresponding to search API params + https://www.yelp.com/developers/documentation/v2/search_api#searchGP + + Returns: + SearchResponse object that wraps the response. + + """ + url_params.update({ + 'location': location + }) + if current_lat is not None and current_long is not None: + url_params['cll'] = self._format_current_lat_long( + current_lat, + current_long + ) + + return SearchResponse( + self.client._make_request(SEARCH_PATH, url_params) + ) + + def search_by_bounding_box( + self, + sw_latitude, + sw_longitude, + ne_latitude, + ne_longitude, + **url_params + ): + """Make a request to the search endpoint by bounding box. Specify a + southwest latitude/longitude and a northeast latitude/longitude. See + http://www.yelp.com/developers/documentation/v2/search_api#searchGBB + + Args: + sw_latitude (float): Southwest latitude of bounding box. + sw_longitude (float): Southwest longitude of bounding box. + ne_latitude (float): Northeast latitude of bounding box. + ne_longitude (float): Northeast longitude of bounding box. + **url_params: Dict corresponding to search API params + https://www.yelp.ca/developers/documentation/v2/search_api#searchGP + + Returns: + SearchResponse object that wraps the response. + + """ + url_params['bounds'] = self._format_bounds( + sw_latitude, + sw_longitude, + ne_latitude, + ne_longitude + ) + + return SearchResponse( + self.client._make_request(SEARCH_PATH, url_params) + ) + + def search_by_coordinates( + self, + latitude, + longitude, + accuracy=None, + altitude=None, + altitude_accuracy=None, + **url_params + ): + """Make a request to the search endpoint by geographic coordinate. + Specify a latitude and longitude with optional accuracy, altitude, and + altitude_accuracy. More info at + http://www.yelp.com/developers/documentation/v2/search_api#searchGC + + Args: + latitude (float): Latitude of geo-point to search near. + longitude (float): Longitude of geo-point to search near. + accuracy (float): Optional accuracy of latitude, longitude. + altitude (float): Optional altitude of geo-point to search near. + altitude_accuracy (float): Optional accuracy of altitude. + **url_params: Dict corresponding to search API params + https://www.yelp.ca/developers/documentation/v2/search_api#searchGP + + Returns: + SearchResponse object that wraps the response. + + """ + url_params['ll'] = self._format_coordinates( + latitude, + longitude, + accuracy, + altitude, + altitude_accuracy + ) + + return SearchResponse( + self.client._make_request(SEARCH_PATH, url_params) + ) + + def _format_current_lat_long(self, lat, long): + return '{0},{1}'.format(lat, long) + + def _format_bounds( + self, + sw_latitude, + sw_longitude, + ne_latitude, + ne_longitude + ): + return '{0},{1}|{2},{3}'.format( + sw_latitude, + sw_longitude, + ne_latitude, + ne_longitude + ) + + def _format_coordinates( + self, + latitude, + longitude, + accuracy, + altitude, + altitude_accuracy + ): + coord = '{0},{1}'.format(latitude, longitude) + for field in (accuracy, altitude, altitude_accuracy): + if field is not None: + coord += ',' + str(field) + else: + break + return coord