Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
82 changes: 17 additions & 65 deletions ecommerce_api_client/client.py
Original file line number Diff line number Diff line change
@@ -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))
7 changes: 7 additions & 0 deletions ecommerce_api_client/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# noqa pylint: skip-file
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jimabramson Any opposition to the imports here? Consumers are slightly cleaner since they don't have to import slumber directly.

Choose a reason for hiding this comment

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

that seems good, yeah.


# noinspection PyUnresolvedReferences
from slumber.exceptions import *

# noinspection PyUnresolvedReferences
from requests.exceptions import Timeout
84 changes: 21 additions & 63 deletions ecommerce_api_client/tests/test_client.py
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down