diff --git a/.travis.yml b/.travis.yml index bf4eae8..08e2e2b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,9 @@ language: python python: - "2.7" sudo: false +branches: + only: + - master # Cache the pip directory. "cache: pip" doesn't work due to install override. See https://github.com/travis-ci/travis-ci/issues/3239. cache: diff --git a/ecommerce_api_client/client.py b/ecommerce_api_client/client.py index 9c4e91f..acee3c8 100644 --- a/ecommerce_api_client/client.py +++ b/ecommerce_api_client/client.py @@ -1,79 +1,31 @@ -import datetime - import requests import slumber from ecommerce_api_client.auth import JwtAuth -class EcommerceApiClient(object): - """ E-Commerce API client. """ - - DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ' +class EcommerceApiClient(slumber.API): + DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ" def __init__(self, url, signing_key, username, email, timeout=5): - """ Instantiate a new client. """ - session = requests.Session() - session.timeout = timeout - self.api = slumber.API(url, session=session, auth=JwtAuth(username, email, signing_key)) - - def _format_order(self, order): - order['date_placed'] = datetime.datetime.strptime(order['date_placed'], self.DATETIME_FORMAT) - return order - - def get_order(self, order_number): - """ - Retrieve a paid order. - - Arguments - user -- User associated with the requested order. - order_number -- The unique identifier for the order. - - Returns a tuple with the order number, order status, API response data. """ - order = self.api.orders(order_number).get() - self._format_order(order) - return order + Instantiate a new client. - def get_processors(self): + Raises + ValueError if any of the arguments--url, signing_key, username, email--are non-truthy values. """ - Retrieve the list of available payment processors. - Returns a list of strings. - """ - return self.api.payment.processors.get() - - def create_basket(self, sku, payment_processor=None): - """Create a new basket and immediately trigger checkout. - - Note that while the API supports deferring checkout to a separate step, - as well as adding multiple products to the basket, this client does not - currently need that capability, so that case is not supported. + args = (('url', url), ('signing_key', signing_key), ('username', username), ('email', email)) + invalid_fields = [] + for field, value in args: + if not value: + invalid_fields.append(field) - Args: - user: the django.auth.User for which the basket should be created. - sku: a string containing the SKU of the course seat being ordered. - payment_processor: (optional) the name of the payment processor to - use for checkout. + if invalid_fields: + raise ValueError( + 'Cannot instantiate API client. Values for the following fields are invalid: {}'.format( + ', '.join(invalid_fields))) - Returns: - A dictionary containing {id, order, payment_data}. - - Raises: - TimeoutError: the request to the API server timed out. - InvalidResponseError: the API server response was not understood. - """ - data = {'products': [{'sku': sku}], 'checkout': True, 'payment_processor_name': payment_processor} - return self.api.baskets.post(data) - - def get_basket_order(self, basket_id): - """ Retrieve an order associated with a basket. """ - order = self.api.baskets(basket_id).order.get() - self._format_order(order) - return order - - def get_orders(self): - """ Retrieve all orders for a user. """ - orders = self.api.orders.get()['results'] - orders = [self._format_order(order) for order in orders] - return orders + session = requests.Session() + session.timeout = timeout + super(EcommerceApiClient, self).__init__(url, session=session, auth=JwtAuth(username, email, signing_key)) diff --git a/ecommerce_api_client/exceptions.py b/ecommerce_api_client/exceptions.py new file mode 100644 index 0000000..39992a2 --- /dev/null +++ b/ecommerce_api_client/exceptions.py @@ -0,0 +1,7 @@ +# noqa pylint: skip-file + +# noinspection PyUnresolvedReferences +from slumber.exceptions import * + +# noinspection PyUnresolvedReferences +from requests.exceptions import Timeout diff --git a/ecommerce_api_client/tests/test_client.py b/ecommerce_api_client/tests/test_client.py index 571a5c0..536b98b 100644 --- a/ecommerce_api_client/tests/test_client.py +++ b/ecommerce_api_client/tests/test_client.py @@ -1,72 +1,30 @@ -import json from unittest import TestCase -from datetime import datetime -import httpretty +import ddt from ecommerce_api_client.client import EcommerceApiClient -@httpretty.activate -class EcommerceApiClientTests(TestCase): - """ Tests for the E-Commerce API client. """ - api_url = 'http://example.com/api/v2' - date_placed_string = '2015-01-01T00:00:00Z' - date_placed = datetime.strptime(date_placed_string, EcommerceApiClient.DATETIME_FORMAT) - - def setUp(self): - super(EcommerceApiClientTests, self).setUp() - self.client = EcommerceApiClient(self.api_url, 'edx', 'edx', 'edx@example.com') - - def _mock_api_response(self, path, body, method=httpretty.GET): - url = self.api_url + path - httpretty.register_uri(method, url, body=json.dumps(body), content_type='application/json') - - def test_get_order(self): - """ Verify the API retrieves an order. """ - order_number = 'EDX-10001' - body = {'date_placed': self.date_placed_string} - self._mock_api_response('/orders/{}/'.format(order_number), body) - - order = self.client.get_order(order_number) - self.assertEqual(order['date_placed'], self.date_placed) - - def test_get_basket_order(self): - """ Verify the API retrieves an order associated with a basket. """ - basket_id = '10001' - body = {'date_placed': self.date_placed_string} - self._mock_api_response('/baskets/{}/order/'.format(basket_id), body) - - order = self.client.get_basket_order(basket_id) - self.assertEqual(order['date_placed'], self.date_placed) +URL = 'http://example.com/api/v2' +SIGNING_KEY = 'edx' +USERNAME = 'edx' +EMAIL = 'edx@example.com' - def test_get_orders(self): - """ Verify the API retrieves a list of orders. """ - body = {'results': [{'date_placed': self.date_placed_string}]} - self._mock_api_response('/orders/', body) - - orders = self.client.get_orders() - self.assertEqual(len(orders), 1) - self.assertEqual(orders[0]['date_placed'], self.date_placed) - - def test_get_processors(self): - """ Verify the API retrieves the list of payment processors. """ - body = ['cybersource', 'paypal'] - self._mock_api_response('/payment/processors/', body) - - processors = self.client.get_processors() - self.assertEqual(processors, body) - - def test_create_basket(self): - """ Verify the API creates a new basket. """ - sku = 'test-product' - payment_processor = 'cybersource' - - self._mock_api_response('/baskets/', {}, httpretty.POST) - self.client.create_basket(sku, payment_processor) +@ddt.ddt +class EcommerceApiClientTests(TestCase): + """ Tests for the E-Commerce API client. """ - request_body = httpretty.last_request().body - expected = json.dumps( - {'products': [{'sku': sku}], 'checkout': True, 'payment_processor_name': payment_processor}) - self.assertEqual(request_body, expected) + def test_valid_configuration(self): + """ The constructor should return successfully if all arguments are valid. """ + EcommerceApiClient(URL, SIGNING_KEY, USERNAME, EMAIL) + + @ddt.data( + (None, SIGNING_KEY, USERNAME, EMAIL), + (URL, None, USERNAME, EMAIL), + (URL, SIGNING_KEY, None, EMAIL), + (URL, SIGNING_KEY, USERNAME, None), + ) + def test_invalid_configuration(self, args): + """ If the constructor arguments are invalid, an InvalidConfigurationError should be raised. """ + self.assertRaises(ValueError, EcommerceApiClient, *args) diff --git a/requirements.txt b/requirements.txt index 6d9c6b9..788481e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ slumber==0.7.0 # Testing coverage==3.7.1 +ddt==1.0.0 httpretty==0.8.8 nose==1.3.6 pep8==1.6.2 diff --git a/setup.py b/setup.py index ec071a7..c0b7e86 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='ecommerce-api-client', - version='0.1.0', + version='0.2.0', packages=['ecommerce_api_client'], url='https://github.com/edx/ecommerce-api-client', description='Client used to access edX E-Commerce Service', @@ -12,6 +12,7 @@ ], tests_require=[ 'coverage==3.7.1', + 'ddt==1.0.0', 'httpretty==0.8.8', 'nose==1.3.6', 'pep8==1.6.2',